mirror of
https://github.com/langgenius/dify.git
synced 2026-04-19 18:27:27 +08:00
Compare commits
101 Commits
feat/knowl
...
feat/webap
| Author | SHA1 | Date | |
|---|---|---|---|
| fc3d3e0565 | |||
| 3761944a3f | |||
| 09f8da1429 | |||
| fcc274d679 | |||
| bfa5828259 | |||
| 455d14296f | |||
| d1a25e54e5 | |||
| 9462ed7bbf | |||
| c6e63ac816 | |||
| a27db51b83 | |||
| e52a9fbfb7 | |||
| 2af1dd6de3 | |||
| 509733fbf0 | |||
| 7770a45253 | |||
| bafdbade52 | |||
| fa76590c24 | |||
| d5b75470e4 | |||
| 5f87bdbe3a | |||
| cb13b53ccd | |||
| a1dc3cfdec | |||
| 7a4ec9cf23 | |||
| 4785c061a9 | |||
| 4105c8ff70 | |||
| b922c8c215 | |||
| cbea30e65f | |||
| e9a207b38e | |||
| 5e50570739 | |||
| 46d43e6758 | |||
| 1045f6db7a | |||
| 50d36612f0 | |||
| e38631db8a | |||
| 7f63cd52a2 | |||
| 5b357fdbf0 | |||
| 9283a5414f | |||
| 8923e64b8d | |||
| 2a2a0e9be9 | |||
| 061a765b7d | |||
| acd7fead87 | |||
| 64e9d96d84 | |||
| d27de3818c | |||
| bbb080d5b2 | |||
| 8c025abb3b | |||
| c01d8a70f3 | |||
| 98606ca558 | |||
| adf3e18ebd | |||
| 1ca15989e0 | |||
| 8b5a3a9424 | |||
| 42ddcf1edd | |||
| 21561df10f | |||
| 4327ec8c4c | |||
| bbc5ec8301 | |||
| 4a51a72c1d | |||
| 4b6adffa8e | |||
| c7fd73d330 | |||
| 8a709e445a | |||
| f02b77b99f | |||
| abc625bcce | |||
| b6bc1f8bc4 | |||
| b8f9037cd3 | |||
| 02606ba3c7 | |||
| 79311d3fb5 | |||
| 31086a1fbf | |||
| 6ae5d052e5 | |||
| c794ecf101 | |||
| d887aae012 | |||
| 1b1e96eff7 | |||
| eecd091063 | |||
| d38f2cb380 | |||
| 56aaee5558 | |||
| d72b4752c9 | |||
| ea769c6483 | |||
| ec194fa3d4 | |||
| b877039859 | |||
| 54634f26d2 | |||
| 3bef91a2cd | |||
| 7da45ba589 | |||
| e0232c67cc | |||
| 1dc4a229d4 | |||
| 0e0bada1f3 | |||
| 5366a814f9 | |||
| f1240a22db | |||
| 66f35c2b7e | |||
| 766ee48531 | |||
| 083045f45c | |||
| fe237802c9 | |||
| 00b923651f | |||
| 24fce3cc64 | |||
| 8ba969f67d | |||
| 6844d59371 | |||
| fe5529db85 | |||
| d89034d913 | |||
| 360fbeb108 | |||
| e7c2fa1cfa | |||
| 735f09d977 | |||
| f83a5e3e49 | |||
| 01a8d4efcc | |||
| fdb1e649d4 | |||
| 0856792a57 | |||
| 0e33a3aa5f | |||
| d3895bcd6b | |||
| eeb390650b |
2
.github/workflows/build-push.yml
vendored
2
.github/workflows/build-push.yml
vendored
@ -5,6 +5,8 @@ on:
|
|||||||
branches:
|
branches:
|
||||||
- "main"
|
- "main"
|
||||||
- "deploy/dev"
|
- "deploy/dev"
|
||||||
|
- "deploy/enterprise"
|
||||||
|
- "e-0154"
|
||||||
release:
|
release:
|
||||||
types: [published]
|
types: [published]
|
||||||
|
|
||||||
|
|||||||
29
.github/workflows/deploy-enterprise.yml
vendored
Normal file
29
.github/workflows/deploy-enterprise.yml
vendored
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
name: Deploy Enterprise
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_run:
|
||||||
|
workflows: ["Build and Push API & Web"]
|
||||||
|
branches:
|
||||||
|
- "deploy/enterprise"
|
||||||
|
types:
|
||||||
|
- completed
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
deploy:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: |
|
||||||
|
github.event.workflow_run.conclusion == 'success' &&
|
||||||
|
github.event.workflow_run.head_branch == 'deploy/enterprise'
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Deploy to server
|
||||||
|
uses: appleboy/ssh-action@v0.1.8
|
||||||
|
with:
|
||||||
|
host: ${{ secrets.ENTERPRISE_SSH_HOST }}
|
||||||
|
username: ${{ secrets.ENTERPRISE_SSH_USER }}
|
||||||
|
password: ${{ secrets.ENTERPRISE_SSH_PASSWORD }}
|
||||||
|
script: |
|
||||||
|
${{ vars.ENTERPRISE_SSH_SCRIPT || secrets.ENTERPRISE_SSH_SCRIPT }}
|
||||||
@ -498,11 +498,6 @@ class AuthConfig(BaseSettings):
|
|||||||
default=86400,
|
default=86400,
|
||||||
)
|
)
|
||||||
|
|
||||||
FORGOT_PASSWORD_LOCKOUT_DURATION: PositiveInt = Field(
|
|
||||||
description="Time (in seconds) a user must wait before retrying password reset after exceeding the rate limit.",
|
|
||||||
default=86400,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class ModerationConfig(BaseSettings):
|
class ModerationConfig(BaseSettings):
|
||||||
"""
|
"""
|
||||||
|
|||||||
@ -9,7 +9,7 @@ class PackagingInfo(BaseSettings):
|
|||||||
|
|
||||||
CURRENT_VERSION: str = Field(
|
CURRENT_VERSION: str = Field(
|
||||||
description="Dify version",
|
description="Dify version",
|
||||||
default="0.15.3",
|
default="0.15.4",
|
||||||
)
|
)
|
||||||
|
|
||||||
COMMIT_SHA: str = Field(
|
COMMIT_SHA: str = Field(
|
||||||
|
|||||||
@ -2,30 +2,28 @@ import uuid
|
|||||||
from typing import cast
|
from typing import cast
|
||||||
|
|
||||||
from flask_login import current_user # type: ignore
|
from flask_login import current_user # type: ignore
|
||||||
from flask_restful import Resource, inputs, marshal, marshal_with, reqparse # type: ignore
|
from flask_restful import (Resource, inputs, marshal, # type: ignore
|
||||||
|
marshal_with, reqparse)
|
||||||
from sqlalchemy import select
|
from sqlalchemy import select
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
from werkzeug.exceptions import BadRequest, Forbidden, abort
|
from werkzeug.exceptions import BadRequest, Forbidden, abort
|
||||||
|
|
||||||
from controllers.console import api
|
from controllers.console import api
|
||||||
from controllers.console.app.wraps import get_app_model
|
from controllers.console.app.wraps import get_app_model
|
||||||
from controllers.console.wraps import (
|
from controllers.console.wraps import (account_initialization_required,
|
||||||
account_initialization_required,
|
cloud_edition_billing_resource_check,
|
||||||
cloud_edition_billing_resource_check,
|
enterprise_license_required,
|
||||||
enterprise_license_required,
|
setup_required)
|
||||||
setup_required,
|
|
||||||
)
|
|
||||||
from core.ops.ops_trace_manager import OpsTraceManager
|
from core.ops.ops_trace_manager import OpsTraceManager
|
||||||
from extensions.ext_database import db
|
from extensions.ext_database import db
|
||||||
from fields.app_fields import (
|
from fields.app_fields import (app_detail_fields, app_detail_fields_with_site,
|
||||||
app_detail_fields,
|
app_pagination_fields)
|
||||||
app_detail_fields_with_site,
|
|
||||||
app_pagination_fields,
|
|
||||||
)
|
|
||||||
from libs.login import login_required
|
from libs.login import login_required
|
||||||
from models import Account, App
|
from models import Account, App
|
||||||
from services.app_dsl_service import AppDslService, ImportMode
|
from services.app_dsl_service import AppDslService, ImportMode
|
||||||
from services.app_service import AppService
|
from services.app_service import AppService
|
||||||
|
from services.enterprise.enterprise_service import EnterpriseService
|
||||||
|
from services.feature_service import FeatureService
|
||||||
|
|
||||||
ALLOW_CREATE_APP_MODES = ["chat", "agent-chat", "advanced-chat", "workflow", "completion"]
|
ALLOW_CREATE_APP_MODES = ["chat", "agent-chat", "advanced-chat", "workflow", "completion"]
|
||||||
|
|
||||||
@ -67,7 +65,17 @@ class AppListApi(Resource):
|
|||||||
if not app_pagination:
|
if not app_pagination:
|
||||||
return {"data": [], "total": 0, "page": 1, "limit": 20, "has_more": False}
|
return {"data": [], "total": 0, "page": 1, "limit": 20, "has_more": False}
|
||||||
|
|
||||||
return marshal(app_pagination, app_pagination_fields)
|
if FeatureService.get_system_features().webapp_auth.enabled:
|
||||||
|
app_ids = [str(app.id) for app in app_pagination.items]
|
||||||
|
res = EnterpriseService.WebAppAuth.batch_get_app_access_mode_by_id(app_ids=app_ids)
|
||||||
|
if len(res) != len(app_ids):
|
||||||
|
raise BadRequest("Invalid app id in webapp auth")
|
||||||
|
|
||||||
|
for app in app_pagination.items:
|
||||||
|
if str(app.id) in res:
|
||||||
|
app.access_mode = res[str(app.id)].access_mode
|
||||||
|
|
||||||
|
return marshal(app_pagination, app_pagination_fields), 200
|
||||||
|
|
||||||
@setup_required
|
@setup_required
|
||||||
@login_required
|
@login_required
|
||||||
@ -111,6 +119,10 @@ class AppApi(Resource):
|
|||||||
|
|
||||||
app_model = app_service.get_app(app_model)
|
app_model = app_service.get_app(app_model)
|
||||||
|
|
||||||
|
if FeatureService.get_system_features().webapp_auth.enabled:
|
||||||
|
app_setting = EnterpriseService.WebAppAuth.get_app_access_mode_by_id(app_id=str(app_model.id))
|
||||||
|
app_model.access_mode = app_setting.access_mode
|
||||||
|
|
||||||
return app_model
|
return app_model
|
||||||
|
|
||||||
@setup_required
|
@setup_required
|
||||||
|
|||||||
@ -59,9 +59,3 @@ class EmailCodeAccountDeletionRateLimitExceededError(BaseHTTPException):
|
|||||||
error_code = "email_code_account_deletion_rate_limit_exceeded"
|
error_code = "email_code_account_deletion_rate_limit_exceeded"
|
||||||
description = "Too many account deletion emails have been sent. Please try again in 5 minutes."
|
description = "Too many account deletion emails have been sent. Please try again in 5 minutes."
|
||||||
code = 429
|
code = 429
|
||||||
|
|
||||||
|
|
||||||
class EmailPasswordResetLimitError(BaseHTTPException):
|
|
||||||
error_code = "email_password_reset_limit"
|
|
||||||
description = "Too many failed password reset attempts. Please try again in 24 hours."
|
|
||||||
code = 429
|
|
||||||
|
|||||||
@ -6,15 +6,13 @@ from flask_restful import Resource, reqparse # type: ignore
|
|||||||
|
|
||||||
from constants.languages import languages
|
from constants.languages import languages
|
||||||
from controllers.console import api
|
from controllers.console import api
|
||||||
from controllers.console.auth.error import (
|
from controllers.console.auth.error import (EmailCodeError, InvalidEmailError,
|
||||||
EmailCodeError,
|
InvalidTokenError,
|
||||||
EmailPasswordResetLimitError,
|
PasswordMismatchError)
|
||||||
InvalidEmailError,
|
from controllers.console.error import (AccountInFreezeError, AccountNotFound,
|
||||||
InvalidTokenError,
|
EmailSendIpLimitError)
|
||||||
PasswordMismatchError,
|
from controllers.console.wraps import (email_password_login_enabled,
|
||||||
)
|
setup_required)
|
||||||
from controllers.console.error import AccountInFreezeError, AccountNotFound, EmailSendIpLimitError
|
|
||||||
from controllers.console.wraps import setup_required
|
|
||||||
from events.tenant_event import tenant_was_created
|
from events.tenant_event import tenant_was_created
|
||||||
from extensions.ext_database import db
|
from extensions.ext_database import db
|
||||||
from libs.helper import email, extract_remote_ip
|
from libs.helper import email, extract_remote_ip
|
||||||
@ -28,6 +26,7 @@ from services.feature_service import FeatureService
|
|||||||
|
|
||||||
class ForgotPasswordSendEmailApi(Resource):
|
class ForgotPasswordSendEmailApi(Resource):
|
||||||
@setup_required
|
@setup_required
|
||||||
|
@email_password_login_enabled
|
||||||
def post(self):
|
def post(self):
|
||||||
parser = reqparse.RequestParser()
|
parser = reqparse.RequestParser()
|
||||||
parser.add_argument("email", type=email, required=True, location="json")
|
parser.add_argument("email", type=email, required=True, location="json")
|
||||||
@ -59,6 +58,7 @@ class ForgotPasswordSendEmailApi(Resource):
|
|||||||
|
|
||||||
class ForgotPasswordCheckApi(Resource):
|
class ForgotPasswordCheckApi(Resource):
|
||||||
@setup_required
|
@setup_required
|
||||||
|
@email_password_login_enabled
|
||||||
def post(self):
|
def post(self):
|
||||||
parser = reqparse.RequestParser()
|
parser = reqparse.RequestParser()
|
||||||
parser.add_argument("email", type=str, required=True, location="json")
|
parser.add_argument("email", type=str, required=True, location="json")
|
||||||
@ -68,10 +68,6 @@ class ForgotPasswordCheckApi(Resource):
|
|||||||
|
|
||||||
user_email = args["email"]
|
user_email = args["email"]
|
||||||
|
|
||||||
is_forgot_password_error_rate_limit = AccountService.is_forgot_password_error_rate_limit(args["email"])
|
|
||||||
if is_forgot_password_error_rate_limit:
|
|
||||||
raise EmailPasswordResetLimitError()
|
|
||||||
|
|
||||||
token_data = AccountService.get_reset_password_data(args["token"])
|
token_data = AccountService.get_reset_password_data(args["token"])
|
||||||
if token_data is None:
|
if token_data is None:
|
||||||
raise InvalidTokenError()
|
raise InvalidTokenError()
|
||||||
@ -80,15 +76,22 @@ class ForgotPasswordCheckApi(Resource):
|
|||||||
raise InvalidEmailError()
|
raise InvalidEmailError()
|
||||||
|
|
||||||
if args["code"] != token_data.get("code"):
|
if args["code"] != token_data.get("code"):
|
||||||
AccountService.add_forgot_password_error_rate_limit(args["email"])
|
|
||||||
raise EmailCodeError()
|
raise EmailCodeError()
|
||||||
|
|
||||||
AccountService.reset_forgot_password_error_rate_limit(args["email"])
|
# Verified, revoke the first token
|
||||||
return {"is_valid": True, "email": token_data.get("email")}
|
AccountService.revoke_reset_password_token(args["token"])
|
||||||
|
|
||||||
|
# Refresh token data by generating a new token
|
||||||
|
_, new_token = AccountService.generate_reset_password_token(
|
||||||
|
user_email, code=args["code"], additional_data={"phase": "reset"}
|
||||||
|
)
|
||||||
|
|
||||||
|
return {"is_valid": True, "email": token_data.get("email"), "token": new_token}
|
||||||
|
|
||||||
|
|
||||||
class ForgotPasswordResetApi(Resource):
|
class ForgotPasswordResetApi(Resource):
|
||||||
@setup_required
|
@setup_required
|
||||||
|
@email_password_login_enabled
|
||||||
def post(self):
|
def post(self):
|
||||||
parser = reqparse.RequestParser()
|
parser = reqparse.RequestParser()
|
||||||
parser.add_argument("token", type=str, required=True, nullable=False, location="json")
|
parser.add_argument("token", type=str, required=True, nullable=False, location="json")
|
||||||
@ -107,6 +110,9 @@ class ForgotPasswordResetApi(Resource):
|
|||||||
|
|
||||||
if reset_data is None:
|
if reset_data is None:
|
||||||
raise InvalidTokenError()
|
raise InvalidTokenError()
|
||||||
|
# Must use token in reset phase
|
||||||
|
if reset_data.get("phase", "") != "reset":
|
||||||
|
raise InvalidTokenError()
|
||||||
|
|
||||||
AccountService.revoke_reset_password_token(token)
|
AccountService.revoke_reset_password_token(token)
|
||||||
|
|
||||||
|
|||||||
@ -22,7 +22,7 @@ from controllers.console.error import (
|
|||||||
EmailSendIpLimitError,
|
EmailSendIpLimitError,
|
||||||
NotAllowedCreateWorkspace,
|
NotAllowedCreateWorkspace,
|
||||||
)
|
)
|
||||||
from controllers.console.wraps import setup_required
|
from controllers.console.wraps import email_password_login_enabled, setup_required
|
||||||
from events.tenant_event import tenant_was_created
|
from events.tenant_event import tenant_was_created
|
||||||
from libs.helper import email, extract_remote_ip
|
from libs.helper import email, extract_remote_ip
|
||||||
from libs.password import valid_password
|
from libs.password import valid_password
|
||||||
@ -38,6 +38,7 @@ class LoginApi(Resource):
|
|||||||
"""Resource for user login."""
|
"""Resource for user login."""
|
||||||
|
|
||||||
@setup_required
|
@setup_required
|
||||||
|
@email_password_login_enabled
|
||||||
def post(self):
|
def post(self):
|
||||||
"""Authenticate user and login."""
|
"""Authenticate user and login."""
|
||||||
parser = reqparse.RequestParser()
|
parser = reqparse.RequestParser()
|
||||||
@ -110,6 +111,7 @@ class LogoutApi(Resource):
|
|||||||
|
|
||||||
class ResetPasswordSendEmailApi(Resource):
|
class ResetPasswordSendEmailApi(Resource):
|
||||||
@setup_required
|
@setup_required
|
||||||
|
@email_password_login_enabled
|
||||||
def post(self):
|
def post(self):
|
||||||
parser = reqparse.RequestParser()
|
parser = reqparse.RequestParser()
|
||||||
parser.add_argument("email", type=email, required=True, location="json")
|
parser.add_argument("email", type=email, required=True, location="json")
|
||||||
|
|||||||
@ -23,3 +23,9 @@ class AppSuggestedQuestionsAfterAnswerDisabledError(BaseHTTPException):
|
|||||||
error_code = "app_suggested_questions_after_answer_disabled"
|
error_code = "app_suggested_questions_after_answer_disabled"
|
||||||
description = "Function Suggested questions after answer disabled."
|
description = "Function Suggested questions after answer disabled."
|
||||||
code = 403
|
code = 403
|
||||||
|
|
||||||
|
|
||||||
|
class AppAccessDeniedError(BaseHTTPException):
|
||||||
|
error_code = "access_denied"
|
||||||
|
description = "App access denied."
|
||||||
|
code = 403
|
||||||
|
|||||||
@ -1,20 +1,26 @@
|
|||||||
|
import logging
|
||||||
from datetime import UTC, datetime
|
from datetime import UTC, datetime
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from flask import request
|
from flask import request
|
||||||
from flask_login import current_user # type: ignore
|
from flask_login import current_user # type: ignore
|
||||||
from flask_restful import Resource, inputs, marshal_with, reqparse # type: ignore
|
from flask_restful import (Resource, inputs, marshal_with, # type: ignore
|
||||||
|
reqparse)
|
||||||
from sqlalchemy import and_
|
from sqlalchemy import and_
|
||||||
from werkzeug.exceptions import BadRequest, Forbidden, NotFound
|
from werkzeug.exceptions import BadRequest, Forbidden, NotFound
|
||||||
|
|
||||||
from controllers.console import api
|
from controllers.console import api
|
||||||
from controllers.console.explore.wraps import InstalledAppResource
|
from controllers.console.explore.wraps import InstalledAppResource
|
||||||
from controllers.console.wraps import account_initialization_required, cloud_edition_billing_resource_check
|
from controllers.console.wraps import (account_initialization_required,
|
||||||
|
cloud_edition_billing_resource_check)
|
||||||
from extensions.ext_database import db
|
from extensions.ext_database import db
|
||||||
from fields.installed_app_fields import installed_app_list_fields
|
from fields.installed_app_fields import installed_app_list_fields
|
||||||
from libs.login import login_required
|
from libs.login import login_required
|
||||||
from models import App, InstalledApp, RecommendedApp
|
from models import App, InstalledApp, RecommendedApp
|
||||||
from services.account_service import TenantService
|
from services.account_service import TenantService
|
||||||
|
from services.app_service import AppService
|
||||||
|
from services.enterprise.enterprise_service import EnterpriseService
|
||||||
|
from services.feature_service import FeatureService
|
||||||
|
|
||||||
|
|
||||||
class InstalledAppsListApi(Resource):
|
class InstalledAppsListApi(Resource):
|
||||||
@ -48,6 +54,23 @@ class InstalledAppsListApi(Resource):
|
|||||||
for installed_app in installed_apps
|
for installed_app in installed_apps
|
||||||
if installed_app.app is not None
|
if installed_app.app is not None
|
||||||
]
|
]
|
||||||
|
|
||||||
|
# filter out apps that user doesn't have access to
|
||||||
|
if FeatureService.get_system_features().webapp_auth.enabled:
|
||||||
|
user_id = current_user.id
|
||||||
|
res = []
|
||||||
|
for installed_app in installed_app_list:
|
||||||
|
app_code = AppService.get_app_code_by_id(str(installed_app["app"].id))
|
||||||
|
if EnterpriseService.WebAppAuth.is_user_allowed_to_access_webapp(
|
||||||
|
user_id=user_id,
|
||||||
|
app_code=app_code,
|
||||||
|
):
|
||||||
|
res.append(installed_app)
|
||||||
|
installed_app_list = res
|
||||||
|
logging.info(
|
||||||
|
f"installed_app_list: {installed_app_list}, user_id: {user_id}"
|
||||||
|
)
|
||||||
|
|
||||||
installed_app_list.sort(
|
installed_app_list.sort(
|
||||||
key=lambda app: (
|
key=lambda app: (
|
||||||
-app["is_pinned"],
|
-app["is_pinned"],
|
||||||
|
|||||||
@ -4,10 +4,14 @@ from flask_login import current_user # type: ignore
|
|||||||
from flask_restful import Resource # type: ignore
|
from flask_restful import Resource # type: ignore
|
||||||
from werkzeug.exceptions import NotFound
|
from werkzeug.exceptions import NotFound
|
||||||
|
|
||||||
|
from controllers.console.explore.error import AppAccessDeniedError
|
||||||
from controllers.console.wraps import account_initialization_required
|
from controllers.console.wraps import account_initialization_required
|
||||||
from extensions.ext_database import db
|
from extensions.ext_database import db
|
||||||
from libs.login import login_required
|
from libs.login import login_required
|
||||||
from models import InstalledApp
|
from models import InstalledApp
|
||||||
|
from services.app_service import AppService
|
||||||
|
from services.enterprise.enterprise_service import EnterpriseService
|
||||||
|
from services.feature_service import FeatureService
|
||||||
|
|
||||||
|
|
||||||
def installed_app_required(view=None):
|
def installed_app_required(view=None):
|
||||||
@ -48,6 +52,30 @@ def installed_app_required(view=None):
|
|||||||
return decorator
|
return decorator
|
||||||
|
|
||||||
|
|
||||||
|
def user_allowed_to_access_app(view=None):
|
||||||
|
def decorator(view):
|
||||||
|
@wraps(view)
|
||||||
|
def decorated(installed_app: InstalledApp, *args, **kwargs):
|
||||||
|
feature = FeatureService.get_system_features()
|
||||||
|
if feature.webapp_auth.enabled:
|
||||||
|
app_id = installed_app.app_id
|
||||||
|
app_code = AppService.get_app_code_by_id(app_id)
|
||||||
|
res = EnterpriseService.WebAppAuth.is_user_allowed_to_access_webapp(
|
||||||
|
user_id=str(current_user.id),
|
||||||
|
app_code=app_code,
|
||||||
|
)
|
||||||
|
if not res:
|
||||||
|
raise AppAccessDeniedError()
|
||||||
|
|
||||||
|
return view(installed_app, *args, **kwargs)
|
||||||
|
|
||||||
|
return decorated
|
||||||
|
if view:
|
||||||
|
return decorator(view)
|
||||||
|
return decorator
|
||||||
|
|
||||||
|
|
||||||
class InstalledAppResource(Resource):
|
class InstalledAppResource(Resource):
|
||||||
# must be reversed if there are multiple decorators
|
# must be reversed if there are multiple decorators
|
||||||
method_decorators = [installed_app_required, account_initialization_required, login_required]
|
|
||||||
|
method_decorators = [user_allowed_to_access_app, installed_app_required, account_initialization_required, login_required]
|
||||||
|
|||||||
@ -11,7 +11,8 @@ from models.model import DifySetup
|
|||||||
from services.feature_service import FeatureService, LicenseStatus
|
from services.feature_service import FeatureService, LicenseStatus
|
||||||
from services.operation_service import OperationService
|
from services.operation_service import OperationService
|
||||||
|
|
||||||
from .error import NotInitValidateError, NotSetupError, UnauthorizedAndForceLogout
|
from .error import (NotInitValidateError, NotSetupError,
|
||||||
|
UnauthorizedAndForceLogout)
|
||||||
|
|
||||||
|
|
||||||
def account_initialization_required(view):
|
def account_initialization_required(view):
|
||||||
@ -39,6 +40,17 @@ def only_edition_cloud(view):
|
|||||||
return decorated
|
return decorated
|
||||||
|
|
||||||
|
|
||||||
|
def only_enterprise_edition(view):
|
||||||
|
@wraps(view)
|
||||||
|
def decorated(*args, **kwargs):
|
||||||
|
if not dify_config.ENTERPRISE_ENABLED:
|
||||||
|
abort(404)
|
||||||
|
|
||||||
|
return view(*args, **kwargs)
|
||||||
|
|
||||||
|
return decorated
|
||||||
|
|
||||||
|
|
||||||
def only_edition_self_hosted(view):
|
def only_edition_self_hosted(view):
|
||||||
@wraps(view)
|
@wraps(view)
|
||||||
def decorated(*args, **kwargs):
|
def decorated(*args, **kwargs):
|
||||||
@ -154,3 +166,16 @@ def enterprise_license_required(view):
|
|||||||
return view(*args, **kwargs)
|
return view(*args, **kwargs)
|
||||||
|
|
||||||
return decorated
|
return decorated
|
||||||
|
|
||||||
|
|
||||||
|
def email_password_login_enabled(view):
|
||||||
|
@wraps(view)
|
||||||
|
def decorated(*args, **kwargs):
|
||||||
|
features = FeatureService.get_system_features()
|
||||||
|
if features.enable_email_password_login:
|
||||||
|
return view(*args, **kwargs)
|
||||||
|
|
||||||
|
# otherwise, return 403
|
||||||
|
abort(403)
|
||||||
|
|
||||||
|
return decorated
|
||||||
|
|||||||
@ -5,4 +5,5 @@ from libs.external_api import ExternalApi
|
|||||||
bp = Blueprint("inner_api", __name__, url_prefix="/inner/api")
|
bp = Blueprint("inner_api", __name__, url_prefix="/inner/api")
|
||||||
api = ExternalApi(bp)
|
api = ExternalApi(bp)
|
||||||
|
|
||||||
|
from . import mail
|
||||||
from .workspace import workspace
|
from .workspace import workspace
|
||||||
|
|||||||
27
api/controllers/inner_api/mail.py
Normal file
27
api/controllers/inner_api/mail.py
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
from flask_restful import (
|
||||||
|
Resource, # type: ignore
|
||||||
|
reqparse,
|
||||||
|
)
|
||||||
|
|
||||||
|
from controllers.console.wraps import setup_required
|
||||||
|
from controllers.inner_api import api
|
||||||
|
from controllers.inner_api.wraps import inner_api_only
|
||||||
|
from services.enterprise.mail_service import DifyMail, EnterpriseMailService
|
||||||
|
|
||||||
|
|
||||||
|
class EnterpriseMail(Resource):
|
||||||
|
@setup_required
|
||||||
|
@inner_api_only
|
||||||
|
def post(self):
|
||||||
|
parser = reqparse.RequestParser()
|
||||||
|
parser.add_argument("to", type=str, action="append", required=True)
|
||||||
|
parser.add_argument("subject", type=str, required=True)
|
||||||
|
parser.add_argument("body", type=str, required=True)
|
||||||
|
parser.add_argument("substitutions", type=dict, required=False)
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
EnterpriseMailService.send_mail(DifyMail(**args))
|
||||||
|
return {"message": "success"}, 200
|
||||||
|
|
||||||
|
|
||||||
|
api.add_resource(EnterpriseMail, "/enterprise/mail")
|
||||||
@ -1,12 +1,16 @@
|
|||||||
from flask_restful import marshal_with # type: ignore
|
|
||||||
|
from flask import request
|
||||||
|
from flask_restful import Resource, marshal_with, reqparse # type: ignore
|
||||||
|
|
||||||
from controllers.common import fields
|
from controllers.common import fields
|
||||||
from controllers.common import helpers as controller_helpers
|
from controllers.common import helpers as controller_helpers
|
||||||
from controllers.web import api
|
from controllers.web import api
|
||||||
from controllers.web.error import AppUnavailableError
|
from controllers.web.error import AppUnavailableError
|
||||||
from controllers.web.wraps import WebApiResource
|
from controllers.web.wraps import WebApiResource
|
||||||
|
from libs.passport import PassportService
|
||||||
from models.model import App, AppMode
|
from models.model import App, AppMode
|
||||||
from services.app_service import AppService
|
from services.app_service import AppService
|
||||||
|
from services.enterprise.enterprise_service import EnterpriseService
|
||||||
|
|
||||||
|
|
||||||
class AppParameterApi(WebApiResource):
|
class AppParameterApi(WebApiResource):
|
||||||
@ -42,5 +46,51 @@ class AppMeta(WebApiResource):
|
|||||||
return AppService().get_app_meta(app_model)
|
return AppService().get_app_meta(app_model)
|
||||||
|
|
||||||
|
|
||||||
|
class AppAccessMode(Resource):
|
||||||
|
def get(self):
|
||||||
|
parser = reqparse.RequestParser()
|
||||||
|
parser.add_argument("appId", type=str, required=True, location="args")
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
app_id = args["appId"]
|
||||||
|
res = EnterpriseService.WebAppAuth.get_app_access_mode_by_id(app_id)
|
||||||
|
|
||||||
|
return {"accessMode": res.access_mode}
|
||||||
|
|
||||||
|
|
||||||
|
class AppWebAuthPermission(Resource):
|
||||||
|
def get(self):
|
||||||
|
user_id = "visitor"
|
||||||
|
try:
|
||||||
|
auth_header = request.headers.get("Authorization")
|
||||||
|
if auth_header is None:
|
||||||
|
raise
|
||||||
|
if " " not in auth_header:
|
||||||
|
raise
|
||||||
|
|
||||||
|
auth_scheme, tk = auth_header.split(None, 1)
|
||||||
|
auth_scheme = auth_scheme.lower()
|
||||||
|
if auth_scheme != "bearer":
|
||||||
|
raise
|
||||||
|
|
||||||
|
decoded = PassportService().verify(tk)
|
||||||
|
user_id = decoded.get("user_id", "visitor")
|
||||||
|
except Exception as e:
|
||||||
|
pass
|
||||||
|
|
||||||
|
parser = reqparse.RequestParser()
|
||||||
|
parser.add_argument("appId", type=str, required=True, location="args")
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
app_id = args["appId"]
|
||||||
|
app_code = AppService.get_app_code_by_id(app_id)
|
||||||
|
|
||||||
|
res = EnterpriseService.WebAppAuth.is_user_allowed_to_access_webapp(str(user_id), app_code)
|
||||||
|
return {"result": res}
|
||||||
|
|
||||||
|
|
||||||
api.add_resource(AppParameterApi, "/parameters")
|
api.add_resource(AppParameterApi, "/parameters")
|
||||||
api.add_resource(AppMeta, "/meta")
|
api.add_resource(AppMeta, "/meta")
|
||||||
|
# webapp auth apis
|
||||||
|
api.add_resource(AppAccessMode, "/webapp/access-mode")
|
||||||
|
api.add_resource(AppWebAuthPermission, "/webapp/permission")
|
||||||
|
|||||||
@ -121,9 +121,15 @@ class UnsupportedFileTypeError(BaseHTTPException):
|
|||||||
code = 415
|
code = 415
|
||||||
|
|
||||||
|
|
||||||
class WebSSOAuthRequiredError(BaseHTTPException):
|
class WebAppAuthRequiredError(BaseHTTPException):
|
||||||
error_code = "web_sso_auth_required"
|
error_code = "web_sso_auth_required"
|
||||||
description = "Web SSO authentication required."
|
description = "Web app authentication required."
|
||||||
|
code = 401
|
||||||
|
|
||||||
|
|
||||||
|
class WebAppAuthAccessDeniedError(BaseHTTPException):
|
||||||
|
error_code = "web_app_access_denied"
|
||||||
|
description = "You do not have permission to access this web app."
|
||||||
code = 401
|
code = 401
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
121
api/controllers/web/login.py
Normal file
121
api/controllers/web/login.py
Normal file
@ -0,0 +1,121 @@
|
|||||||
|
from flask import request
|
||||||
|
from flask_restful import Resource, reqparse
|
||||||
|
from jwt import InvalidTokenError # type: ignore
|
||||||
|
from web import api
|
||||||
|
from werkzeug.exceptions import BadRequest
|
||||||
|
|
||||||
|
import services
|
||||||
|
from controllers.console.auth.error import EmailCodeError, EmailOrPasswordMismatchError, InvalidEmailError
|
||||||
|
from controllers.console.error import AccountBannedError, AccountNotFound
|
||||||
|
from controllers.console.wraps import setup_required
|
||||||
|
from libs.helper import email
|
||||||
|
from libs.password import valid_password
|
||||||
|
from services.account_service import AccountService
|
||||||
|
from services.webapp_auth_service import WebAppAuthService
|
||||||
|
|
||||||
|
|
||||||
|
class LoginApi(Resource):
|
||||||
|
"""Resource for web app email/password login."""
|
||||||
|
|
||||||
|
def post(self):
|
||||||
|
"""Authenticate user and login."""
|
||||||
|
parser = reqparse.RequestParser()
|
||||||
|
parser.add_argument("email", type=email, required=True, location="json")
|
||||||
|
parser.add_argument("password", type=valid_password, required=True, location="json")
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
app_code = request.headers.get("X-App-Code")
|
||||||
|
if app_code is None:
|
||||||
|
raise BadRequest("X-App-Code header is missing.")
|
||||||
|
|
||||||
|
try:
|
||||||
|
account = WebAppAuthService.authenticate(args["email"], args["password"])
|
||||||
|
except services.errors.account.AccountLoginError:
|
||||||
|
raise AccountBannedError()
|
||||||
|
except services.errors.account.AccountPasswordError:
|
||||||
|
raise EmailOrPasswordMismatchError()
|
||||||
|
except services.errors.account.AccountNotFoundError:
|
||||||
|
raise AccountNotFound()
|
||||||
|
|
||||||
|
WebAppAuthService._validate_user_accessibility(account=account, app_code=app_code)
|
||||||
|
|
||||||
|
end_user = WebAppAuthService.create_end_user(email=args["email"], app_code=app_code)
|
||||||
|
|
||||||
|
token = WebAppAuthService.login(account=account, app_code=app_code, end_user_id=end_user.id)
|
||||||
|
return {"result": "success", "token": token}
|
||||||
|
|
||||||
|
|
||||||
|
# class LogoutApi(Resource):
|
||||||
|
# @setup_required
|
||||||
|
# def get(self):
|
||||||
|
# account = cast(Account, flask_login.current_user)
|
||||||
|
# if isinstance(account, flask_login.AnonymousUserMixin):
|
||||||
|
# return {"result": "success"}
|
||||||
|
# flask_login.logout_user()
|
||||||
|
# return {"result": "success"}
|
||||||
|
|
||||||
|
|
||||||
|
class EmailCodeLoginSendEmailApi(Resource):
|
||||||
|
@setup_required
|
||||||
|
def post(self):
|
||||||
|
parser = reqparse.RequestParser()
|
||||||
|
parser.add_argument("email", type=email, required=True, location="json")
|
||||||
|
parser.add_argument("language", type=str, required=False, location="json")
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
if args["language"] is not None and args["language"] == "zh-Hans":
|
||||||
|
language = "zh-Hans"
|
||||||
|
else:
|
||||||
|
language = "en-US"
|
||||||
|
|
||||||
|
account = WebAppAuthService.get_user_through_email(args["email"])
|
||||||
|
if account is None:
|
||||||
|
raise AccountNotFound()
|
||||||
|
else:
|
||||||
|
token = WebAppAuthService.send_email_code_login_email(account=account, language=language)
|
||||||
|
|
||||||
|
return {"result": "success", "data": token}
|
||||||
|
|
||||||
|
|
||||||
|
class EmailCodeLoginApi(Resource):
|
||||||
|
@setup_required
|
||||||
|
def post(self):
|
||||||
|
parser = reqparse.RequestParser()
|
||||||
|
parser.add_argument("email", type=str, required=True, location="json")
|
||||||
|
parser.add_argument("code", type=str, required=True, location="json")
|
||||||
|
parser.add_argument("token", type=str, required=True, location="json")
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
user_email = args["email"]
|
||||||
|
app_code = request.headers.get("X-App-Code")
|
||||||
|
if app_code is None:
|
||||||
|
raise BadRequest("X-App-Code header is missing.")
|
||||||
|
|
||||||
|
token_data = WebAppAuthService.get_email_code_login_data(args["token"])
|
||||||
|
if token_data is None:
|
||||||
|
raise InvalidTokenError()
|
||||||
|
|
||||||
|
if token_data["email"] != args["email"]:
|
||||||
|
raise InvalidEmailError()
|
||||||
|
|
||||||
|
if token_data["code"] != args["code"]:
|
||||||
|
raise EmailCodeError()
|
||||||
|
|
||||||
|
WebAppAuthService.revoke_email_code_login_token(args["token"])
|
||||||
|
account = WebAppAuthService.get_user_through_email(user_email)
|
||||||
|
if not account:
|
||||||
|
raise AccountNotFound()
|
||||||
|
|
||||||
|
WebAppAuthService._validate_user_accessibility(account=account, app_code=app_code)
|
||||||
|
|
||||||
|
end_user = WebAppAuthService.create_end_user(email=user_email, app_code=app_code)
|
||||||
|
|
||||||
|
token = WebAppAuthService.login(account=account, app_code=app_code, end_user_id=end_user.id)
|
||||||
|
AccountService.reset_login_error_rate_limit(args["email"])
|
||||||
|
return {"result": "success", "token": token}
|
||||||
|
|
||||||
|
|
||||||
|
api.add_resource(LoginApi, "/login")
|
||||||
|
# api.add_resource(LogoutApi, "/logout")
|
||||||
|
api.add_resource(EmailCodeLoginSendEmailApi, "/email-code-login")
|
||||||
|
api.add_resource(EmailCodeLoginApi, "/email-code-login/validity")
|
||||||
@ -5,7 +5,7 @@ from flask_restful import Resource # type: ignore
|
|||||||
from werkzeug.exceptions import NotFound, Unauthorized
|
from werkzeug.exceptions import NotFound, Unauthorized
|
||||||
|
|
||||||
from controllers.web import api
|
from controllers.web import api
|
||||||
from controllers.web.error import WebSSOAuthRequiredError
|
from controllers.web.error import WebAppAuthRequiredError
|
||||||
from extensions.ext_database import db
|
from extensions.ext_database import db
|
||||||
from libs.passport import PassportService
|
from libs.passport import PassportService
|
||||||
from models.model import App, EndUser, Site
|
from models.model import App, EndUser, Site
|
||||||
@ -22,10 +22,10 @@ class PassportResource(Resource):
|
|||||||
if app_code is None:
|
if app_code is None:
|
||||||
raise Unauthorized("X-App-Code header is missing.")
|
raise Unauthorized("X-App-Code header is missing.")
|
||||||
|
|
||||||
if system_features.sso_enforced_for_web:
|
if system_features.webapp_auth.enabled:
|
||||||
app_web_sso_enabled = EnterpriseService.get_app_web_sso_enabled(app_code).get("enabled", False)
|
app_settings = EnterpriseService.WebAppAuth.get_app_access_mode_by_code(app_code=app_code)
|
||||||
if app_web_sso_enabled:
|
if not app_settings or not app_settings.access_mode == "public":
|
||||||
raise WebSSOAuthRequiredError()
|
raise WebAppAuthRequiredError()
|
||||||
|
|
||||||
# get site from db and check if it is normal
|
# get site from db and check if it is normal
|
||||||
site = db.session.query(Site).filter(Site.code == app_code, Site.status == "normal").first()
|
site = db.session.query(Site).filter(Site.code == app_code, Site.status == "normal").first()
|
||||||
|
|||||||
@ -4,7 +4,7 @@ from flask import request
|
|||||||
from flask_restful import Resource # type: ignore
|
from flask_restful import Resource # type: ignore
|
||||||
from werkzeug.exceptions import BadRequest, NotFound, Unauthorized
|
from werkzeug.exceptions import BadRequest, NotFound, Unauthorized
|
||||||
|
|
||||||
from controllers.web.error import WebSSOAuthRequiredError
|
from controllers.web.error import WebAppAuthAccessDeniedError, WebAppAuthRequiredError
|
||||||
from extensions.ext_database import db
|
from extensions.ext_database import db
|
||||||
from libs.passport import PassportService
|
from libs.passport import PassportService
|
||||||
from models.model import App, EndUser, Site
|
from models.model import App, EndUser, Site
|
||||||
@ -57,35 +57,53 @@ def decode_jwt_token():
|
|||||||
if not end_user:
|
if not end_user:
|
||||||
raise NotFound()
|
raise NotFound()
|
||||||
|
|
||||||
_validate_web_sso_token(decoded, system_features, app_code)
|
# for enterprise webapp auth
|
||||||
|
app_web_auth_enabled = False
|
||||||
|
if system_features.webapp_auth.enabled:
|
||||||
|
app_web_auth_enabled = (
|
||||||
|
EnterpriseService.WebAppAuth.get_app_access_mode_by_code(app_code=app_code).access_mode != "public"
|
||||||
|
)
|
||||||
|
|
||||||
|
_validate_webapp_token(decoded, app_web_auth_enabled, system_features.webapp_auth.enabled)
|
||||||
|
_validate_user_accessibility(decoded, app_code, app_web_auth_enabled, system_features.webapp_auth.enabled)
|
||||||
|
|
||||||
return app_model, end_user
|
return app_model, end_user
|
||||||
except Unauthorized as e:
|
except Unauthorized as e:
|
||||||
if system_features.sso_enforced_for_web:
|
if system_features.webapp_auth.enabled:
|
||||||
app_web_sso_enabled = EnterpriseService.get_app_web_sso_enabled(app_code).get("enabled", False)
|
app_web_auth_enabled = (
|
||||||
if app_web_sso_enabled:
|
EnterpriseService.WebAppAuth.get_app_access_mode_by_code(app_code=app_code).access_mode != "public"
|
||||||
raise WebSSOAuthRequiredError()
|
)
|
||||||
|
if app_web_auth_enabled:
|
||||||
|
raise WebAppAuthRequiredError()
|
||||||
|
|
||||||
raise Unauthorized(e.description)
|
raise Unauthorized(e.description)
|
||||||
|
|
||||||
|
|
||||||
def _validate_web_sso_token(decoded, system_features, app_code):
|
def _validate_webapp_token(decoded, app_web_auth_enabled: bool, system_webapp_auth_enabled: bool):
|
||||||
app_web_sso_enabled = False
|
# Check if authentication is enforced for web app, and if the token source is not webapp,
|
||||||
|
# raise an error and redirect to login
|
||||||
# Check if SSO is enforced for web, and if the token source is not SSO, raise an error and redirect to SSO login
|
if system_webapp_auth_enabled and app_web_auth_enabled:
|
||||||
if system_features.sso_enforced_for_web:
|
|
||||||
app_web_sso_enabled = EnterpriseService.get_app_web_sso_enabled(app_code).get("enabled", False)
|
|
||||||
if app_web_sso_enabled:
|
|
||||||
source = decoded.get("token_source")
|
|
||||||
if not source or source != "sso":
|
|
||||||
raise WebSSOAuthRequiredError()
|
|
||||||
|
|
||||||
# Check if SSO is not enforced for web, and if the token source is SSO,
|
|
||||||
# raise an error and redirect to normal passport login
|
|
||||||
if not system_features.sso_enforced_for_web or not app_web_sso_enabled:
|
|
||||||
source = decoded.get("token_source")
|
source = decoded.get("token_source")
|
||||||
if source and source == "sso":
|
if not source or source != "webapp":
|
||||||
raise Unauthorized("sso token expired.")
|
raise WebAppAuthRequiredError()
|
||||||
|
|
||||||
|
# Check if authentication is not enforced for web, and if the token source is webapp,
|
||||||
|
# raise an error and redirect to normal passport login
|
||||||
|
if not system_webapp_auth_enabled or not app_web_auth_enabled:
|
||||||
|
source = decoded.get("token_source")
|
||||||
|
if source and source == "webapp":
|
||||||
|
raise Unauthorized("webapp token expired.")
|
||||||
|
|
||||||
|
|
||||||
|
def _validate_user_accessibility(decoded, app_code, app_web_auth_enabled: bool, system_webapp_auth_enabled: bool):
|
||||||
|
if system_webapp_auth_enabled and app_web_auth_enabled:
|
||||||
|
# Check if the user is allowed to access the web app
|
||||||
|
user_id = decoded.get("user_id")
|
||||||
|
if not user_id:
|
||||||
|
raise WebAppAuthRequiredError()
|
||||||
|
|
||||||
|
if not EnterpriseService.WebAppAuth.is_user_allowed_to_access_webapp(user_id, app_code=app_code):
|
||||||
|
raise WebAppAuthAccessDeniedError()
|
||||||
|
|
||||||
|
|
||||||
class WebApiResource(Resource):
|
class WebApiResource(Resource):
|
||||||
|
|||||||
@ -17,13 +17,6 @@
|
|||||||
- deepseek-ai/DeepSeek-V2.5
|
- deepseek-ai/DeepSeek-V2.5
|
||||||
- deepseek-ai/DeepSeek-V3
|
- deepseek-ai/DeepSeek-V3
|
||||||
- deepseek-ai/DeepSeek-Coder-V2-Instruct
|
- deepseek-ai/DeepSeek-Coder-V2-Instruct
|
||||||
- deepseek-ai/DeepSeek-R1-Distill-Llama-8B
|
|
||||||
- deepseek-ai/DeepSeek-R1-Distill-Llama-70B
|
|
||||||
- deepseek-ai/DeepSeek-R1-Distill-Qwen-1.5B
|
|
||||||
- deepseek-ai/DeepSeek-R1-Distill-Qwen-7B
|
|
||||||
- deepseek-ai/DeepSeek-R1-Distill-Qwen-14B
|
|
||||||
- deepseek-ai/DeepSeek-R1-Distill-Qwen-32B
|
|
||||||
- deepseek-ai/Janus-Pro-7B
|
|
||||||
- THUDM/glm-4-9b-chat
|
- THUDM/glm-4-9b-chat
|
||||||
- 01-ai/Yi-1.5-34B-Chat-16K
|
- 01-ai/Yi-1.5-34B-Chat-16K
|
||||||
- 01-ai/Yi-1.5-9B-Chat-16K
|
- 01-ai/Yi-1.5-9B-Chat-16K
|
||||||
|
|||||||
@ -1,21 +0,0 @@
|
|||||||
model: deepseek-ai/DeepSeek-R1-Distill-Llama-70B
|
|
||||||
label:
|
|
||||||
zh_Hans: deepseek-ai/DeepSeek-R1-Distill-Llama-70B
|
|
||||||
en_US: deepseek-ai/DeepSeek-R1-Distill-Llama-70B
|
|
||||||
model_type: llm
|
|
||||||
features:
|
|
||||||
- agent-thought
|
|
||||||
model_properties:
|
|
||||||
mode: chat
|
|
||||||
context_size: 32000
|
|
||||||
parameter_rules:
|
|
||||||
- name: max_tokens
|
|
||||||
use_template: max_tokens
|
|
||||||
min: 1
|
|
||||||
max: 8192
|
|
||||||
default: 4096
|
|
||||||
pricing:
|
|
||||||
input: "0.00"
|
|
||||||
output: "4.3"
|
|
||||||
unit: "0.000001"
|
|
||||||
currency: RMB
|
|
||||||
@ -1,21 +0,0 @@
|
|||||||
model: deepseek-ai/DeepSeek-R1-Distill-Llama-8B
|
|
||||||
label:
|
|
||||||
zh_Hans: deepseek-ai/DeepSeek-R1-Distill-Llama-8B
|
|
||||||
en_US: deepseek-ai/DeepSeek-R1-Distill-Llama-8B
|
|
||||||
model_type: llm
|
|
||||||
features:
|
|
||||||
- agent-thought
|
|
||||||
model_properties:
|
|
||||||
mode: chat
|
|
||||||
context_size: 32000
|
|
||||||
parameter_rules:
|
|
||||||
- name: max_tokens
|
|
||||||
use_template: max_tokens
|
|
||||||
min: 1
|
|
||||||
max: 8192
|
|
||||||
default: 4096
|
|
||||||
pricing:
|
|
||||||
input: "0.00"
|
|
||||||
output: "0.00"
|
|
||||||
unit: "0.000001"
|
|
||||||
currency: RMB
|
|
||||||
@ -1,21 +0,0 @@
|
|||||||
model: deepseek-ai/DeepSeek-R1-Distill-Qwen-1.5B
|
|
||||||
label:
|
|
||||||
zh_Hans: deepseek-ai/DeepSeek-R1-Distill-Qwen-1.5B
|
|
||||||
en_US: deepseek-ai/DeepSeek-R1-Distill-Qwen-1.5B
|
|
||||||
model_type: llm
|
|
||||||
features:
|
|
||||||
- agent-thought
|
|
||||||
model_properties:
|
|
||||||
mode: chat
|
|
||||||
context_size: 32000
|
|
||||||
parameter_rules:
|
|
||||||
- name: max_tokens
|
|
||||||
use_template: max_tokens
|
|
||||||
min: 1
|
|
||||||
max: 8192
|
|
||||||
default: 4096
|
|
||||||
pricing:
|
|
||||||
input: "0.00"
|
|
||||||
output: "1.26"
|
|
||||||
unit: "0.000001"
|
|
||||||
currency: RMB
|
|
||||||
@ -1,21 +0,0 @@
|
|||||||
model: deepseek-ai/DeepSeek-R1-Distill-Qwen-14B
|
|
||||||
label:
|
|
||||||
zh_Hans: deepseek-ai/DeepSeek-R1-Distill-Qwen-14B
|
|
||||||
en_US: deepseek-ai/DeepSeek-R1-Distill-Qwen-14B
|
|
||||||
model_type: llm
|
|
||||||
features:
|
|
||||||
- agent-thought
|
|
||||||
model_properties:
|
|
||||||
mode: chat
|
|
||||||
context_size: 32000
|
|
||||||
parameter_rules:
|
|
||||||
- name: max_tokens
|
|
||||||
use_template: max_tokens
|
|
||||||
min: 1
|
|
||||||
max: 8192
|
|
||||||
default: 4096
|
|
||||||
pricing:
|
|
||||||
input: "0.00"
|
|
||||||
output: "0.70"
|
|
||||||
unit: "0.000001"
|
|
||||||
currency: RMB
|
|
||||||
@ -1,21 +0,0 @@
|
|||||||
model: deepseek-ai/DeepSeek-R1-Distill-Qwen-32B
|
|
||||||
label:
|
|
||||||
zh_Hans: deepseek-ai/DeepSeek-R1-Distill-Qwen-32B
|
|
||||||
en_US: deepseek-ai/DeepSeek-R1-Distill-Qwen-32B
|
|
||||||
model_type: llm
|
|
||||||
features:
|
|
||||||
- agent-thought
|
|
||||||
model_properties:
|
|
||||||
mode: chat
|
|
||||||
context_size: 32000
|
|
||||||
parameter_rules:
|
|
||||||
- name: max_tokens
|
|
||||||
use_template: max_tokens
|
|
||||||
min: 1
|
|
||||||
max: 8192
|
|
||||||
default: 4096
|
|
||||||
pricing:
|
|
||||||
input: "0.00"
|
|
||||||
output: "1.26"
|
|
||||||
unit: "0.000001"
|
|
||||||
currency: RMB
|
|
||||||
@ -1,21 +0,0 @@
|
|||||||
model: deepseek-ai/DeepSeek-R1-Distill-Qwen-7B
|
|
||||||
label:
|
|
||||||
zh_Hans: deepseek-ai/DeepSeek-R1-Distill-Qwen-7B
|
|
||||||
en_US: deepseek-ai/DeepSeek-R1-Distill-Qwen-7B
|
|
||||||
model_type: llm
|
|
||||||
features:
|
|
||||||
- agent-thought
|
|
||||||
model_properties:
|
|
||||||
mode: chat
|
|
||||||
context_size: 32000
|
|
||||||
parameter_rules:
|
|
||||||
- name: max_tokens
|
|
||||||
use_template: max_tokens
|
|
||||||
min: 1
|
|
||||||
max: 8192
|
|
||||||
default: 4096
|
|
||||||
pricing:
|
|
||||||
input: "0.00"
|
|
||||||
output: "0.00"
|
|
||||||
unit: "0.000001"
|
|
||||||
currency: RMB
|
|
||||||
@ -1,22 +0,0 @@
|
|||||||
model: deepseek-ai/Janus-Pro-7B
|
|
||||||
label:
|
|
||||||
zh_Hans: deepseek-ai/Janus-Pro-7B
|
|
||||||
en_US: deepseek-ai/Janus-Pro-7B
|
|
||||||
model_type: llm
|
|
||||||
features:
|
|
||||||
- agent-thought
|
|
||||||
- vision
|
|
||||||
model_properties:
|
|
||||||
mode: chat
|
|
||||||
context_size: 32000
|
|
||||||
parameter_rules:
|
|
||||||
- name: max_tokens
|
|
||||||
use_template: max_tokens
|
|
||||||
min: 1
|
|
||||||
max: 8192
|
|
||||||
default: 4096
|
|
||||||
pricing:
|
|
||||||
input: "0.00"
|
|
||||||
output: "0.00"
|
|
||||||
unit: "0.000001"
|
|
||||||
currency: RMB
|
|
||||||
@ -69,15 +69,6 @@ parameter_rules:
|
|||||||
help:
|
help:
|
||||||
zh_Hans: 用于控制模型生成时的重复度。提高repetition_penalty时可以降低模型生成的重复度。1.0表示不做惩罚。
|
zh_Hans: 用于控制模型生成时的重复度。提高repetition_penalty时可以降低模型生成的重复度。1.0表示不做惩罚。
|
||||||
en_US: Used to control the repeatability when generating models. Increasing repetition_penalty can reduce the duplication of model generation. 1.0 means no punishment.
|
en_US: Used to control the repeatability when generating models. Increasing repetition_penalty can reduce the duplication of model generation. 1.0 means no punishment.
|
||||||
- name: enable_search
|
|
||||||
type: boolean
|
|
||||||
default: false
|
|
||||||
label:
|
|
||||||
zh_Hans: 联网搜索
|
|
||||||
en_US: Web Search
|
|
||||||
help:
|
|
||||||
zh_Hans: 模型内置了互联网搜索服务,该参数控制模型在生成文本时是否参考使用互联网搜索结果。启用互联网搜索,模型会将搜索结果作为文本生成过程中的参考信息,但模型会基于其内部逻辑“自行判断”是否使用互联网搜索结果。
|
|
||||||
en_US: The model has a built-in Internet search service. This parameter controls whether the model refers to Internet search results when generating text. When Internet search is enabled, the model will use the search results as reference information in the text generation process, but the model will "judge" whether to use Internet search results based on its internal logic.
|
|
||||||
- name: response_format
|
- name: response_format
|
||||||
use_template: response_format
|
use_template: response_format
|
||||||
pricing:
|
pricing:
|
||||||
|
|||||||
@ -69,15 +69,6 @@ parameter_rules:
|
|||||||
help:
|
help:
|
||||||
zh_Hans: 用于控制模型生成时的重复度。提高repetition_penalty时可以降低模型生成的重复度。1.0表示不做惩罚。
|
zh_Hans: 用于控制模型生成时的重复度。提高repetition_penalty时可以降低模型生成的重复度。1.0表示不做惩罚。
|
||||||
en_US: Used to control the repeatability when generating models. Increasing repetition_penalty can reduce the duplication of model generation. 1.0 means no punishment.
|
en_US: Used to control the repeatability when generating models. Increasing repetition_penalty can reduce the duplication of model generation. 1.0 means no punishment.
|
||||||
- name: enable_search
|
|
||||||
type: boolean
|
|
||||||
default: false
|
|
||||||
label:
|
|
||||||
zh_Hans: 联网搜索
|
|
||||||
en_US: Web Search
|
|
||||||
help:
|
|
||||||
zh_Hans: 模型内置了互联网搜索服务,该参数控制模型在生成文本时是否参考使用互联网搜索结果。启用互联网搜索,模型会将搜索结果作为文本生成过程中的参考信息,但模型会基于其内部逻辑“自行判断”是否使用互联网搜索结果。
|
|
||||||
en_US: The model has a built-in Internet search service. This parameter controls whether the model refers to Internet search results when generating text. When Internet search is enabled, the model will use the search results as reference information in the text generation process, but the model will "judge" whether to use Internet search results based on its internal logic.
|
|
||||||
- name: response_format
|
- name: response_format
|
||||||
use_template: response_format
|
use_template: response_format
|
||||||
pricing:
|
pricing:
|
||||||
|
|||||||
@ -69,15 +69,6 @@ parameter_rules:
|
|||||||
help:
|
help:
|
||||||
zh_Hans: 用于控制模型生成时的重复度。提高repetition_penalty时可以降低模型生成的重复度。1.0表示不做惩罚。
|
zh_Hans: 用于控制模型生成时的重复度。提高repetition_penalty时可以降低模型生成的重复度。1.0表示不做惩罚。
|
||||||
en_US: Used to control the repeatability when generating models. Increasing repetition_penalty can reduce the duplication of model generation. 1.0 means no punishment.
|
en_US: Used to control the repeatability when generating models. Increasing repetition_penalty can reduce the duplication of model generation. 1.0 means no punishment.
|
||||||
- name: enable_search
|
|
||||||
type: boolean
|
|
||||||
default: false
|
|
||||||
label:
|
|
||||||
zh_Hans: 联网搜索
|
|
||||||
en_US: Web Search
|
|
||||||
help:
|
|
||||||
zh_Hans: 模型内置了互联网搜索服务,该参数控制模型在生成文本时是否参考使用互联网搜索结果。启用互联网搜索,模型会将搜索结果作为文本生成过程中的参考信息,但模型会基于其内部逻辑“自行判断”是否使用互联网搜索结果。
|
|
||||||
en_US: The model has a built-in Internet search service. This parameter controls whether the model refers to Internet search results when generating text. When Internet search is enabled, the model will use the search results as reference information in the text generation process, but the model will "judge" whether to use Internet search results based on its internal logic.
|
|
||||||
- name: response_format
|
- name: response_format
|
||||||
use_template: response_format
|
use_template: response_format
|
||||||
pricing:
|
pricing:
|
||||||
|
|||||||
@ -69,15 +69,6 @@ parameter_rules:
|
|||||||
help:
|
help:
|
||||||
zh_Hans: 用于控制模型生成时的重复度。提高repetition_penalty时可以降低模型生成的重复度。1.0表示不做惩罚。
|
zh_Hans: 用于控制模型生成时的重复度。提高repetition_penalty时可以降低模型生成的重复度。1.0表示不做惩罚。
|
||||||
en_US: Used to control the repeatability when generating models. Increasing repetition_penalty can reduce the duplication of model generation. 1.0 means no punishment.
|
en_US: Used to control the repeatability when generating models. Increasing repetition_penalty can reduce the duplication of model generation. 1.0 means no punishment.
|
||||||
- name: enable_search
|
|
||||||
type: boolean
|
|
||||||
default: false
|
|
||||||
label:
|
|
||||||
zh_Hans: 联网搜索
|
|
||||||
en_US: Web Search
|
|
||||||
help:
|
|
||||||
zh_Hans: 模型内置了互联网搜索服务,该参数控制模型在生成文本时是否参考使用互联网搜索结果。启用互联网搜索,模型会将搜索结果作为文本生成过程中的参考信息,但模型会基于其内部逻辑“自行判断”是否使用互联网搜索结果。
|
|
||||||
en_US: The model has a built-in Internet search service. This parameter controls whether the model refers to Internet search results when generating text. When Internet search is enabled, the model will use the search results as reference information in the text generation process, but the model will "judge" whether to use Internet search results based on its internal logic.
|
|
||||||
- name: response_format
|
- name: response_format
|
||||||
use_template: response_format
|
use_template: response_format
|
||||||
pricing:
|
pricing:
|
||||||
|
|||||||
@ -68,15 +68,6 @@ parameter_rules:
|
|||||||
help:
|
help:
|
||||||
zh_Hans: 用于控制模型生成时的重复度。提高repetition_penalty时可以降低模型生成的重复度。1.0表示不做惩罚。
|
zh_Hans: 用于控制模型生成时的重复度。提高repetition_penalty时可以降低模型生成的重复度。1.0表示不做惩罚。
|
||||||
en_US: Used to control the repeatability when generating models. Increasing repetition_penalty can reduce the duplication of model generation. 1.0 means no punishment.
|
en_US: Used to control the repeatability when generating models. Increasing repetition_penalty can reduce the duplication of model generation. 1.0 means no punishment.
|
||||||
- name: enable_search
|
|
||||||
type: boolean
|
|
||||||
default: false
|
|
||||||
label:
|
|
||||||
zh_Hans: 联网搜索
|
|
||||||
en_US: Web Search
|
|
||||||
help:
|
|
||||||
zh_Hans: 模型内置了互联网搜索服务,该参数控制模型在生成文本时是否参考使用互联网搜索结果。启用互联网搜索,模型会将搜索结果作为文本生成过程中的参考信息,但模型会基于其内部逻辑“自行判断”是否使用互联网搜索结果。
|
|
||||||
en_US: The model has a built-in Internet search service. This parameter controls whether the model refers to Internet search results when generating text. When Internet search is enabled, the model will use the search results as reference information in the text generation process, but the model will "judge" whether to use Internet search results based on its internal logic.
|
|
||||||
- name: response_format
|
- name: response_format
|
||||||
use_template: response_format
|
use_template: response_format
|
||||||
pricing:
|
pricing:
|
||||||
|
|||||||
@ -69,15 +69,6 @@ parameter_rules:
|
|||||||
help:
|
help:
|
||||||
zh_Hans: 用于控制模型生成时的重复度。提高repetition_penalty时可以降低模型生成的重复度。1.0表示不做惩罚。
|
zh_Hans: 用于控制模型生成时的重复度。提高repetition_penalty时可以降低模型生成的重复度。1.0表示不做惩罚。
|
||||||
en_US: Used to control the repeatability when generating models. Increasing repetition_penalty can reduce the duplication of model generation. 1.0 means no punishment.
|
en_US: Used to control the repeatability when generating models. Increasing repetition_penalty can reduce the duplication of model generation. 1.0 means no punishment.
|
||||||
- name: enable_search
|
|
||||||
type: boolean
|
|
||||||
default: false
|
|
||||||
label:
|
|
||||||
zh_Hans: 联网搜索
|
|
||||||
en_US: Web Search
|
|
||||||
help:
|
|
||||||
zh_Hans: 模型内置了互联网搜索服务,该参数控制模型在生成文本时是否参考使用互联网搜索结果。启用互联网搜索,模型会将搜索结果作为文本生成过程中的参考信息,但模型会基于其内部逻辑“自行判断”是否使用互联网搜索结果。
|
|
||||||
en_US: The model has a built-in Internet search service. This parameter controls whether the model refers to Internet search results when generating text. When Internet search is enabled, the model will use the search results as reference information in the text generation process, but the model will "judge" whether to use Internet search results based on its internal logic.
|
|
||||||
- name: response_format
|
- name: response_format
|
||||||
use_template: response_format
|
use_template: response_format
|
||||||
pricing:
|
pricing:
|
||||||
|
|||||||
@ -69,15 +69,6 @@ parameter_rules:
|
|||||||
help:
|
help:
|
||||||
zh_Hans: 用于控制模型生成时的重复度。提高repetition_penalty时可以降低模型生成的重复度。1.0表示不做惩罚。
|
zh_Hans: 用于控制模型生成时的重复度。提高repetition_penalty时可以降低模型生成的重复度。1.0表示不做惩罚。
|
||||||
en_US: Used to control the repeatability when generating models. Increasing repetition_penalty can reduce the duplication of model generation. 1.0 means no punishment.
|
en_US: Used to control the repeatability when generating models. Increasing repetition_penalty can reduce the duplication of model generation. 1.0 means no punishment.
|
||||||
- name: enable_search
|
|
||||||
type: boolean
|
|
||||||
default: false
|
|
||||||
label:
|
|
||||||
zh_Hans: 联网搜索
|
|
||||||
en_US: Web Search
|
|
||||||
help:
|
|
||||||
zh_Hans: 模型内置了互联网搜索服务,该参数控制模型在生成文本时是否参考使用互联网搜索结果。启用互联网搜索,模型会将搜索结果作为文本生成过程中的参考信息,但模型会基于其内部逻辑“自行判断”是否使用互联网搜索结果。
|
|
||||||
en_US: The model has a built-in Internet search service. This parameter controls whether the model refers to Internet search results when generating text. When Internet search is enabled, the model will use the search results as reference information in the text generation process, but the model will "judge" whether to use Internet search results based on its internal logic.
|
|
||||||
- name: response_format
|
- name: response_format
|
||||||
use_template: response_format
|
use_template: response_format
|
||||||
pricing:
|
pricing:
|
||||||
|
|||||||
@ -67,15 +67,6 @@ parameter_rules:
|
|||||||
help:
|
help:
|
||||||
zh_Hans: 用于控制模型生成时的重复度。提高repetition_penalty时可以降低模型生成的重复度。1.0表示不做惩罚。
|
zh_Hans: 用于控制模型生成时的重复度。提高repetition_penalty时可以降低模型生成的重复度。1.0表示不做惩罚。
|
||||||
en_US: Used to control the repeatability when generating models. Increasing repetition_penalty can reduce the duplication of model generation. 1.0 means no punishment.
|
en_US: Used to control the repeatability when generating models. Increasing repetition_penalty can reduce the duplication of model generation. 1.0 means no punishment.
|
||||||
- name: enable_search
|
|
||||||
type: boolean
|
|
||||||
default: false
|
|
||||||
label:
|
|
||||||
zh_Hans: 联网搜索
|
|
||||||
en_US: Web Search
|
|
||||||
help:
|
|
||||||
zh_Hans: 模型内置了互联网搜索服务,该参数控制模型在生成文本时是否参考使用互联网搜索结果。启用互联网搜索,模型会将搜索结果作为文本生成过程中的参考信息,但模型会基于其内部逻辑“自行判断”是否使用互联网搜索结果。
|
|
||||||
en_US: The model has a built-in Internet search service. This parameter controls whether the model refers to Internet search results when generating text. When Internet search is enabled, the model will use the search results as reference information in the text generation process, but the model will "judge" whether to use Internet search results based on its internal logic.
|
|
||||||
- name: response_format
|
- name: response_format
|
||||||
use_template: response_format
|
use_template: response_format
|
||||||
pricing:
|
pricing:
|
||||||
|
|||||||
@ -67,15 +67,6 @@ parameter_rules:
|
|||||||
help:
|
help:
|
||||||
zh_Hans: 用于控制模型生成时的重复度。提高repetition_penalty时可以降低模型生成的重复度。1.0表示不做惩罚。
|
zh_Hans: 用于控制模型生成时的重复度。提高repetition_penalty时可以降低模型生成的重复度。1.0表示不做惩罚。
|
||||||
en_US: Used to control the repeatability when generating models. Increasing repetition_penalty can reduce the duplication of model generation. 1.0 means no punishment.
|
en_US: Used to control the repeatability when generating models. Increasing repetition_penalty can reduce the duplication of model generation. 1.0 means no punishment.
|
||||||
- name: enable_search
|
|
||||||
type: boolean
|
|
||||||
default: false
|
|
||||||
label:
|
|
||||||
zh_Hans: 联网搜索
|
|
||||||
en_US: Web Search
|
|
||||||
help:
|
|
||||||
zh_Hans: 模型内置了互联网搜索服务,该参数控制模型在生成文本时是否参考使用互联网搜索结果。启用互联网搜索,模型会将搜索结果作为文本生成过程中的参考信息,但模型会基于其内部逻辑“自行判断”是否使用互联网搜索结果。
|
|
||||||
en_US: The model has a built-in Internet search service. This parameter controls whether the model refers to Internet search results when generating text. When Internet search is enabled, the model will use the search results as reference information in the text generation process, but the model will "judge" whether to use Internet search results based on its internal logic.
|
|
||||||
- name: response_format
|
- name: response_format
|
||||||
use_template: response_format
|
use_template: response_format
|
||||||
pricing:
|
pricing:
|
||||||
|
|||||||
@ -67,15 +67,6 @@ parameter_rules:
|
|||||||
help:
|
help:
|
||||||
zh_Hans: 用于控制模型生成时的重复度。提高repetition_penalty时可以降低模型生成的重复度。1.0表示不做惩罚。
|
zh_Hans: 用于控制模型生成时的重复度。提高repetition_penalty时可以降低模型生成的重复度。1.0表示不做惩罚。
|
||||||
en_US: Used to control the repeatability when generating models. Increasing repetition_penalty can reduce the duplication of model generation. 1.0 means no punishment.
|
en_US: Used to control the repeatability when generating models. Increasing repetition_penalty can reduce the duplication of model generation. 1.0 means no punishment.
|
||||||
- name: enable_search
|
|
||||||
type: boolean
|
|
||||||
default: false
|
|
||||||
label:
|
|
||||||
zh_Hans: 联网搜索
|
|
||||||
en_US: Web Search
|
|
||||||
help:
|
|
||||||
zh_Hans: 模型内置了互联网搜索服务,该参数控制模型在生成文本时是否参考使用互联网搜索结果。启用互联网搜索,模型会将搜索结果作为文本生成过程中的参考信息,但模型会基于其内部逻辑“自行判断”是否使用互联网搜索结果。
|
|
||||||
en_US: The model has a built-in Internet search service. This parameter controls whether the model refers to Internet search results when generating text. When Internet search is enabled, the model will use the search results as reference information in the text generation process, but the model will "judge" whether to use Internet search results based on its internal logic.
|
|
||||||
- name: response_format
|
- name: response_format
|
||||||
use_template: response_format
|
use_template: response_format
|
||||||
pricing:
|
pricing:
|
||||||
|
|||||||
@ -67,15 +67,6 @@ parameter_rules:
|
|||||||
help:
|
help:
|
||||||
zh_Hans: 用于控制模型生成时的重复度。提高repetition_penalty时可以降低模型生成的重复度。1.0表示不做惩罚。
|
zh_Hans: 用于控制模型生成时的重复度。提高repetition_penalty时可以降低模型生成的重复度。1.0表示不做惩罚。
|
||||||
en_US: Used to control the repeatability when generating models. Increasing repetition_penalty can reduce the duplication of model generation. 1.0 means no punishment.
|
en_US: Used to control the repeatability when generating models. Increasing repetition_penalty can reduce the duplication of model generation. 1.0 means no punishment.
|
||||||
- name: enable_search
|
|
||||||
type: boolean
|
|
||||||
default: false
|
|
||||||
label:
|
|
||||||
zh_Hans: 联网搜索
|
|
||||||
en_US: Web Search
|
|
||||||
help:
|
|
||||||
zh_Hans: 模型内置了互联网搜索服务,该参数控制模型在生成文本时是否参考使用互联网搜索结果。启用互联网搜索,模型会将搜索结果作为文本生成过程中的参考信息,但模型会基于其内部逻辑“自行判断”是否使用互联网搜索结果。
|
|
||||||
en_US: The model has a built-in Internet search service. This parameter controls whether the model refers to Internet search results when generating text. When Internet search is enabled, the model will use the search results as reference information in the text generation process, but the model will "judge" whether to use Internet search results based on its internal logic.
|
|
||||||
- name: response_format
|
- name: response_format
|
||||||
use_template: response_format
|
use_template: response_format
|
||||||
pricing:
|
pricing:
|
||||||
|
|||||||
@ -67,15 +67,6 @@ parameter_rules:
|
|||||||
help:
|
help:
|
||||||
zh_Hans: 用于控制模型生成时的重复度。提高repetition_penalty时可以降低模型生成的重复度。1.0表示不做惩罚。
|
zh_Hans: 用于控制模型生成时的重复度。提高repetition_penalty时可以降低模型生成的重复度。1.0表示不做惩罚。
|
||||||
en_US: Used to control the repeatability when generating models. Increasing repetition_penalty can reduce the duplication of model generation. 1.0 means no punishment.
|
en_US: Used to control the repeatability when generating models. Increasing repetition_penalty can reduce the duplication of model generation. 1.0 means no punishment.
|
||||||
- name: enable_search
|
|
||||||
type: boolean
|
|
||||||
default: false
|
|
||||||
label:
|
|
||||||
zh_Hans: 联网搜索
|
|
||||||
en_US: Web Search
|
|
||||||
help:
|
|
||||||
zh_Hans: 模型内置了互联网搜索服务,该参数控制模型在生成文本时是否参考使用互联网搜索结果。启用互联网搜索,模型会将搜索结果作为文本生成过程中的参考信息,但模型会基于其内部逻辑“自行判断”是否使用互联网搜索结果。
|
|
||||||
en_US: The model has a built-in Internet search service. This parameter controls whether the model refers to Internet search results when generating text. When Internet search is enabled, the model will use the search results as reference information in the text generation process, but the model will "judge" whether to use Internet search results based on its internal logic.
|
|
||||||
- name: response_format
|
- name: response_format
|
||||||
use_template: response_format
|
use_template: response_format
|
||||||
pricing:
|
pricing:
|
||||||
|
|||||||
@ -69,15 +69,6 @@ parameter_rules:
|
|||||||
help:
|
help:
|
||||||
zh_Hans: 用于控制模型生成时的重复度。提高repetition_penalty时可以降低模型生成的重复度。1.0表示不做惩罚。
|
zh_Hans: 用于控制模型生成时的重复度。提高repetition_penalty时可以降低模型生成的重复度。1.0表示不做惩罚。
|
||||||
en_US: Used to control the repeatability when generating models. Increasing repetition_penalty can reduce the duplication of model generation. 1.0 means no punishment.
|
en_US: Used to control the repeatability when generating models. Increasing repetition_penalty can reduce the duplication of model generation. 1.0 means no punishment.
|
||||||
- name: enable_search
|
|
||||||
type: boolean
|
|
||||||
default: false
|
|
||||||
label:
|
|
||||||
zh_Hans: 联网搜索
|
|
||||||
en_US: Web Search
|
|
||||||
help:
|
|
||||||
zh_Hans: 模型内置了互联网搜索服务,该参数控制模型在生成文本时是否参考使用互联网搜索结果。启用互联网搜索,模型会将搜索结果作为文本生成过程中的参考信息,但模型会基于其内部逻辑“自行判断”是否使用互联网搜索结果。
|
|
||||||
en_US: The model has a built-in Internet search service. This parameter controls whether the model refers to Internet search results when generating text. When Internet search is enabled, the model will use the search results as reference information in the text generation process, but the model will "judge" whether to use Internet search results based on its internal logic.
|
|
||||||
- name: response_format
|
- name: response_format
|
||||||
use_template: response_format
|
use_template: response_format
|
||||||
pricing:
|
pricing:
|
||||||
|
|||||||
@ -67,15 +67,6 @@ parameter_rules:
|
|||||||
help:
|
help:
|
||||||
zh_Hans: 用于控制模型生成时的重复度。提高repetition_penalty时可以降低模型生成的重复度。1.0表示不做惩罚。
|
zh_Hans: 用于控制模型生成时的重复度。提高repetition_penalty时可以降低模型生成的重复度。1.0表示不做惩罚。
|
||||||
en_US: Used to control the repeatability when generating models. Increasing repetition_penalty can reduce the duplication of model generation. 1.0 means no punishment.
|
en_US: Used to control the repeatability when generating models. Increasing repetition_penalty can reduce the duplication of model generation. 1.0 means no punishment.
|
||||||
- name: enable_search
|
|
||||||
type: boolean
|
|
||||||
default: false
|
|
||||||
label:
|
|
||||||
zh_Hans: 联网搜索
|
|
||||||
en_US: Web Search
|
|
||||||
help:
|
|
||||||
zh_Hans: 模型内置了互联网搜索服务,该参数控制模型在生成文本时是否参考使用互联网搜索结果。启用互联网搜索,模型会将搜索结果作为文本生成过程中的参考信息,但模型会基于其内部逻辑“自行判断”是否使用互联网搜索结果。
|
|
||||||
en_US: The model has a built-in Internet search service. This parameter controls whether the model refers to Internet search results when generating text. When Internet search is enabled, the model will use the search results as reference information in the text generation process, but the model will "judge" whether to use Internet search results based on its internal logic.
|
|
||||||
- name: response_format
|
- name: response_format
|
||||||
use_template: response_format
|
use_template: response_format
|
||||||
pricing:
|
pricing:
|
||||||
|
|||||||
@ -68,15 +68,6 @@ parameter_rules:
|
|||||||
help:
|
help:
|
||||||
zh_Hans: 用于控制模型生成时的重复度。提高repetition_penalty时可以降低模型生成的重复度。1.0表示不做惩罚。
|
zh_Hans: 用于控制模型生成时的重复度。提高repetition_penalty时可以降低模型生成的重复度。1.0表示不做惩罚。
|
||||||
en_US: Used to control the repeatability when generating models. Increasing repetition_penalty can reduce the duplication of model generation. 1.0 means no punishment.
|
en_US: Used to control the repeatability when generating models. Increasing repetition_penalty can reduce the duplication of model generation. 1.0 means no punishment.
|
||||||
- name: enable_search
|
|
||||||
type: boolean
|
|
||||||
default: false
|
|
||||||
label:
|
|
||||||
zh_Hans: 联网搜索
|
|
||||||
en_US: Web Search
|
|
||||||
help:
|
|
||||||
zh_Hans: 模型内置了互联网搜索服务,该参数控制模型在生成文本时是否参考使用互联网搜索结果。启用互联网搜索,模型会将搜索结果作为文本生成过程中的参考信息,但模型会基于其内部逻辑“自行判断”是否使用互联网搜索结果。
|
|
||||||
en_US: The model has a built-in Internet search service. This parameter controls whether the model refers to Internet search results when generating text. When Internet search is enabled, the model will use the search results as reference information in the text generation process, but the model will "judge" whether to use Internet search results based on its internal logic.
|
|
||||||
- name: response_format
|
- name: response_format
|
||||||
use_template: response_format
|
use_template: response_format
|
||||||
pricing:
|
pricing:
|
||||||
|
|||||||
@ -67,15 +67,6 @@ parameter_rules:
|
|||||||
help:
|
help:
|
||||||
zh_Hans: 用于控制模型生成时的重复度。提高repetition_penalty时可以降低模型生成的重复度。1.0表示不做惩罚。
|
zh_Hans: 用于控制模型生成时的重复度。提高repetition_penalty时可以降低模型生成的重复度。1.0表示不做惩罚。
|
||||||
en_US: Used to control the repeatability when generating models. Increasing repetition_penalty can reduce the duplication of model generation. 1.0 means no punishment.
|
en_US: Used to control the repeatability when generating models. Increasing repetition_penalty can reduce the duplication of model generation. 1.0 means no punishment.
|
||||||
- name: enable_search
|
|
||||||
type: boolean
|
|
||||||
default: false
|
|
||||||
label:
|
|
||||||
zh_Hans: 联网搜索
|
|
||||||
en_US: Web Search
|
|
||||||
help:
|
|
||||||
zh_Hans: 模型内置了互联网搜索服务,该参数控制模型在生成文本时是否参考使用互联网搜索结果。启用互联网搜索,模型会将搜索结果作为文本生成过程中的参考信息,但模型会基于其内部逻辑“自行判断”是否使用互联网搜索结果。
|
|
||||||
en_US: The model has a built-in Internet search service. This parameter controls whether the model refers to Internet search results when generating text. When Internet search is enabled, the model will use the search results as reference information in the text generation process, but the model will "judge" whether to use Internet search results based on its internal logic.
|
|
||||||
- name: response_format
|
- name: response_format
|
||||||
use_template: response_format
|
use_template: response_format
|
||||||
pricing:
|
pricing:
|
||||||
|
|||||||
@ -67,15 +67,6 @@ parameter_rules:
|
|||||||
help:
|
help:
|
||||||
zh_Hans: 用于控制模型生成时的重复度。提高repetition_penalty时可以降低模型生成的重复度。1.0表示不做惩罚。
|
zh_Hans: 用于控制模型生成时的重复度。提高repetition_penalty时可以降低模型生成的重复度。1.0表示不做惩罚。
|
||||||
en_US: Used to control the repeatability when generating models. Increasing repetition_penalty can reduce the duplication of model generation. 1.0 means no punishment.
|
en_US: Used to control the repeatability when generating models. Increasing repetition_penalty can reduce the duplication of model generation. 1.0 means no punishment.
|
||||||
- name: enable_search
|
|
||||||
type: boolean
|
|
||||||
default: false
|
|
||||||
label:
|
|
||||||
zh_Hans: 联网搜索
|
|
||||||
en_US: Web Search
|
|
||||||
help:
|
|
||||||
zh_Hans: 模型内置了互联网搜索服务,该参数控制模型在生成文本时是否参考使用互联网搜索结果。启用互联网搜索,模型会将搜索结果作为文本生成过程中的参考信息,但模型会基于其内部逻辑“自行判断”是否使用互联网搜索结果。
|
|
||||||
en_US: The model has a built-in Internet search service. This parameter controls whether the model refers to Internet search results when generating text. When Internet search is enabled, the model will use the search results as reference information in the text generation process, but the model will "judge" whether to use Internet search results based on its internal logic.
|
|
||||||
- name: response_format
|
- name: response_format
|
||||||
use_template: response_format
|
use_template: response_format
|
||||||
pricing:
|
pricing:
|
||||||
|
|||||||
@ -69,15 +69,6 @@ parameter_rules:
|
|||||||
help:
|
help:
|
||||||
zh_Hans: 用于控制模型生成时的重复度。提高repetition_penalty时可以降低模型生成的重复度。1.0表示不做惩罚。
|
zh_Hans: 用于控制模型生成时的重复度。提高repetition_penalty时可以降低模型生成的重复度。1.0表示不做惩罚。
|
||||||
en_US: Used to control the repeatability when generating models. Increasing repetition_penalty can reduce the duplication of model generation. 1.0 means no punishment.
|
en_US: Used to control the repeatability when generating models. Increasing repetition_penalty can reduce the duplication of model generation. 1.0 means no punishment.
|
||||||
- name: enable_search
|
|
||||||
type: boolean
|
|
||||||
default: false
|
|
||||||
label:
|
|
||||||
zh_Hans: 联网搜索
|
|
||||||
en_US: Web Search
|
|
||||||
help:
|
|
||||||
zh_Hans: 模型内置了互联网搜索服务,该参数控制模型在生成文本时是否参考使用互联网搜索结果。启用互联网搜索,模型会将搜索结果作为文本生成过程中的参考信息,但模型会基于其内部逻辑“自行判断”是否使用互联网搜索结果。
|
|
||||||
en_US: The model has a built-in Internet search service. This parameter controls whether the model refers to Internet search results when generating text. When Internet search is enabled, the model will use the search results as reference information in the text generation process, but the model will "judge" whether to use Internet search results based on its internal logic.
|
|
||||||
- name: response_format
|
- name: response_format
|
||||||
use_template: response_format
|
use_template: response_format
|
||||||
pricing:
|
pricing:
|
||||||
|
|||||||
@ -67,15 +67,6 @@ parameter_rules:
|
|||||||
help:
|
help:
|
||||||
zh_Hans: 用于控制模型生成时的重复度。提高repetition_penalty时可以降低模型生成的重复度。1.0表示不做惩罚。
|
zh_Hans: 用于控制模型生成时的重复度。提高repetition_penalty时可以降低模型生成的重复度。1.0表示不做惩罚。
|
||||||
en_US: Used to control the repeatability when generating models. Increasing repetition_penalty can reduce the duplication of model generation. 1.0 means no punishment.
|
en_US: Used to control the repeatability when generating models. Increasing repetition_penalty can reduce the duplication of model generation. 1.0 means no punishment.
|
||||||
- name: enable_search
|
|
||||||
type: boolean
|
|
||||||
default: false
|
|
||||||
label:
|
|
||||||
zh_Hans: 联网搜索
|
|
||||||
en_US: Web Search
|
|
||||||
help:
|
|
||||||
zh_Hans: 模型内置了互联网搜索服务,该参数控制模型在生成文本时是否参考使用互联网搜索结果。启用互联网搜索,模型会将搜索结果作为文本生成过程中的参考信息,但模型会基于其内部逻辑“自行判断”是否使用互联网搜索结果。
|
|
||||||
en_US: The model has a built-in Internet search service. This parameter controls whether the model refers to Internet search results when generating text. When Internet search is enabled, the model will use the search results as reference information in the text generation process, but the model will "judge" whether to use Internet search results based on its internal logic.
|
|
||||||
- name: response_format
|
- name: response_format
|
||||||
use_template: response_format
|
use_template: response_format
|
||||||
pricing:
|
pricing:
|
||||||
|
|||||||
@ -77,5 +77,4 @@
|
|||||||
- onebot
|
- onebot
|
||||||
- regex
|
- regex
|
||||||
- trello
|
- trello
|
||||||
- vanna
|
|
||||||
- fal
|
- fal
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 4.5 KiB |
@ -1,134 +0,0 @@
|
|||||||
from typing import Any, Union
|
|
||||||
|
|
||||||
from vanna.remote import VannaDefault # type: ignore
|
|
||||||
|
|
||||||
from core.tools.entities.tool_entities import ToolInvokeMessage
|
|
||||||
from core.tools.errors import ToolProviderCredentialValidationError
|
|
||||||
from core.tools.tool.builtin_tool import BuiltinTool
|
|
||||||
|
|
||||||
|
|
||||||
class VannaTool(BuiltinTool):
|
|
||||||
def _invoke(
|
|
||||||
self, user_id: str, tool_parameters: dict[str, Any]
|
|
||||||
) -> Union[ToolInvokeMessage, list[ToolInvokeMessage]]:
|
|
||||||
"""
|
|
||||||
invoke tools
|
|
||||||
"""
|
|
||||||
# Ensure runtime and credentials
|
|
||||||
if not self.runtime or not self.runtime.credentials:
|
|
||||||
raise ToolProviderCredentialValidationError("Tool runtime or credentials are missing")
|
|
||||||
api_key = self.runtime.credentials.get("api_key", None)
|
|
||||||
if not api_key:
|
|
||||||
raise ToolProviderCredentialValidationError("Please input api key")
|
|
||||||
|
|
||||||
model = tool_parameters.get("model", "")
|
|
||||||
if not model:
|
|
||||||
return self.create_text_message("Please input RAG model")
|
|
||||||
|
|
||||||
prompt = tool_parameters.get("prompt", "")
|
|
||||||
if not prompt:
|
|
||||||
return self.create_text_message("Please input prompt")
|
|
||||||
|
|
||||||
url = tool_parameters.get("url", "")
|
|
||||||
if not url:
|
|
||||||
return self.create_text_message("Please input URL/Host/DSN")
|
|
||||||
|
|
||||||
db_name = tool_parameters.get("db_name", "")
|
|
||||||
username = tool_parameters.get("username", "")
|
|
||||||
password = tool_parameters.get("password", "")
|
|
||||||
port = tool_parameters.get("port", 0)
|
|
||||||
|
|
||||||
base_url = self.runtime.credentials.get("base_url", None)
|
|
||||||
vn = VannaDefault(model=model, api_key=api_key, config={"endpoint": base_url})
|
|
||||||
|
|
||||||
db_type = tool_parameters.get("db_type", "")
|
|
||||||
if db_type in {"Postgres", "MySQL", "Hive", "ClickHouse"}:
|
|
||||||
if not db_name:
|
|
||||||
return self.create_text_message("Please input database name")
|
|
||||||
if not username:
|
|
||||||
return self.create_text_message("Please input username")
|
|
||||||
if port < 1:
|
|
||||||
return self.create_text_message("Please input port")
|
|
||||||
|
|
||||||
schema_sql = "SELECT * FROM INFORMATION_SCHEMA.COLUMNS"
|
|
||||||
match db_type:
|
|
||||||
case "SQLite":
|
|
||||||
schema_sql = "SELECT type, sql FROM sqlite_master WHERE sql is not null"
|
|
||||||
vn.connect_to_sqlite(url)
|
|
||||||
case "Postgres":
|
|
||||||
vn.connect_to_postgres(host=url, dbname=db_name, user=username, password=password, port=port)
|
|
||||||
case "DuckDB":
|
|
||||||
vn.connect_to_duckdb(url=url)
|
|
||||||
case "SQLServer":
|
|
||||||
vn.connect_to_mssql(url)
|
|
||||||
case "MySQL":
|
|
||||||
vn.connect_to_mysql(host=url, dbname=db_name, user=username, password=password, port=port)
|
|
||||||
case "Oracle":
|
|
||||||
vn.connect_to_oracle(user=username, password=password, dsn=url)
|
|
||||||
case "Hive":
|
|
||||||
vn.connect_to_hive(host=url, dbname=db_name, user=username, password=password, port=port)
|
|
||||||
case "ClickHouse":
|
|
||||||
vn.connect_to_clickhouse(host=url, dbname=db_name, user=username, password=password, port=port)
|
|
||||||
|
|
||||||
enable_training = tool_parameters.get("enable_training", False)
|
|
||||||
reset_training_data = tool_parameters.get("reset_training_data", False)
|
|
||||||
if enable_training:
|
|
||||||
if reset_training_data:
|
|
||||||
existing_training_data = vn.get_training_data()
|
|
||||||
if len(existing_training_data) > 0:
|
|
||||||
for _, training_data in existing_training_data.iterrows():
|
|
||||||
vn.remove_training_data(training_data["id"])
|
|
||||||
|
|
||||||
ddl = tool_parameters.get("ddl", "")
|
|
||||||
question = tool_parameters.get("question", "")
|
|
||||||
sql = tool_parameters.get("sql", "")
|
|
||||||
memos = tool_parameters.get("memos", "")
|
|
||||||
training_metadata = tool_parameters.get("training_metadata", False)
|
|
||||||
|
|
||||||
if training_metadata:
|
|
||||||
if db_type == "SQLite":
|
|
||||||
df_ddl = vn.run_sql(schema_sql)
|
|
||||||
for ddl in df_ddl["sql"].to_list():
|
|
||||||
vn.train(ddl=ddl)
|
|
||||||
else:
|
|
||||||
df_information_schema = vn.run_sql(schema_sql)
|
|
||||||
plan = vn.get_training_plan_generic(df_information_schema)
|
|
||||||
vn.train(plan=plan)
|
|
||||||
|
|
||||||
if ddl:
|
|
||||||
vn.train(ddl=ddl)
|
|
||||||
|
|
||||||
if sql:
|
|
||||||
if question:
|
|
||||||
vn.train(question=question, sql=sql)
|
|
||||||
else:
|
|
||||||
vn.train(sql=sql)
|
|
||||||
if memos:
|
|
||||||
vn.train(documentation=memos)
|
|
||||||
|
|
||||||
#########################################################################################
|
|
||||||
# Due to CVE-2024-5565, we have to disable the chart generation feature
|
|
||||||
# The Vanna library uses a prompt function to present the user with visualized results,
|
|
||||||
# it is possible to alter the prompt using prompt injection and run arbitrary Python code
|
|
||||||
# instead of the intended visualization code.
|
|
||||||
# Specifically - allowing external input to the library’s “ask” method
|
|
||||||
# with "visualize" set to True (default behavior) leads to remote code execution.
|
|
||||||
# Affected versions: <= 0.5.5
|
|
||||||
#########################################################################################
|
|
||||||
allow_llm_to_see_data = tool_parameters.get("allow_llm_to_see_data", False)
|
|
||||||
res = vn.ask(
|
|
||||||
prompt, print_results=False, auto_train=True, visualize=False, allow_llm_to_see_data=allow_llm_to_see_data
|
|
||||||
)
|
|
||||||
|
|
||||||
result = []
|
|
||||||
|
|
||||||
if res is not None:
|
|
||||||
result.append(self.create_text_message(res[0]))
|
|
||||||
if len(res) > 1 and res[1] is not None:
|
|
||||||
result.append(self.create_text_message(res[1].to_markdown()))
|
|
||||||
if len(res) > 2 and res[2] is not None:
|
|
||||||
result.append(
|
|
||||||
self.create_blob_message(blob=res[2].to_image(format="svg"), meta={"mime_type": "image/svg+xml"})
|
|
||||||
)
|
|
||||||
|
|
||||||
return result
|
|
||||||
@ -1,213 +0,0 @@
|
|||||||
identity:
|
|
||||||
name: vanna
|
|
||||||
author: QCTC
|
|
||||||
label:
|
|
||||||
en_US: Vanna.AI
|
|
||||||
zh_Hans: Vanna.AI
|
|
||||||
description:
|
|
||||||
human:
|
|
||||||
en_US: The fastest way to get actionable insights from your database just by asking questions.
|
|
||||||
zh_Hans: 一个基于大模型和RAG的Text2SQL工具。
|
|
||||||
llm: A tool for converting text to SQL.
|
|
||||||
parameters:
|
|
||||||
- name: prompt
|
|
||||||
type: string
|
|
||||||
required: true
|
|
||||||
label:
|
|
||||||
en_US: Prompt
|
|
||||||
zh_Hans: 提示词
|
|
||||||
pt_BR: Prompt
|
|
||||||
human_description:
|
|
||||||
en_US: used for generating SQL
|
|
||||||
zh_Hans: 用于生成SQL
|
|
||||||
llm_description: key words for generating SQL
|
|
||||||
form: llm
|
|
||||||
- name: model
|
|
||||||
type: string
|
|
||||||
required: true
|
|
||||||
label:
|
|
||||||
en_US: RAG Model
|
|
||||||
zh_Hans: RAG模型
|
|
||||||
human_description:
|
|
||||||
en_US: RAG Model for your database DDL
|
|
||||||
zh_Hans: 存储数据库训练数据的RAG模型
|
|
||||||
llm_description: RAG Model for generating SQL
|
|
||||||
form: llm
|
|
||||||
- name: db_type
|
|
||||||
type: select
|
|
||||||
required: true
|
|
||||||
options:
|
|
||||||
- value: SQLite
|
|
||||||
label:
|
|
||||||
en_US: SQLite
|
|
||||||
zh_Hans: SQLite
|
|
||||||
- value: Postgres
|
|
||||||
label:
|
|
||||||
en_US: Postgres
|
|
||||||
zh_Hans: Postgres
|
|
||||||
- value: DuckDB
|
|
||||||
label:
|
|
||||||
en_US: DuckDB
|
|
||||||
zh_Hans: DuckDB
|
|
||||||
- value: SQLServer
|
|
||||||
label:
|
|
||||||
en_US: Microsoft SQL Server
|
|
||||||
zh_Hans: 微软 SQL Server
|
|
||||||
- value: MySQL
|
|
||||||
label:
|
|
||||||
en_US: MySQL
|
|
||||||
zh_Hans: MySQL
|
|
||||||
- value: Oracle
|
|
||||||
label:
|
|
||||||
en_US: Oracle
|
|
||||||
zh_Hans: Oracle
|
|
||||||
- value: Hive
|
|
||||||
label:
|
|
||||||
en_US: Hive
|
|
||||||
zh_Hans: Hive
|
|
||||||
- value: ClickHouse
|
|
||||||
label:
|
|
||||||
en_US: ClickHouse
|
|
||||||
zh_Hans: ClickHouse
|
|
||||||
default: SQLite
|
|
||||||
label:
|
|
||||||
en_US: DB Type
|
|
||||||
zh_Hans: 数据库类型
|
|
||||||
human_description:
|
|
||||||
en_US: Database type.
|
|
||||||
zh_Hans: 选择要链接的数据库类型。
|
|
||||||
form: form
|
|
||||||
- name: url
|
|
||||||
type: string
|
|
||||||
required: true
|
|
||||||
label:
|
|
||||||
en_US: URL/Host/DSN
|
|
||||||
zh_Hans: URL/Host/DSN
|
|
||||||
human_description:
|
|
||||||
en_US: Please input depending on DB type, visit https://vanna.ai/docs/ for more specification
|
|
||||||
zh_Hans: 请根据数据库类型,填入对应值,详情参考https://vanna.ai/docs/
|
|
||||||
form: form
|
|
||||||
- name: db_name
|
|
||||||
type: string
|
|
||||||
required: false
|
|
||||||
label:
|
|
||||||
en_US: DB name
|
|
||||||
zh_Hans: 数据库名
|
|
||||||
human_description:
|
|
||||||
en_US: Database name
|
|
||||||
zh_Hans: 数据库名
|
|
||||||
form: form
|
|
||||||
- name: username
|
|
||||||
type: string
|
|
||||||
required: false
|
|
||||||
label:
|
|
||||||
en_US: Username
|
|
||||||
zh_Hans: 用户名
|
|
||||||
human_description:
|
|
||||||
en_US: Username
|
|
||||||
zh_Hans: 用户名
|
|
||||||
form: form
|
|
||||||
- name: password
|
|
||||||
type: secret-input
|
|
||||||
required: false
|
|
||||||
label:
|
|
||||||
en_US: Password
|
|
||||||
zh_Hans: 密码
|
|
||||||
human_description:
|
|
||||||
en_US: Password
|
|
||||||
zh_Hans: 密码
|
|
||||||
form: form
|
|
||||||
- name: port
|
|
||||||
type: number
|
|
||||||
required: false
|
|
||||||
label:
|
|
||||||
en_US: Port
|
|
||||||
zh_Hans: 端口
|
|
||||||
human_description:
|
|
||||||
en_US: Port
|
|
||||||
zh_Hans: 端口
|
|
||||||
form: form
|
|
||||||
- name: ddl
|
|
||||||
type: string
|
|
||||||
required: false
|
|
||||||
label:
|
|
||||||
en_US: Training DDL
|
|
||||||
zh_Hans: 训练DDL
|
|
||||||
human_description:
|
|
||||||
en_US: DDL statements for training data
|
|
||||||
zh_Hans: 用于训练RAG Model的建表语句
|
|
||||||
form: llm
|
|
||||||
- name: question
|
|
||||||
type: string
|
|
||||||
required: false
|
|
||||||
label:
|
|
||||||
en_US: Training Question
|
|
||||||
zh_Hans: 训练问题
|
|
||||||
human_description:
|
|
||||||
en_US: Question-SQL Pairs
|
|
||||||
zh_Hans: Question-SQL中的问题
|
|
||||||
form: llm
|
|
||||||
- name: sql
|
|
||||||
type: string
|
|
||||||
required: false
|
|
||||||
label:
|
|
||||||
en_US: Training SQL
|
|
||||||
zh_Hans: 训练SQL
|
|
||||||
human_description:
|
|
||||||
en_US: SQL queries to your training data
|
|
||||||
zh_Hans: 用于训练RAG Model的SQL语句
|
|
||||||
form: llm
|
|
||||||
- name: memos
|
|
||||||
type: string
|
|
||||||
required: false
|
|
||||||
label:
|
|
||||||
en_US: Training Memos
|
|
||||||
zh_Hans: 训练说明
|
|
||||||
human_description:
|
|
||||||
en_US: Sometimes you may want to add documentation about your business terminology or definitions
|
|
||||||
zh_Hans: 添加更多关于数据库的业务说明
|
|
||||||
form: llm
|
|
||||||
- name: enable_training
|
|
||||||
type: boolean
|
|
||||||
required: false
|
|
||||||
default: false
|
|
||||||
label:
|
|
||||||
en_US: Training Data
|
|
||||||
zh_Hans: 训练数据
|
|
||||||
human_description:
|
|
||||||
en_US: You only need to train once. Do not train again unless you want to add more training data
|
|
||||||
zh_Hans: 训练数据无更新时,训练一次即可
|
|
||||||
form: form
|
|
||||||
- name: reset_training_data
|
|
||||||
type: boolean
|
|
||||||
required: false
|
|
||||||
default: false
|
|
||||||
label:
|
|
||||||
en_US: Reset Training Data
|
|
||||||
zh_Hans: 重置训练数据
|
|
||||||
human_description:
|
|
||||||
en_US: Remove all training data in the current RAG Model
|
|
||||||
zh_Hans: 删除当前RAG Model中的所有训练数据
|
|
||||||
form: form
|
|
||||||
- name: training_metadata
|
|
||||||
type: boolean
|
|
||||||
required: false
|
|
||||||
default: false
|
|
||||||
label:
|
|
||||||
en_US: Training Metadata
|
|
||||||
zh_Hans: 训练元数据
|
|
||||||
human_description:
|
|
||||||
en_US: If enabled, it will attempt to train on the metadata of that database
|
|
||||||
zh_Hans: 是否自动从数据库获取元数据来训练
|
|
||||||
form: form
|
|
||||||
- name: allow_llm_to_see_data
|
|
||||||
type: boolean
|
|
||||||
required: false
|
|
||||||
default: false
|
|
||||||
label:
|
|
||||||
en_US: Whether to allow the LLM to see the data
|
|
||||||
zh_Hans: 是否允许LLM查看数据
|
|
||||||
human_description:
|
|
||||||
en_US: Whether to allow the LLM to see the data
|
|
||||||
zh_Hans: 是否允许LLM查看数据
|
|
||||||
form: form
|
|
||||||
@ -1,46 +0,0 @@
|
|||||||
import re
|
|
||||||
from typing import Any
|
|
||||||
from urllib.parse import urlparse
|
|
||||||
|
|
||||||
from core.tools.errors import ToolProviderCredentialValidationError
|
|
||||||
from core.tools.provider.builtin.vanna.tools.vanna import VannaTool
|
|
||||||
from core.tools.provider.builtin_tool_provider import BuiltinToolProviderController
|
|
||||||
|
|
||||||
|
|
||||||
class VannaProvider(BuiltinToolProviderController):
|
|
||||||
def _get_protocol_and_main_domain(self, url):
|
|
||||||
parsed_url = urlparse(url)
|
|
||||||
protocol = parsed_url.scheme
|
|
||||||
hostname = parsed_url.hostname
|
|
||||||
port = f":{parsed_url.port}" if parsed_url.port else ""
|
|
||||||
|
|
||||||
# Check if the hostname is an IP address
|
|
||||||
is_ip = re.match(r"^\d{1,3}(\.\d{1,3}){3}$", hostname) is not None
|
|
||||||
|
|
||||||
# Return the full hostname (with port if present) for IP addresses, otherwise return the main domain
|
|
||||||
main_domain = f"{hostname}{port}" if is_ip else ".".join(hostname.split(".")[-2:]) + port
|
|
||||||
return f"{protocol}://{main_domain}"
|
|
||||||
|
|
||||||
def _validate_credentials(self, credentials: dict[str, Any]) -> None:
|
|
||||||
base_url = credentials.get("base_url")
|
|
||||||
if not base_url:
|
|
||||||
base_url = "https://ask.vanna.ai/rpc"
|
|
||||||
else:
|
|
||||||
base_url = base_url.removesuffix("/")
|
|
||||||
credentials["base_url"] = base_url
|
|
||||||
try:
|
|
||||||
VannaTool().fork_tool_runtime(
|
|
||||||
runtime={
|
|
||||||
"credentials": credentials,
|
|
||||||
}
|
|
||||||
).invoke(
|
|
||||||
user_id="",
|
|
||||||
tool_parameters={
|
|
||||||
"model": "chinook",
|
|
||||||
"db_type": "SQLite",
|
|
||||||
"url": f"{self._get_protocol_and_main_domain(credentials['base_url'])}/Chinook.sqlite",
|
|
||||||
"query": "What are the top 10 customers by sales?",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
raise ToolProviderCredentialValidationError(str(e))
|
|
||||||
@ -1,35 +0,0 @@
|
|||||||
identity:
|
|
||||||
author: QCTC
|
|
||||||
name: vanna
|
|
||||||
label:
|
|
||||||
en_US: Vanna.AI
|
|
||||||
zh_Hans: Vanna.AI
|
|
||||||
description:
|
|
||||||
en_US: The fastest way to get actionable insights from your database just by asking questions.
|
|
||||||
zh_Hans: 一个基于大模型和RAG的Text2SQL工具。
|
|
||||||
icon: icon.png
|
|
||||||
tags:
|
|
||||||
- utilities
|
|
||||||
- productivity
|
|
||||||
credentials_for_provider:
|
|
||||||
api_key:
|
|
||||||
type: secret-input
|
|
||||||
required: true
|
|
||||||
label:
|
|
||||||
en_US: API key
|
|
||||||
zh_Hans: API key
|
|
||||||
placeholder:
|
|
||||||
en_US: Please input your API key
|
|
||||||
zh_Hans: 请输入你的 API key
|
|
||||||
pt_BR: Please input your API key
|
|
||||||
help:
|
|
||||||
en_US: Get your API key from Vanna.AI
|
|
||||||
zh_Hans: 从 Vanna.AI 获取你的 API key
|
|
||||||
url: https://vanna.ai/account/profile
|
|
||||||
base_url:
|
|
||||||
type: text-input
|
|
||||||
required: false
|
|
||||||
label:
|
|
||||||
en_US: Vanna.AI Endpoint Base URL
|
|
||||||
placeholder:
|
|
||||||
en_US: https://ask.vanna.ai/rpc
|
|
||||||
@ -32,11 +32,7 @@ class AwsS3Storage(BaseStorage):
|
|||||||
aws_access_key_id=dify_config.S3_ACCESS_KEY,
|
aws_access_key_id=dify_config.S3_ACCESS_KEY,
|
||||||
endpoint_url=dify_config.S3_ENDPOINT,
|
endpoint_url=dify_config.S3_ENDPOINT,
|
||||||
region_name=dify_config.S3_REGION,
|
region_name=dify_config.S3_REGION,
|
||||||
config=Config(
|
config=Config(s3={"addressing_style": dify_config.S3_ADDRESS_STYLE}),
|
||||||
s3={"addressing_style": dify_config.S3_ADDRESS_STYLE},
|
|
||||||
request_checksum_calculation="when_required",
|
|
||||||
response_checksum_validation="when_required",
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
# create bucket
|
# create bucket
|
||||||
try:
|
try:
|
||||||
|
|||||||
@ -63,6 +63,7 @@ app_detail_fields = {
|
|||||||
"created_at": TimestampField,
|
"created_at": TimestampField,
|
||||||
"updated_by": fields.String,
|
"updated_by": fields.String,
|
||||||
"updated_at": TimestampField,
|
"updated_at": TimestampField,
|
||||||
|
"access_mode": fields.String,
|
||||||
}
|
}
|
||||||
|
|
||||||
prompt_config_fields = {
|
prompt_config_fields = {
|
||||||
@ -98,6 +99,7 @@ app_partial_fields = {
|
|||||||
"updated_by": fields.String,
|
"updated_by": fields.String,
|
||||||
"updated_at": TimestampField,
|
"updated_at": TimestampField,
|
||||||
"tags": fields.List(fields.Nested(tag_fields)),
|
"tags": fields.List(fields.Nested(tag_fields)),
|
||||||
|
"access_mode": fields.String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -170,6 +172,7 @@ app_detail_fields_with_site = {
|
|||||||
"updated_by": fields.String,
|
"updated_by": fields.String,
|
||||||
"updated_at": TimestampField,
|
"updated_at": TimestampField,
|
||||||
"deleted_tools": fields.List(fields.String),
|
"deleted_tools": fields.List(fields.String),
|
||||||
|
"access_mode": fields.String,
|
||||||
}
|
}
|
||||||
|
|
||||||
app_site_fields = {
|
app_site_fields = {
|
||||||
|
|||||||
@ -77,7 +77,6 @@ class AccountService:
|
|||||||
prefix="email_code_account_deletion_rate_limit", max_attempts=1, time_window=60 * 1
|
prefix="email_code_account_deletion_rate_limit", max_attempts=1, time_window=60 * 1
|
||||||
)
|
)
|
||||||
LOGIN_MAX_ERROR_LIMITS = 5
|
LOGIN_MAX_ERROR_LIMITS = 5
|
||||||
FORGOT_PASSWORD_MAX_ERROR_LIMITS = 5
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _get_refresh_token_key(refresh_token: str) -> str:
|
def _get_refresh_token_key(refresh_token: str) -> str:
|
||||||
@ -407,10 +406,8 @@ class AccountService:
|
|||||||
|
|
||||||
raise PasswordResetRateLimitExceededError()
|
raise PasswordResetRateLimitExceededError()
|
||||||
|
|
||||||
code = "".join([str(random.randint(0, 9)) for _ in range(6)])
|
code, token = cls.generate_reset_password_token(account_email, account)
|
||||||
token = TokenManager.generate_token(
|
|
||||||
account=account, email=email, token_type="reset_password", additional_data={"code": code}
|
|
||||||
)
|
|
||||||
send_reset_password_mail_task.delay(
|
send_reset_password_mail_task.delay(
|
||||||
language=language,
|
language=language,
|
||||||
to=account_email,
|
to=account_email,
|
||||||
@ -419,6 +416,22 @@ class AccountService:
|
|||||||
cls.reset_password_rate_limiter.increment_rate_limit(account_email)
|
cls.reset_password_rate_limiter.increment_rate_limit(account_email)
|
||||||
return token
|
return token
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def generate_reset_password_token(
|
||||||
|
cls,
|
||||||
|
email: str,
|
||||||
|
account: Optional[Account] = None,
|
||||||
|
code: Optional[str] = None,
|
||||||
|
additional_data: dict[str, Any] = {},
|
||||||
|
):
|
||||||
|
if not code:
|
||||||
|
code = "".join([str(random.randint(0, 9)) for _ in range(6)])
|
||||||
|
additional_data["code"] = code
|
||||||
|
token = TokenManager.generate_token(
|
||||||
|
account=account, email=email, token_type="reset_password", additional_data=additional_data
|
||||||
|
)
|
||||||
|
return code, token
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def revoke_reset_password_token(cls, token: str):
|
def revoke_reset_password_token(cls, token: str):
|
||||||
TokenManager.revoke_token(token, "reset_password")
|
TokenManager.revoke_token(token, "reset_password")
|
||||||
@ -504,32 +517,6 @@ class AccountService:
|
|||||||
key = f"login_error_rate_limit:{email}"
|
key = f"login_error_rate_limit:{email}"
|
||||||
redis_client.delete(key)
|
redis_client.delete(key)
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def add_forgot_password_error_rate_limit(email: str) -> None:
|
|
||||||
key = f"forgot_password_error_rate_limit:{email}"
|
|
||||||
count = redis_client.get(key)
|
|
||||||
if count is None:
|
|
||||||
count = 0
|
|
||||||
count = int(count) + 1
|
|
||||||
redis_client.setex(key, dify_config.FORGOT_PASSWORD_LOCKOUT_DURATION, count)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def is_forgot_password_error_rate_limit(email: str) -> bool:
|
|
||||||
key = f"forgot_password_error_rate_limit:{email}"
|
|
||||||
count = redis_client.get(key)
|
|
||||||
if count is None:
|
|
||||||
return False
|
|
||||||
|
|
||||||
count = int(count)
|
|
||||||
if count > AccountService.FORGOT_PASSWORD_MAX_ERROR_LIMITS:
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def reset_forgot_password_error_rate_limit(email: str):
|
|
||||||
key = f"forgot_password_error_rate_limit:{email}"
|
|
||||||
redis_client.delete(key)
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def is_email_send_ip_limit(ip_address: str):
|
def is_email_send_ip_limit(ip_address: str):
|
||||||
minute_key = f"email_send_ip_limit_minute:{ip_address}"
|
minute_key = f"email_send_ip_limit_minute:{ip_address}"
|
||||||
|
|||||||
@ -19,8 +19,10 @@ from core.tools.utils.configuration import ToolParameterConfigurationManager
|
|||||||
from events.app_event import app_was_created
|
from events.app_event import app_was_created
|
||||||
from extensions.ext_database import db
|
from extensions.ext_database import db
|
||||||
from models.account import Account
|
from models.account import Account
|
||||||
from models.model import App, AppMode, AppModelConfig
|
from models.model import App, AppMode, AppModelConfig, Site
|
||||||
from models.tools import ApiToolProvider
|
from models.tools import ApiToolProvider
|
||||||
|
from services.enterprise.enterprise_service import EnterpriseService
|
||||||
|
from services.feature_service import FeatureService
|
||||||
from services.tag_service import TagService
|
from services.tag_service import TagService
|
||||||
from tasks.remove_app_and_related_data_task import remove_app_and_related_data_task
|
from tasks.remove_app_and_related_data_task import remove_app_and_related_data_task
|
||||||
|
|
||||||
@ -152,6 +154,10 @@ class AppService:
|
|||||||
|
|
||||||
app_was_created.send(app, account=account)
|
app_was_created.send(app, account=account)
|
||||||
|
|
||||||
|
if FeatureService.get_system_features().webapp_auth.enabled:
|
||||||
|
# update web app setting as private
|
||||||
|
EnterpriseService.WebAppAuth.update_app_access_mode(app.id, "private")
|
||||||
|
|
||||||
return app
|
return app
|
||||||
|
|
||||||
def get_app(self, app: App) -> App:
|
def get_app(self, app: App) -> App:
|
||||||
@ -308,6 +314,10 @@ class AppService:
|
|||||||
db.session.delete(app)
|
db.session.delete(app)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
|
# clean up web app settings
|
||||||
|
if FeatureService.get_system_features().webapp_auth.enabled:
|
||||||
|
EnterpriseService.WebAppAuth.cleanup_webapp(app.id)
|
||||||
|
|
||||||
# Trigger asynchronous deletion of app and related data
|
# Trigger asynchronous deletion of app and related data
|
||||||
remove_app_and_related_data_task.delay(tenant_id=app.tenant_id, app_id=app.id)
|
remove_app_and_related_data_task.delay(tenant_id=app.tenant_id, app_id=app.id)
|
||||||
|
|
||||||
@ -374,3 +384,15 @@ class AppService:
|
|||||||
meta["tool_icons"][tool_name] = {"background": "#252525", "content": "\ud83d\ude01"}
|
meta["tool_icons"][tool_name] = {"background": "#252525", "content": "\ud83d\ude01"}
|
||||||
|
|
||||||
return meta
|
return meta
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_app_code_by_id(app_id: str) -> str:
|
||||||
|
"""
|
||||||
|
Get app code by app id
|
||||||
|
:param app_id: app id
|
||||||
|
:return: app code
|
||||||
|
"""
|
||||||
|
site = db.session.query(Site).filter(Site.app_id == app_id).first()
|
||||||
|
if not site:
|
||||||
|
raise ValueError(f"App with id {app_id} not found")
|
||||||
|
return str(site.code)
|
||||||
|
|||||||
@ -1,11 +1,87 @@
|
|||||||
|
|
||||||
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
from services.enterprise.base import EnterpriseRequest
|
from services.enterprise.base import EnterpriseRequest
|
||||||
|
|
||||||
|
|
||||||
|
class WebAppSettings(BaseModel):
|
||||||
|
access_mode: str = Field(
|
||||||
|
description="Access mode for the web app. Can be 'public' or 'private'",
|
||||||
|
default="private",
|
||||||
|
alias="accessMode",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class EnterpriseService:
|
class EnterpriseService:
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_info(cls):
|
def get_info(cls):
|
||||||
return EnterpriseRequest.send_request("GET", "/info")
|
return EnterpriseRequest.send_request("GET", "/info")
|
||||||
|
|
||||||
@classmethod
|
class WebAppAuth:
|
||||||
def get_app_web_sso_enabled(cls, app_code):
|
@classmethod
|
||||||
return EnterpriseRequest.send_request("GET", f"/app-sso-setting?appCode={app_code}")
|
def is_user_allowed_to_access_webapp(cls, user_id: str, app_code: str) -> bool:
|
||||||
|
params = {"userId": user_id, "appCode": app_code}
|
||||||
|
data = EnterpriseRequest.send_request("GET", "/webapp/permission", params=params)
|
||||||
|
|
||||||
|
return data.get("result", False)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_app_access_mode_by_id(cls, app_id: str) -> WebAppSettings:
|
||||||
|
if not app_id:
|
||||||
|
raise ValueError("app_id must be provided.")
|
||||||
|
params = {"appId": app_id}
|
||||||
|
data = EnterpriseRequest.send_request("GET", "/webapp/access-mode/id", params=params)
|
||||||
|
if not data:
|
||||||
|
raise ValueError("No data found.")
|
||||||
|
return WebAppSettings(**data)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def batch_get_app_access_mode_by_id(cls, app_ids: list[str]) -> dict[str, WebAppSettings]:
|
||||||
|
if not app_ids:
|
||||||
|
return {}
|
||||||
|
body = {"appIds": app_ids}
|
||||||
|
data: dict[str, str] = EnterpriseRequest.send_request("POST", "/webapp/access-mode/batch/id", json=body)
|
||||||
|
if not data:
|
||||||
|
raise ValueError("No data found.")
|
||||||
|
|
||||||
|
if not isinstance(data["accessModes"], dict):
|
||||||
|
raise ValueError("Invalid data format.")
|
||||||
|
|
||||||
|
ret = {}
|
||||||
|
for key, value in data["accessModes"].items():
|
||||||
|
curr = WebAppSettings()
|
||||||
|
curr.access_mode = value
|
||||||
|
ret[key] = curr
|
||||||
|
|
||||||
|
return ret
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_app_access_mode_by_code(cls, app_code: str) -> WebAppSettings:
|
||||||
|
if not app_code:
|
||||||
|
raise ValueError("app_code must be provided.")
|
||||||
|
params = {"appCode": app_code}
|
||||||
|
data = EnterpriseRequest.send_request("GET", "/webapp/access-mode/code", params=params)
|
||||||
|
if not data:
|
||||||
|
raise ValueError("No data found.")
|
||||||
|
return WebAppSettings(**data)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def update_app_access_mode(cls, app_id: str, access_mode: str) -> bool:
|
||||||
|
if not app_id:
|
||||||
|
raise ValueError("app_id must be provided.")
|
||||||
|
if access_mode not in ["public", "private", "private_all"]:
|
||||||
|
raise ValueError("access_mode must be either 'public', 'private', or 'private_all'")
|
||||||
|
|
||||||
|
data = {"appId": app_id, "accessMode": access_mode}
|
||||||
|
|
||||||
|
response = EnterpriseRequest.send_request("POST", "/webapp/access-mode", json=data)
|
||||||
|
|
||||||
|
return response.get("result", False)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def cleanup_webapp(cls, app_id: str):
|
||||||
|
if not app_id:
|
||||||
|
raise ValueError("app_id must be provided.")
|
||||||
|
|
||||||
|
body = {"appId": app_id}
|
||||||
|
EnterpriseRequest.send_request("DELETE", "/webapp/clean", json=body)
|
||||||
|
|||||||
18
api/services/enterprise/mail_service.py
Normal file
18
api/services/enterprise/mail_service.py
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
from tasks.mail_enterprise_task import send_enterprise_email_task
|
||||||
|
|
||||||
|
|
||||||
|
class DifyMail(BaseModel):
|
||||||
|
to: list[str]
|
||||||
|
subject: str
|
||||||
|
body: str
|
||||||
|
substitutions: dict[str, str] = {}
|
||||||
|
|
||||||
|
|
||||||
|
class EnterpriseMailService:
|
||||||
|
@classmethod
|
||||||
|
def send_mail(cls, mail: DifyMail):
|
||||||
|
send_enterprise_email_task.delay(
|
||||||
|
to=mail.to, subject=mail.subject, body=mail.body, substitutions=mail.substitutions
|
||||||
|
)
|
||||||
@ -36,6 +36,26 @@ class LicenseModel(BaseModel):
|
|||||||
expired_at: str = ""
|
expired_at: str = ""
|
||||||
|
|
||||||
|
|
||||||
|
class BrandingModel(BaseModel):
|
||||||
|
enabled: bool = False
|
||||||
|
application_title: str = ""
|
||||||
|
login_page_logo: str = ""
|
||||||
|
workspace_logo: str = ""
|
||||||
|
favicon: str = ""
|
||||||
|
|
||||||
|
|
||||||
|
class WebAppAuthSSOModel(BaseModel):
|
||||||
|
protocol: str = ""
|
||||||
|
|
||||||
|
|
||||||
|
class WebAppAuthModel(BaseModel):
|
||||||
|
enabled: bool = False
|
||||||
|
allow_sso: bool = False
|
||||||
|
sso_config: WebAppAuthSSOModel = WebAppAuthSSOModel()
|
||||||
|
allow_email_code_login: bool = False
|
||||||
|
allow_email_password_login: bool = False
|
||||||
|
|
||||||
|
|
||||||
class FeatureModel(BaseModel):
|
class FeatureModel(BaseModel):
|
||||||
billing: BillingModel = BillingModel()
|
billing: BillingModel = BillingModel()
|
||||||
members: LimitationModel = LimitationModel(size=0, limit=1)
|
members: LimitationModel = LimitationModel(size=0, limit=1)
|
||||||
@ -47,6 +67,7 @@ class FeatureModel(BaseModel):
|
|||||||
can_replace_logo: bool = False
|
can_replace_logo: bool = False
|
||||||
model_load_balancing_enabled: bool = False
|
model_load_balancing_enabled: bool = False
|
||||||
dataset_operator_enabled: bool = False
|
dataset_operator_enabled: bool = False
|
||||||
|
webapp_copyright_enabled: bool = False
|
||||||
|
|
||||||
# pydantic configs
|
# pydantic configs
|
||||||
model_config = ConfigDict(protected_namespaces=())
|
model_config = ConfigDict(protected_namespaces=())
|
||||||
@ -55,9 +76,6 @@ class FeatureModel(BaseModel):
|
|||||||
class SystemFeatureModel(BaseModel):
|
class SystemFeatureModel(BaseModel):
|
||||||
sso_enforced_for_signin: bool = False
|
sso_enforced_for_signin: bool = False
|
||||||
sso_enforced_for_signin_protocol: str = ""
|
sso_enforced_for_signin_protocol: str = ""
|
||||||
sso_enforced_for_web: bool = False
|
|
||||||
sso_enforced_for_web_protocol: str = ""
|
|
||||||
enable_web_sso_switch_component: bool = False
|
|
||||||
enable_email_code_login: bool = False
|
enable_email_code_login: bool = False
|
||||||
enable_email_password_login: bool = True
|
enable_email_password_login: bool = True
|
||||||
enable_social_oauth_login: bool = False
|
enable_social_oauth_login: bool = False
|
||||||
@ -65,6 +83,8 @@ class SystemFeatureModel(BaseModel):
|
|||||||
is_allow_create_workspace: bool = False
|
is_allow_create_workspace: bool = False
|
||||||
is_email_setup: bool = False
|
is_email_setup: bool = False
|
||||||
license: LicenseModel = LicenseModel()
|
license: LicenseModel = LicenseModel()
|
||||||
|
branding: BrandingModel = BrandingModel()
|
||||||
|
webapp_auth: WebAppAuthModel = WebAppAuthModel()
|
||||||
|
|
||||||
|
|
||||||
class FeatureService:
|
class FeatureService:
|
||||||
@ -77,6 +97,9 @@ class FeatureService:
|
|||||||
if dify_config.BILLING_ENABLED and tenant_id:
|
if dify_config.BILLING_ENABLED and tenant_id:
|
||||||
cls._fulfill_params_from_billing_api(features, tenant_id)
|
cls._fulfill_params_from_billing_api(features, tenant_id)
|
||||||
|
|
||||||
|
if dify_config.ENTERPRISE_ENABLED:
|
||||||
|
features.webapp_copyright_enabled = True
|
||||||
|
|
||||||
return features
|
return features
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@ -86,8 +109,8 @@ class FeatureService:
|
|||||||
cls._fulfill_system_params_from_env(system_features)
|
cls._fulfill_system_params_from_env(system_features)
|
||||||
|
|
||||||
if dify_config.ENTERPRISE_ENABLED:
|
if dify_config.ENTERPRISE_ENABLED:
|
||||||
system_features.enable_web_sso_switch_component = True
|
system_features.branding.enabled = True
|
||||||
|
system_features.webapp_auth.enabled = True
|
||||||
cls._fulfill_params_from_enterprise(system_features)
|
cls._fulfill_params_from_enterprise(system_features)
|
||||||
|
|
||||||
return system_features
|
return system_features
|
||||||
@ -115,6 +138,9 @@ class FeatureService:
|
|||||||
features.billing.subscription.plan = billing_info["subscription"]["plan"]
|
features.billing.subscription.plan = billing_info["subscription"]["plan"]
|
||||||
features.billing.subscription.interval = billing_info["subscription"]["interval"]
|
features.billing.subscription.interval = billing_info["subscription"]["interval"]
|
||||||
|
|
||||||
|
if features.billing.subscription.plan != "sandbox":
|
||||||
|
features.webapp_copyright_enabled = True
|
||||||
|
|
||||||
if "members" in billing_info:
|
if "members" in billing_info:
|
||||||
features.members.size = billing_info["members"]["size"]
|
features.members.size = billing_info["members"]["size"]
|
||||||
features.members.limit = billing_info["members"]["limit"]
|
features.members.limit = billing_info["members"]["limit"]
|
||||||
@ -145,38 +171,45 @@ class FeatureService:
|
|||||||
features.model_load_balancing_enabled = billing_info["model_load_balancing_enabled"]
|
features.model_load_balancing_enabled = billing_info["model_load_balancing_enabled"]
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _fulfill_params_from_enterprise(cls, features):
|
def _fulfill_params_from_enterprise(cls, features: SystemFeatureModel):
|
||||||
enterprise_info = EnterpriseService.get_info()
|
enterprise_info = EnterpriseService.get_info()
|
||||||
|
|
||||||
if "sso_enforced_for_signin" in enterprise_info:
|
if "SSOEnforcedForSignin" in enterprise_info:
|
||||||
features.sso_enforced_for_signin = enterprise_info["sso_enforced_for_signin"]
|
features.sso_enforced_for_signin = enterprise_info["SSOEnforcedForSignin"]
|
||||||
|
|
||||||
if "sso_enforced_for_signin_protocol" in enterprise_info:
|
if "EnableEmailCodeLogin" in enterprise_info:
|
||||||
features.sso_enforced_for_signin_protocol = enterprise_info["sso_enforced_for_signin_protocol"]
|
features.enable_email_code_login = enterprise_info["EnableEmailCodeLogin"]
|
||||||
|
|
||||||
if "sso_enforced_for_web" in enterprise_info:
|
if "EnableEmailPasswordLogin" in enterprise_info:
|
||||||
features.sso_enforced_for_web = enterprise_info["sso_enforced_for_web"]
|
features.enable_email_password_login = enterprise_info["EnableEmailPasswordLogin"]
|
||||||
|
|
||||||
if "sso_enforced_for_web_protocol" in enterprise_info:
|
if "IsAllowRegister" in enterprise_info:
|
||||||
features.sso_enforced_for_web_protocol = enterprise_info["sso_enforced_for_web_protocol"]
|
features.is_allow_register = enterprise_info["IsAllowRegister"]
|
||||||
|
|
||||||
if "enable_email_code_login" in enterprise_info:
|
if "IsAllowCreateWorkspace" in enterprise_info:
|
||||||
features.enable_email_code_login = enterprise_info["enable_email_code_login"]
|
features.is_allow_create_workspace = enterprise_info["IsAllowCreateWorkspace"]
|
||||||
|
|
||||||
if "enable_email_password_login" in enterprise_info:
|
if "Branding" in enterprise_info:
|
||||||
features.enable_email_password_login = enterprise_info["enable_email_password_login"]
|
features.branding.application_title = enterprise_info["Branding"].get("applicationTitle", "")
|
||||||
|
features.branding.login_page_logo = enterprise_info["Branding"].get("loginPageLogo", "")
|
||||||
|
features.branding.workspace_logo = enterprise_info["Branding"].get("workspaceLogo", "")
|
||||||
|
features.branding.favicon = enterprise_info["Branding"].get("favicon", "")
|
||||||
|
|
||||||
if "is_allow_register" in enterprise_info:
|
if "WebAppAuth" in enterprise_info:
|
||||||
features.is_allow_register = enterprise_info["is_allow_register"]
|
features.webapp_auth.allow_sso = enterprise_info["WebAppAuth"].get("allowSso", False)
|
||||||
|
features.webapp_auth.allow_email_code_login = enterprise_info["WebAppAuth"].get(
|
||||||
|
"allowEmailCodeLogin", False
|
||||||
|
)
|
||||||
|
features.webapp_auth.allow_email_password_login = enterprise_info["WebAppAuth"].get(
|
||||||
|
"allowEmailPasswordLogin", False
|
||||||
|
)
|
||||||
|
features.webapp_auth.sso_config.protocol = enterprise_info.get("SSOEnforcedForWebProtocol", "")
|
||||||
|
|
||||||
if "is_allow_create_workspace" in enterprise_info:
|
if "License" in enterprise_info:
|
||||||
features.is_allow_create_workspace = enterprise_info["is_allow_create_workspace"]
|
license_info = enterprise_info["License"]
|
||||||
|
|
||||||
if "license" in enterprise_info:
|
|
||||||
license_info = enterprise_info["license"]
|
|
||||||
|
|
||||||
if "status" in license_info:
|
if "status" in license_info:
|
||||||
features.license.status = LicenseStatus(license_info.get("status", LicenseStatus.INACTIVE))
|
features.license.status = LicenseStatus(license_info.get("status", LicenseStatus.INACTIVE))
|
||||||
|
|
||||||
if "expired_at" in license_info:
|
if "expiredAt" in license_info:
|
||||||
features.license.expired_at = license_info["expired_at"]
|
features.license.expired_at = license_info["expiredAt"]
|
||||||
|
|||||||
137
api/services/webapp_auth_service.py
Normal file
137
api/services/webapp_auth_service.py
Normal file
@ -0,0 +1,137 @@
|
|||||||
|
import random
|
||||||
|
from datetime import UTC, datetime, timedelta
|
||||||
|
from typing import Any, Optional, cast
|
||||||
|
|
||||||
|
from werkzeug.exceptions import NotFound, Unauthorized
|
||||||
|
|
||||||
|
from configs import dify_config
|
||||||
|
from controllers.web.error import WebAppAuthAccessDeniedError
|
||||||
|
from extensions.ext_database import db
|
||||||
|
from libs.helper import TokenManager
|
||||||
|
from libs.passport import PassportService
|
||||||
|
from libs.password import compare_password
|
||||||
|
from models.account import Account, AccountStatus
|
||||||
|
from models.model import App, EndUser, Site
|
||||||
|
from services.enterprise.enterprise_service import EnterpriseService
|
||||||
|
from services.errors.account import AccountLoginError, AccountNotFoundError, AccountPasswordError
|
||||||
|
from services.feature_service import FeatureService
|
||||||
|
from tasks.mail_email_code_login import send_email_code_login_mail_task
|
||||||
|
|
||||||
|
|
||||||
|
class WebAppAuthService:
|
||||||
|
"""Service for web app authentication."""
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def authenticate(email: str, password: str) -> Account:
|
||||||
|
"""authenticate account with email and password"""
|
||||||
|
|
||||||
|
account = Account.query.filter_by(email=email).first()
|
||||||
|
if not account:
|
||||||
|
raise AccountNotFoundError()
|
||||||
|
|
||||||
|
if account.status == AccountStatus.BANNED.value:
|
||||||
|
raise AccountLoginError("Account is banned.")
|
||||||
|
|
||||||
|
if account.password is None or not compare_password(password, account.password, account.password_salt):
|
||||||
|
raise AccountPasswordError("Invalid email or password.")
|
||||||
|
|
||||||
|
return cast(Account, account)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def login(cls, account: Account, app_code: str, end_user_id: str) -> str:
|
||||||
|
site = db.session.query(Site).filter(Site.code == app_code).first()
|
||||||
|
if not site:
|
||||||
|
raise NotFound("Site not found.")
|
||||||
|
|
||||||
|
access_token = cls._get_account_jwt_token(account=account, site=site, end_user_id=end_user_id)
|
||||||
|
|
||||||
|
return access_token
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_user_through_email(cls, email: str):
|
||||||
|
account = db.session.query(Account).filter(Account.email == email).first()
|
||||||
|
if not account:
|
||||||
|
return None
|
||||||
|
|
||||||
|
if account.status == AccountStatus.BANNED.value:
|
||||||
|
raise Unauthorized("Account is banned.")
|
||||||
|
|
||||||
|
return account
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def send_email_code_login_email(
|
||||||
|
cls, account: Optional[Account] = None, email: Optional[str] = None, language: Optional[str] = "en-US"
|
||||||
|
):
|
||||||
|
email = account.email if account else email
|
||||||
|
if email is None:
|
||||||
|
raise ValueError("Email must be provided.")
|
||||||
|
|
||||||
|
code = "".join([str(random.randint(0, 9)) for _ in range(6)])
|
||||||
|
token = TokenManager.generate_token(
|
||||||
|
account=account, email=email, token_type="webapp_email_code_login", additional_data={"code": code}
|
||||||
|
)
|
||||||
|
send_email_code_login_mail_task.delay(
|
||||||
|
language=language,
|
||||||
|
to=account.email if account else email,
|
||||||
|
code=code,
|
||||||
|
)
|
||||||
|
|
||||||
|
return token
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_email_code_login_data(cls, token: str) -> Optional[dict[str, Any]]:
|
||||||
|
return TokenManager.get_token_data(token, "webapp_email_code_login")
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def revoke_email_code_login_token(cls, token: str):
|
||||||
|
TokenManager.revoke_token(token, "webapp_email_code_login")
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def create_end_user(cls, app_code, email) -> EndUser:
|
||||||
|
site = db.session.query(Site).filter(Site.code == app_code).first()
|
||||||
|
app_model = db.session.query(App).filter(App.id == site.app_id).first()
|
||||||
|
end_user = EndUser(
|
||||||
|
tenant_id=app_model.tenant_id,
|
||||||
|
app_id=app_model.id,
|
||||||
|
type="browser",
|
||||||
|
is_anonymous=False,
|
||||||
|
session_id=email,
|
||||||
|
name="enterpriseuser",
|
||||||
|
external_user_id="enterpriseuser",
|
||||||
|
)
|
||||||
|
db.session.add(end_user)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
return end_user
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _validate_user_accessibility(cls, account: Account, app_code: str):
|
||||||
|
"""Check if the user is allowed to access the app."""
|
||||||
|
system_features = FeatureService.get_system_features()
|
||||||
|
if system_features.webapp_auth.enabled:
|
||||||
|
app_settings = EnterpriseService.WebAppAuth.get_app_access_mode_by_code(app_code=app_code)
|
||||||
|
|
||||||
|
if (
|
||||||
|
app_settings.access_mode != "public"
|
||||||
|
and not EnterpriseService.WebAppAuth.is_user_allowed_to_access_webapp(account.id, app_code=app_code)
|
||||||
|
):
|
||||||
|
raise WebAppAuthAccessDeniedError()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _get_account_jwt_token(cls, account: Account, site: Site, end_user_id: str) -> str:
|
||||||
|
exp_dt = datetime.now(UTC) + timedelta(hours=dify_config.WebAppSessionTimeoutInHours * 24)
|
||||||
|
exp = int(exp_dt.timestamp())
|
||||||
|
|
||||||
|
payload = {
|
||||||
|
"iss": site.id,
|
||||||
|
"sub": "Web API Passport",
|
||||||
|
"app_id": site.app_id,
|
||||||
|
"app_code": site.code,
|
||||||
|
"user_id": account.id,
|
||||||
|
"end_user_id": end_user_id,
|
||||||
|
"token_source": "webapp",
|
||||||
|
"exp": exp,
|
||||||
|
}
|
||||||
|
|
||||||
|
token: str = PassportService().issue(payload)
|
||||||
|
return token
|
||||||
@ -6,6 +6,7 @@ from celery import shared_task # type: ignore
|
|||||||
from flask import render_template
|
from flask import render_template
|
||||||
|
|
||||||
from extensions.ext_mail import mail
|
from extensions.ext_mail import mail
|
||||||
|
from services.feature_service import FeatureService
|
||||||
|
|
||||||
|
|
||||||
@shared_task(queue="mail")
|
@shared_task(queue="mail")
|
||||||
@ -25,10 +26,24 @@ def send_email_code_login_mail_task(language: str, to: str, code: str):
|
|||||||
# send email code login mail using different languages
|
# send email code login mail using different languages
|
||||||
try:
|
try:
|
||||||
if language == "zh-Hans":
|
if language == "zh-Hans":
|
||||||
html_content = render_template("email_code_login_mail_template_zh-CN.html", to=to, code=code)
|
template = "email_code_login_mail_template_zh-CN.html"
|
||||||
|
system_features = FeatureService.get_system_features()
|
||||||
|
if system_features.branding.enabled:
|
||||||
|
application_title = system_features.branding.application_title
|
||||||
|
template = "without-brand/email_code_login_mail_template_zh-CN.html"
|
||||||
|
html_content = render_template(template, to=to, code=code, application_title=application_title)
|
||||||
|
else:
|
||||||
|
html_content = render_template(template, to=to, code=code)
|
||||||
mail.send(to=to, subject="邮箱验证码", html=html_content)
|
mail.send(to=to, subject="邮箱验证码", html=html_content)
|
||||||
else:
|
else:
|
||||||
html_content = render_template("email_code_login_mail_template_en-US.html", to=to, code=code)
|
template = "email_code_login_mail_template_en-US.html"
|
||||||
|
system_features = FeatureService.get_system_features()
|
||||||
|
if system_features.branding.enabled:
|
||||||
|
application_title = system_features.branding.application_title
|
||||||
|
template = "without-brand/email_code_login_mail_template_en-US.html"
|
||||||
|
html_content = render_template(template, to=to, code=code, application_title=application_title)
|
||||||
|
else:
|
||||||
|
html_content = render_template(template, to=to, code=code)
|
||||||
mail.send(to=to, subject="Email Code", html=html_content)
|
mail.send(to=to, subject="Email Code", html=html_content)
|
||||||
|
|
||||||
end_at = time.perf_counter()
|
end_at = time.perf_counter()
|
||||||
|
|||||||
33
api/tasks/mail_enterprise_task.py
Normal file
33
api/tasks/mail_enterprise_task.py
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
import logging
|
||||||
|
import time
|
||||||
|
|
||||||
|
import click
|
||||||
|
from celery import shared_task # type: ignore
|
||||||
|
from flask import render_template_string
|
||||||
|
|
||||||
|
from extensions.ext_mail import mail
|
||||||
|
|
||||||
|
|
||||||
|
@shared_task(queue="mail")
|
||||||
|
def send_enterprise_email_task(to, subject, body, substitutions):
|
||||||
|
if not mail.is_inited():
|
||||||
|
return
|
||||||
|
|
||||||
|
logging.info(click.style("Start enterprise mail to {} with subject {}".format(to, subject), fg="green"))
|
||||||
|
start_at = time.perf_counter()
|
||||||
|
|
||||||
|
try:
|
||||||
|
html_content = render_template_string(body, **substitutions)
|
||||||
|
|
||||||
|
if isinstance(to, list):
|
||||||
|
for t in to:
|
||||||
|
mail.send(to=t, subject=subject, html=html_content)
|
||||||
|
else:
|
||||||
|
mail.send(to=to, subject=subject, html=html_content)
|
||||||
|
|
||||||
|
end_at = time.perf_counter()
|
||||||
|
logging.info(
|
||||||
|
click.style("Send enterprise mail to {} succeeded: latency: {}".format(to, end_at - start_at), fg="green")
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
logging.exception("Send enterprise mail to {} failed".format(to))
|
||||||
@ -7,6 +7,7 @@ from flask import render_template
|
|||||||
|
|
||||||
from configs import dify_config
|
from configs import dify_config
|
||||||
from extensions.ext_mail import mail
|
from extensions.ext_mail import mail
|
||||||
|
from services.feature_service import FeatureService
|
||||||
|
|
||||||
|
|
||||||
@shared_task(queue="mail")
|
@shared_task(queue="mail")
|
||||||
@ -33,23 +34,45 @@ def send_invite_member_mail_task(language: str, to: str, token: str, inviter_nam
|
|||||||
try:
|
try:
|
||||||
url = f"{dify_config.CONSOLE_WEB_URL}/activate?token={token}"
|
url = f"{dify_config.CONSOLE_WEB_URL}/activate?token={token}"
|
||||||
if language == "zh-Hans":
|
if language == "zh-Hans":
|
||||||
html_content = render_template(
|
template = "invite_member_mail_template_zh-CN.html"
|
||||||
"invite_member_mail_template_zh-CN.html",
|
system_features = FeatureService.get_system_features()
|
||||||
to=to,
|
if system_features.branding.enabled:
|
||||||
inviter_name=inviter_name,
|
application_title = system_features.branding.application_title
|
||||||
workspace_name=workspace_name,
|
template = "without-brand/invite_member_mail_template_zh-CN.html"
|
||||||
url=url,
|
html_content = render_template(
|
||||||
)
|
template,
|
||||||
mail.send(to=to, subject="立即加入 Dify 工作空间", html=html_content)
|
to=to,
|
||||||
|
inviter_name=inviter_name,
|
||||||
|
workspace_name=workspace_name,
|
||||||
|
url=url,
|
||||||
|
application_title=application_title,
|
||||||
|
)
|
||||||
|
mail.send(to=to, subject=f"立即加入 {application_title} 工作空间", html=html_content)
|
||||||
|
else:
|
||||||
|
html_content = render_template(
|
||||||
|
template, to=to, inviter_name=inviter_name, workspace_name=workspace_name, url=url
|
||||||
|
)
|
||||||
|
mail.send(to=to, subject="立即加入 Dify 工作空间", html=html_content)
|
||||||
else:
|
else:
|
||||||
html_content = render_template(
|
template = "invite_member_mail_template_en-US.html"
|
||||||
"invite_member_mail_template_en-US.html",
|
system_features = FeatureService.get_system_features()
|
||||||
to=to,
|
if system_features.branding.enabled:
|
||||||
inviter_name=inviter_name,
|
application_title = system_features.branding.application_title
|
||||||
workspace_name=workspace_name,
|
template = "without-brand/invite_member_mail_template_en-US.html"
|
||||||
url=url,
|
html_content = render_template(
|
||||||
)
|
template,
|
||||||
mail.send(to=to, subject="Join Dify Workspace Now", html=html_content)
|
to=to,
|
||||||
|
inviter_name=inviter_name,
|
||||||
|
workspace_name=workspace_name,
|
||||||
|
url=url,
|
||||||
|
application_title=application_title,
|
||||||
|
)
|
||||||
|
mail.send(to=to, subject=f"Join {application_title} Workspace Now", html=html_content)
|
||||||
|
else:
|
||||||
|
html_content = render_template(
|
||||||
|
template, to=to, inviter_name=inviter_name, workspace_name=workspace_name, url=url
|
||||||
|
)
|
||||||
|
mail.send(to=to, subject="Join Dify Workspace Now", html=html_content)
|
||||||
|
|
||||||
end_at = time.perf_counter()
|
end_at = time.perf_counter()
|
||||||
logging.info(
|
logging.info(
|
||||||
|
|||||||
@ -6,6 +6,7 @@ from celery import shared_task # type: ignore
|
|||||||
from flask import render_template
|
from flask import render_template
|
||||||
|
|
||||||
from extensions.ext_mail import mail
|
from extensions.ext_mail import mail
|
||||||
|
from services.feature_service import FeatureService
|
||||||
|
|
||||||
|
|
||||||
@shared_task(queue="mail")
|
@shared_task(queue="mail")
|
||||||
@ -25,11 +26,27 @@ def send_reset_password_mail_task(language: str, to: str, code: str):
|
|||||||
# send reset password mail using different languages
|
# send reset password mail using different languages
|
||||||
try:
|
try:
|
||||||
if language == "zh-Hans":
|
if language == "zh-Hans":
|
||||||
html_content = render_template("reset_password_mail_template_zh-CN.html", to=to, code=code)
|
template = "reset_password_mail_template_zh-CN.html"
|
||||||
mail.send(to=to, subject="设置您的 Dify 密码", html=html_content)
|
system_features = FeatureService.get_system_features()
|
||||||
|
if system_features.branding.enabled:
|
||||||
|
application_title = system_features.branding.application_title
|
||||||
|
template = "without-brand/reset_password_mail_template_zh-CN.html"
|
||||||
|
html_content = render_template(template, to=to, code=code, application_title=application_title)
|
||||||
|
mail.send(to=to, subject=f"设置您的 {application_title} 密码", html=html_content)
|
||||||
|
else:
|
||||||
|
html_content = render_template(template, to=to, code=code)
|
||||||
|
mail.send(to=to, subject="设置您的 Dify 密码", html=html_content)
|
||||||
else:
|
else:
|
||||||
html_content = render_template("reset_password_mail_template_en-US.html", to=to, code=code)
|
template = "reset_password_mail_template_en-US.html"
|
||||||
mail.send(to=to, subject="Set Your Dify Password", html=html_content)
|
system_features = FeatureService.get_system_features()
|
||||||
|
if system_features.branding.enabled:
|
||||||
|
application_title = system_features.branding.application_title
|
||||||
|
template = "without-brand/reset_password_mail_template_en-US.html"
|
||||||
|
html_content = render_template(template, to=to, code=code, application_title=application_title)
|
||||||
|
mail.send(to=to, subject=f"Set Your {application_title} Password", html=html_content)
|
||||||
|
else:
|
||||||
|
html_content = render_template(template, to=to, code=code)
|
||||||
|
mail.send(to=to, subject="Set Your Dify Password", html=html_content)
|
||||||
|
|
||||||
end_at = time.perf_counter()
|
end_at = time.perf_counter()
|
||||||
logging.info(
|
logging.info(
|
||||||
|
|||||||
@ -0,0 +1,70 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: 'Arial', sans-serif;
|
||||||
|
line-height: 16pt;
|
||||||
|
color: #101828;
|
||||||
|
background-color: #e9ebf0;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
.container {
|
||||||
|
width: 600px;
|
||||||
|
height: 360px;
|
||||||
|
margin: 40px auto;
|
||||||
|
padding: 36px 48px;
|
||||||
|
background-color: #fcfcfd;
|
||||||
|
border-radius: 16px;
|
||||||
|
border: 1px solid #ffffff;
|
||||||
|
box-shadow: 0 2px 4px -2px rgba(9, 9, 11, 0.08);
|
||||||
|
}
|
||||||
|
.header {
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
.header img {
|
||||||
|
max-width: 100px;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
.title {
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 24px;
|
||||||
|
line-height: 28.8px;
|
||||||
|
}
|
||||||
|
.description {
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 16px;
|
||||||
|
color: #676f83;
|
||||||
|
margin-top: 12px;
|
||||||
|
}
|
||||||
|
.code-content {
|
||||||
|
padding: 16px 32px;
|
||||||
|
text-align: center;
|
||||||
|
border-radius: 16px;
|
||||||
|
background-color: #f2f4f7;
|
||||||
|
margin: 16px auto;
|
||||||
|
}
|
||||||
|
.code {
|
||||||
|
line-height: 36px;
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 30px;
|
||||||
|
}
|
||||||
|
.tips {
|
||||||
|
line-height: 16px;
|
||||||
|
color: #676f83;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<p class="title">Your login code for {{application_title}}</p>
|
||||||
|
<p class="description">Copy and paste this code, this code will only be valid for the next 5 minutes.</p>
|
||||||
|
<div class="code-content">
|
||||||
|
<span class="code">{{code}}</span>
|
||||||
|
</div>
|
||||||
|
<p class="tips">If you didn't request a login, don't worry. You can safely ignore this email.</p>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@ -0,0 +1,70 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: 'Arial', sans-serif;
|
||||||
|
line-height: 16pt;
|
||||||
|
color: #101828;
|
||||||
|
background-color: #e9ebf0;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
.container {
|
||||||
|
width: 600px;
|
||||||
|
height: 360px;
|
||||||
|
margin: 40px auto;
|
||||||
|
padding: 36px 48px;
|
||||||
|
background-color: #fcfcfd;
|
||||||
|
border-radius: 16px;
|
||||||
|
border: 1px solid #ffffff;
|
||||||
|
box-shadow: 0 2px 4px -2px rgba(9, 9, 11, 0.08);
|
||||||
|
}
|
||||||
|
.header {
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
.header img {
|
||||||
|
max-width: 100px;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
.title {
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 24px;
|
||||||
|
line-height: 28.8px;
|
||||||
|
}
|
||||||
|
.description {
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 16px;
|
||||||
|
color: #676f83;
|
||||||
|
margin-top: 12px;
|
||||||
|
}
|
||||||
|
.code-content {
|
||||||
|
padding: 16px 32px;
|
||||||
|
text-align: center;
|
||||||
|
border-radius: 16px;
|
||||||
|
background-color: #f2f4f7;
|
||||||
|
margin: 16px auto;
|
||||||
|
}
|
||||||
|
.code {
|
||||||
|
line-height: 36px;
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 30px;
|
||||||
|
}
|
||||||
|
.tips {
|
||||||
|
line-height: 16px;
|
||||||
|
color: #676f83;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<p class="title">{{application_title}} 的登录验证码</p>
|
||||||
|
<p class="description">复制并粘贴此验证码,注意验证码仅在接下来的 5 分钟内有效。</p>
|
||||||
|
<div class="code-content">
|
||||||
|
<span class="code">{{code}}</span>
|
||||||
|
</div>
|
||||||
|
<p class="tips">如果您没有请求登录,请不要担心。您可以安全地忽略此电子邮件。</p>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@ -0,0 +1,69 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: 'Arial', sans-serif;
|
||||||
|
line-height: 16pt;
|
||||||
|
color: #374151;
|
||||||
|
background-color: #E5E7EB;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
.container {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 560px;
|
||||||
|
margin: 40px auto;
|
||||||
|
padding: 20px;
|
||||||
|
background-color: #F3F4F6;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
.header {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
.header img {
|
||||||
|
max-width: 100px;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
.button {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 12px 24px;
|
||||||
|
background-color: #2970FF;
|
||||||
|
color: white;
|
||||||
|
text-decoration: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
text-align: center;
|
||||||
|
transition: background-color 0.3s ease;
|
||||||
|
}
|
||||||
|
.button:hover {
|
||||||
|
background-color: #265DD4;
|
||||||
|
}
|
||||||
|
.footer {
|
||||||
|
font-size: 0.9em;
|
||||||
|
color: #777777;
|
||||||
|
margin-top: 30px;
|
||||||
|
}
|
||||||
|
.content {
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<div class="content">
|
||||||
|
<p>Dear {{ to }},</p>
|
||||||
|
<p>{{ inviter_name }} is pleased to invite you to join our workspace on {{application_title}}, a platform specifically designed for LLM application development. On {{application_title}}, you can explore, create, and collaborate to build and operate AI applications.</p>
|
||||||
|
<p>Click the button below to log in to {{application_title}} and join the workspace.</p>
|
||||||
|
<p style="text-align: center;"><a style="color: #fff; text-decoration: none" class="button" href="{{ url }}">Login Here</a></p>
|
||||||
|
</div>
|
||||||
|
<div class="footer">
|
||||||
|
<p>Best regards,</p>
|
||||||
|
<p>{{application_title}} Team</p>
|
||||||
|
<p>Please do not reply directly to this email; it is automatically sent by the system.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
@ -0,0 +1,69 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: 'Arial', sans-serif;
|
||||||
|
line-height: 16pt;
|
||||||
|
color: #374151;
|
||||||
|
background-color: #E5E7EB;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
.container {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 560px;
|
||||||
|
margin: 40px auto;
|
||||||
|
padding: 20px;
|
||||||
|
background-color: #F3F4F6;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
.header {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
.header img {
|
||||||
|
max-width: 100px;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
.button {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 12px 24px;
|
||||||
|
background-color: #2970FF;
|
||||||
|
color: white;
|
||||||
|
text-decoration: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
text-align: center;
|
||||||
|
transition: background-color 0.3s ease;
|
||||||
|
}
|
||||||
|
.button:hover {
|
||||||
|
background-color: #265DD4;
|
||||||
|
}
|
||||||
|
.footer {
|
||||||
|
font-size: 0.9em;
|
||||||
|
color: #777777;
|
||||||
|
margin-top: 30px;
|
||||||
|
}
|
||||||
|
.content {
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<div class="content">
|
||||||
|
<p>尊敬的 {{ to }},</p>
|
||||||
|
<p>{{ inviter_name }} 现邀请您加入我们在 {{application_title}} 的工作区,这是一个专为 LLM 应用开发而设计的平台。在 {{application_title}} 上,您可以探索、创造和合作,构建和运营 AI 应用。</p>
|
||||||
|
<p>点击下方按钮即可登录 {{application_title}} 并且加入空间。</p>
|
||||||
|
<p style="text-align: center;"><a style="color: #fff; text-decoration: none" class="button" href="{{ url }}">在此登录</a></p>
|
||||||
|
</div>
|
||||||
|
<div class="footer">
|
||||||
|
<p>此致,</p>
|
||||||
|
<p>{{application_title}} 团队</p>
|
||||||
|
<p>请不要直接回复此电子邮件;由系统自动发送。</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@ -0,0 +1,70 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: 'Arial', sans-serif;
|
||||||
|
line-height: 16pt;
|
||||||
|
color: #101828;
|
||||||
|
background-color: #e9ebf0;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
.container {
|
||||||
|
width: 600px;
|
||||||
|
height: 360px;
|
||||||
|
margin: 40px auto;
|
||||||
|
padding: 36px 48px;
|
||||||
|
background-color: #fcfcfd;
|
||||||
|
border-radius: 16px;
|
||||||
|
border: 1px solid #ffffff;
|
||||||
|
box-shadow: 0 2px 4px -2px rgba(9, 9, 11, 0.08);
|
||||||
|
}
|
||||||
|
.header {
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
.header img {
|
||||||
|
max-width: 100px;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
.title {
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 24px;
|
||||||
|
line-height: 28.8px;
|
||||||
|
}
|
||||||
|
.description {
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 16px;
|
||||||
|
color: #676f83;
|
||||||
|
margin-top: 12px;
|
||||||
|
}
|
||||||
|
.code-content {
|
||||||
|
padding: 16px 32px;
|
||||||
|
text-align: center;
|
||||||
|
border-radius: 16px;
|
||||||
|
background-color: #f2f4f7;
|
||||||
|
margin: 16px auto;
|
||||||
|
}
|
||||||
|
.code {
|
||||||
|
line-height: 36px;
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 30px;
|
||||||
|
}
|
||||||
|
.tips {
|
||||||
|
line-height: 16px;
|
||||||
|
color: #676f83;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<p class="title">Set your {{application_title}} password</p>
|
||||||
|
<p class="description">Copy and paste this code, this code will only be valid for the next 5 minutes.</p>
|
||||||
|
<div class="code-content">
|
||||||
|
<span class="code">{{code}}</span>
|
||||||
|
</div>
|
||||||
|
<p class="tips">If you didn't request, don't worry. You can safely ignore this email.</p>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@ -0,0 +1,70 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: 'Arial', sans-serif;
|
||||||
|
line-height: 16pt;
|
||||||
|
color: #101828;
|
||||||
|
background-color: #e9ebf0;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
.container {
|
||||||
|
width: 600px;
|
||||||
|
height: 360px;
|
||||||
|
margin: 40px auto;
|
||||||
|
padding: 36px 48px;
|
||||||
|
background-color: #fcfcfd;
|
||||||
|
border-radius: 16px;
|
||||||
|
border: 1px solid #ffffff;
|
||||||
|
box-shadow: 0 2px 4px -2px rgba(9, 9, 11, 0.08);
|
||||||
|
}
|
||||||
|
.header {
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
.header img {
|
||||||
|
max-width: 100px;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
.title {
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 24px;
|
||||||
|
line-height: 28.8px;
|
||||||
|
}
|
||||||
|
.description {
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 16px;
|
||||||
|
color: #676f83;
|
||||||
|
margin-top: 12px;
|
||||||
|
}
|
||||||
|
.code-content {
|
||||||
|
padding: 16px 32px;
|
||||||
|
text-align: center;
|
||||||
|
border-radius: 16px;
|
||||||
|
background-color: #f2f4f7;
|
||||||
|
margin: 16px auto;
|
||||||
|
}
|
||||||
|
.code {
|
||||||
|
line-height: 36px;
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 30px;
|
||||||
|
}
|
||||||
|
.tips {
|
||||||
|
line-height: 16px;
|
||||||
|
color: #676f83;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<p class="title">设置您的 {{application_title}} 账户密码</p>
|
||||||
|
<p class="description">复制并粘贴此验证码,注意验证码仅在接下来的 5 分钟内有效。</p>
|
||||||
|
<div class="code-content">
|
||||||
|
<span class="code">{{code}}</span>
|
||||||
|
</div>
|
||||||
|
<p class="tips">如果您没有请求,请不要担心。您可以安全地忽略此电子邮件。</p>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@ -2,7 +2,7 @@ x-shared-env: &shared-api-worker-env
|
|||||||
services:
|
services:
|
||||||
# API service
|
# API service
|
||||||
api:
|
api:
|
||||||
image: langgenius/dify-api:0.15.3
|
image: langgenius/dify-api:0.15.4
|
||||||
restart: always
|
restart: always
|
||||||
environment:
|
environment:
|
||||||
# Use the shared environment variables.
|
# Use the shared environment variables.
|
||||||
@ -25,7 +25,7 @@ services:
|
|||||||
# worker service
|
# worker service
|
||||||
# The Celery worker for processing the queue.
|
# The Celery worker for processing the queue.
|
||||||
worker:
|
worker:
|
||||||
image: langgenius/dify-api:0.15.3
|
image: langgenius/dify-api:0.15.4
|
||||||
restart: always
|
restart: always
|
||||||
environment:
|
environment:
|
||||||
# Use the shared environment variables.
|
# Use the shared environment variables.
|
||||||
@ -47,7 +47,7 @@ services:
|
|||||||
|
|
||||||
# Frontend web application.
|
# Frontend web application.
|
||||||
web:
|
web:
|
||||||
image: langgenius/dify-web:0.15.3
|
image: langgenius/dify-web:0.15.4
|
||||||
restart: always
|
restart: always
|
||||||
environment:
|
environment:
|
||||||
CONSOLE_API_URL: ${CONSOLE_API_URL:-}
|
CONSOLE_API_URL: ${CONSOLE_API_URL:-}
|
||||||
@ -98,7 +98,7 @@ services:
|
|||||||
|
|
||||||
# The DifySandbox
|
# The DifySandbox
|
||||||
sandbox:
|
sandbox:
|
||||||
image: langgenius/dify-sandbox:0.2.10
|
image: langgenius/dify-sandbox:0.2.11
|
||||||
restart: always
|
restart: always
|
||||||
environment:
|
environment:
|
||||||
# The DifySandbox configurations
|
# The DifySandbox configurations
|
||||||
|
|||||||
@ -43,7 +43,7 @@ services:
|
|||||||
|
|
||||||
# The DifySandbox
|
# The DifySandbox
|
||||||
sandbox:
|
sandbox:
|
||||||
image: langgenius/dify-sandbox:0.2.10
|
image: langgenius/dify-sandbox:0.2.11
|
||||||
restart: always
|
restart: always
|
||||||
environment:
|
environment:
|
||||||
# The DifySandbox configurations
|
# The DifySandbox configurations
|
||||||
|
|||||||
@ -393,7 +393,7 @@ x-shared-env: &shared-api-worker-env
|
|||||||
services:
|
services:
|
||||||
# API service
|
# API service
|
||||||
api:
|
api:
|
||||||
image: langgenius/dify-api:0.15.3
|
image: langgenius/dify-api:0.15.4
|
||||||
restart: always
|
restart: always
|
||||||
environment:
|
environment:
|
||||||
# Use the shared environment variables.
|
# Use the shared environment variables.
|
||||||
@ -416,7 +416,7 @@ services:
|
|||||||
# worker service
|
# worker service
|
||||||
# The Celery worker for processing the queue.
|
# The Celery worker for processing the queue.
|
||||||
worker:
|
worker:
|
||||||
image: langgenius/dify-api:0.15.3
|
image: langgenius/dify-api:0.15.4
|
||||||
restart: always
|
restart: always
|
||||||
environment:
|
environment:
|
||||||
# Use the shared environment variables.
|
# Use the shared environment variables.
|
||||||
@ -438,7 +438,7 @@ services:
|
|||||||
|
|
||||||
# Frontend web application.
|
# Frontend web application.
|
||||||
web:
|
web:
|
||||||
image: langgenius/dify-web:0.15.3
|
image: langgenius/dify-web:0.15.4
|
||||||
restart: always
|
restart: always
|
||||||
environment:
|
environment:
|
||||||
CONSOLE_API_URL: ${CONSOLE_API_URL:-}
|
CONSOLE_API_URL: ${CONSOLE_API_URL:-}
|
||||||
@ -489,7 +489,7 @@ services:
|
|||||||
|
|
||||||
# The DifySandbox
|
# The DifySandbox
|
||||||
sandbox:
|
sandbox:
|
||||||
image: langgenius/dify-sandbox:0.2.10
|
image: langgenius/dify-sandbox:0.2.11
|
||||||
restart: always
|
restart: always
|
||||||
environment:
|
environment:
|
||||||
# The DifySandbox configurations
|
# The DifySandbox configurations
|
||||||
|
|||||||
@ -15,17 +15,17 @@ import {
|
|||||||
} from '@remixicon/react'
|
} from '@remixicon/react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { useShallow } from 'zustand/react/shallow'
|
import { useShallow } from 'zustand/react/shallow'
|
||||||
import { useContextSelector } from 'use-context-selector'
|
|
||||||
import s from './style.module.css'
|
import s from './style.module.css'
|
||||||
import cn from '@/utils/classnames'
|
import cn from '@/utils/classnames'
|
||||||
import { useStore } from '@/app/components/app/store'
|
import { useStore } from '@/app/components/app/store'
|
||||||
import AppSideBar from '@/app/components/app-sidebar'
|
import AppSideBar from '@/app/components/app-sidebar'
|
||||||
import type { NavIcon } from '@/app/components/app-sidebar/navLink'
|
import type { NavIcon } from '@/app/components/app-sidebar/navLink'
|
||||||
import { fetchAppDetail, fetchAppSSO } from '@/service/apps'
|
import { fetchAppDetail, fetchAppSSO } from '@/service/apps'
|
||||||
import AppContext, { useAppContext } from '@/context/app-context'
|
import { useAppContext } from '@/context/app-context'
|
||||||
import Loading from '@/app/components/base/loading'
|
import Loading from '@/app/components/base/loading'
|
||||||
import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
|
import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
|
||||||
import type { App } from '@/types/app'
|
import type { App } from '@/types/app'
|
||||||
|
import { useGlobalPublicStore } from '@/context/global-public-context'
|
||||||
|
|
||||||
export type IAppDetailLayoutProps = {
|
export type IAppDetailLayoutProps = {
|
||||||
children: React.ReactNode
|
children: React.ReactNode
|
||||||
@ -56,7 +56,7 @@ const AppDetailLayout: FC<IAppDetailLayoutProps> = (props) => {
|
|||||||
icon: NavIcon
|
icon: NavIcon
|
||||||
selectedIcon: NavIcon
|
selectedIcon: NavIcon
|
||||||
}>>([])
|
}>>([])
|
||||||
const systemFeatures = useContextSelector(AppContext, state => state.systemFeatures)
|
const { systemFeatures } = useGlobalPublicStore()
|
||||||
|
|
||||||
const getNavigations = useCallback((appId: string, isCurrentWorkspaceEditor: boolean, mode: string) => {
|
const getNavigations = useCallback((appId: string, isCurrentWorkspaceEditor: boolean, mode: string) => {
|
||||||
const navs = [
|
const navs = [
|
||||||
@ -98,7 +98,11 @@ const AppDetailLayout: FC<IAppDetailLayoutProps> = (props) => {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (appDetail) {
|
if (appDetail) {
|
||||||
document.title = `${(appDetail.name || 'App')} - Dify`
|
if (systemFeatures.branding.enabled)
|
||||||
|
document.title = `${(appDetail.name || 'App')} - ${systemFeatures.branding.application_title}`
|
||||||
|
else
|
||||||
|
document.title = `${(appDetail.name || 'App')} - Dify`
|
||||||
|
|
||||||
const localeMode = localStorage.getItem('app-detail-collapse-or-expand') || 'expand'
|
const localeMode = localStorage.getItem('app-detail-collapse-or-expand') || 'expand'
|
||||||
const mode = isMobile ? 'collapse' : 'expand'
|
const mode = isMobile ? 'collapse' : 'expand'
|
||||||
setAppSiderbarExpand(isMobile ? mode : localeMode)
|
setAppSiderbarExpand(isMobile ? mode : localeMode)
|
||||||
@ -106,7 +110,7 @@ const AppDetailLayout: FC<IAppDetailLayoutProps> = (props) => {
|
|||||||
// if ((appDetail.mode === 'advanced-chat' || appDetail.mode === 'workflow') && (pathname).endsWith('workflow'))
|
// if ((appDetail.mode === 'advanced-chat' || appDetail.mode === 'workflow') && (pathname).endsWith('workflow'))
|
||||||
// setAppSiderbarExpand('collapse')
|
// setAppSiderbarExpand('collapse')
|
||||||
}
|
}
|
||||||
}, [appDetail, isMobile])
|
}, [appDetail, isMobile, pathname, setAppSiderbarExpand, systemFeatures])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setAppDetail()
|
setAppDetail()
|
||||||
@ -161,9 +165,9 @@ const AppDetailLayout: FC<IAppDetailLayoutProps> = (props) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cn(s.app, 'flex relative', 'overflow-hidden')}>
|
<div className={cn(s.app, 'flex', 'overflow-hidden')}>
|
||||||
{appDetail && (
|
{appDetail && (
|
||||||
<AppSideBar title={appDetail.name} icon={appDetail.icon} icon_background={appDetail.icon_background as string} desc={appDetail.mode} navigation={navigation} />
|
<AppSideBar title={appDetail.name} icon={appDetail.icon} icon_background={appDetail.icon_background} desc={appDetail.mode} navigation={navigation} />
|
||||||
)}
|
)}
|
||||||
<div className="bg-components-panel-bg grow overflow-hidden">
|
<div className="bg-components-panel-bg grow overflow-hidden">
|
||||||
{children}
|
{children}
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
import type { FC } from 'react'
|
import type { FC } from 'react'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { useContext, useContextSelector } from 'use-context-selector'
|
import { useContext } from 'use-context-selector'
|
||||||
import AppCard from '@/app/components/app/overview/appCard'
|
import AppCard from '@/app/components/app/overview/appCard'
|
||||||
import Loading from '@/app/components/base/loading'
|
import Loading from '@/app/components/base/loading'
|
||||||
import { ToastContext } from '@/app/components/base/toast'
|
import { ToastContext } from '@/app/components/base/toast'
|
||||||
@ -20,20 +20,18 @@ import { asyncRunSafe } from '@/utils'
|
|||||||
import { NEED_REFRESH_APP_LIST_KEY } from '@/config'
|
import { NEED_REFRESH_APP_LIST_KEY } from '@/config'
|
||||||
import type { IAppCardProps } from '@/app/components/app/overview/appCard'
|
import type { IAppCardProps } from '@/app/components/app/overview/appCard'
|
||||||
import { useStore as useAppStore } from '@/app/components/app/store'
|
import { useStore as useAppStore } from '@/app/components/app/store'
|
||||||
import AppContext from '@/context/app-context'
|
import { useGlobalPublicStore } from '@/context/global-public-context'
|
||||||
|
|
||||||
export type ICardViewProps = {
|
export type ICardViewProps = {
|
||||||
appId: string
|
appId: string
|
||||||
isInPanel?: boolean
|
|
||||||
className?: string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const CardView: FC<ICardViewProps> = ({ appId, isInPanel, className }) => {
|
const CardView: FC<ICardViewProps> = ({ appId }) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { notify } = useContext(ToastContext)
|
const { notify } = useContext(ToastContext)
|
||||||
const appDetail = useAppStore(state => state.appDetail)
|
const appDetail = useAppStore(state => state.appDetail)
|
||||||
const setAppDetail = useAppStore(state => state.setAppDetail)
|
const setAppDetail = useAppStore(state => state.setAppDetail)
|
||||||
const systemFeatures = useContextSelector(AppContext, state => state.systemFeatures)
|
const { systemFeatures } = useGlobalPublicStore()
|
||||||
|
|
||||||
const updateAppDetail = async () => {
|
const updateAppDetail = async () => {
|
||||||
try {
|
try {
|
||||||
@ -122,11 +120,10 @@ const CardView: FC<ICardViewProps> = ({ appId, isInPanel, className }) => {
|
|||||||
return <Loading />
|
return <Loading />
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={className || 'grid gap-6 grid-cols-1 xl:grid-cols-2 w-full mb-6'}>
|
<div className="grid gap-6 grid-cols-1 xl:grid-cols-2 w-full mb-6">
|
||||||
<AppCard
|
<AppCard
|
||||||
appInfo={appDetail}
|
appInfo={appDetail}
|
||||||
cardType="webapp"
|
cardType="webapp"
|
||||||
isInPanel={isInPanel}
|
|
||||||
onChangeStatus={onChangeSiteStatus}
|
onChangeStatus={onChangeSiteStatus}
|
||||||
onGenerateCode={onGenerateCode}
|
onGenerateCode={onGenerateCode}
|
||||||
onSaveSiteConfig={onSaveSiteConfig}
|
onSaveSiteConfig={onSaveSiteConfig}
|
||||||
@ -134,7 +131,6 @@ const CardView: FC<ICardViewProps> = ({ appId, isInPanel, className }) => {
|
|||||||
<AppCard
|
<AppCard
|
||||||
cardType="api"
|
cardType="api"
|
||||||
appInfo={appDetail}
|
appInfo={appDetail}
|
||||||
isInPanel={isInPanel}
|
|
||||||
onChangeStatus={onChangeApiStatus}
|
onChangeStatus={onChangeApiStatus}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -2,7 +2,9 @@
|
|||||||
import type { FC } from 'react'
|
import type { FC } from 'react'
|
||||||
import React, { useEffect } from 'react'
|
import React, { useEffect } from 'react'
|
||||||
import { useRouter } from 'next/navigation'
|
import { useRouter } from 'next/navigation'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
import { useAppContext } from '@/context/app-context'
|
import { useAppContext } from '@/context/app-context'
|
||||||
|
import useDocumentTitle from '@/hooks/use-document-title'
|
||||||
|
|
||||||
export type IAppDetail = {
|
export type IAppDetail = {
|
||||||
children: React.ReactNode
|
children: React.ReactNode
|
||||||
@ -11,11 +13,13 @@ export type IAppDetail = {
|
|||||||
const AppDetail: FC<IAppDetail> = ({ children }) => {
|
const AppDetail: FC<IAppDetail> = ({ children }) => {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const { isCurrentWorkspaceDatasetOperator } = useAppContext()
|
const { isCurrentWorkspaceDatasetOperator } = useAppContext()
|
||||||
|
const { t } = useTranslation()
|
||||||
|
useDocumentTitle(t('common.menus.appDetail'))
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isCurrentWorkspaceDatasetOperator)
|
if (isCurrentWorkspaceDatasetOperator)
|
||||||
return router.replace('/datasets')
|
return router.replace('/datasets')
|
||||||
}, [isCurrentWorkspaceDatasetOperator])
|
}, [isCurrentWorkspaceDatasetOperator, router])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@ -85,7 +85,6 @@ const Apps = () => {
|
|||||||
]
|
]
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
document.title = `${t('common.menus.apps')} - Dify`
|
|
||||||
if (localStorage.getItem(NEED_REFRESH_APP_LIST_KEY) === '1') {
|
if (localStorage.getItem(NEED_REFRESH_APP_LIST_KEY) === '1') {
|
||||||
localStorage.removeItem(NEED_REFRESH_APP_LIST_KEY)
|
localStorage.removeItem(NEED_REFRESH_APP_LIST_KEY)
|
||||||
mutate()
|
mutate()
|
||||||
|
|||||||
@ -1,21 +1,20 @@
|
|||||||
'use client'
|
'use client'
|
||||||
import { useContextSelector } from 'use-context-selector'
|
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { RiDiscordFill, RiGithubFill } from '@remixicon/react'
|
import { RiDiscordFill, RiGithubFill } from '@remixicon/react'
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
import style from '../list.module.css'
|
import style from '../list.module.css'
|
||||||
import Apps from './Apps'
|
import Apps from './Apps'
|
||||||
import AppContext from '@/context/app-context'
|
import { useGlobalPublicStore } from '@/context/global-public-context'
|
||||||
import { LicenseStatus } from '@/types/feature'
|
import useDocumentTitle from '@/hooks/use-document-title'
|
||||||
|
|
||||||
const AppList = () => {
|
const AppList = () => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const systemFeatures = useContextSelector(AppContext, v => v.systemFeatures)
|
const { systemFeatures } = useGlobalPublicStore()
|
||||||
|
useDocumentTitle(t('common.menus.apps'))
|
||||||
return (
|
return (
|
||||||
<div className='relative flex flex-col overflow-y-auto bg-background-body shrink-0 h-0 grow'>
|
<div className='relative flex flex-col overflow-y-auto bg-background-body shrink-0 h-0 grow'>
|
||||||
<Apps />
|
<Apps />
|
||||||
{systemFeatures.license.status === LicenseStatus.NONE && <footer className='px-12 py-6 grow-0 shrink-0'>
|
{!systemFeatures.branding.enabled && <footer className='px-12 py-6 grow-0 shrink-0'>
|
||||||
<h3 className='text-xl font-semibold leading-tight text-gradient'>{t('app.join')}</h3>
|
<h3 className='text-xl font-semibold leading-tight text-gradient'>{t('app.join')}</h3>
|
||||||
<p className='mt-1 system-sm-regular text-text-tertiary'>{t('app.communityIntro')}</p>
|
<p className='mt-1 system-sm-regular text-text-tertiary'>{t('app.communityIntro')}</p>
|
||||||
<div className='flex items-center gap-2 mt-3'>
|
<div className='flex items-center gap-2 mt-3'>
|
||||||
|
|||||||
@ -15,7 +15,7 @@ import {
|
|||||||
// CommandLineIcon as CommandLineSolidIcon,
|
// CommandLineIcon as CommandLineSolidIcon,
|
||||||
DocumentTextIcon as DocumentTextSolidIcon,
|
DocumentTextIcon as DocumentTextSolidIcon,
|
||||||
} from '@heroicons/react/24/solid'
|
} from '@heroicons/react/24/solid'
|
||||||
import { RiApps2AddLine, RiBookOpenLine, RiInformation2Line } from '@remixicon/react'
|
import { RiApps2AddLine, RiInformation2Line } from '@remixicon/react'
|
||||||
import s from './style.module.css'
|
import s from './style.module.css'
|
||||||
import classNames from '@/utils/classnames'
|
import classNames from '@/utils/classnames'
|
||||||
import { fetchDatasetDetail, fetchDatasetRelatedApps } from '@/service/datasets'
|
import { fetchDatasetDetail, fetchDatasetRelatedApps } from '@/service/datasets'
|
||||||
@ -31,6 +31,7 @@ import { getLocaleOnClient } from '@/i18n'
|
|||||||
import { useAppContext } from '@/context/app-context'
|
import { useAppContext } from '@/context/app-context'
|
||||||
import Tooltip from '@/app/components/base/tooltip'
|
import Tooltip from '@/app/components/base/tooltip'
|
||||||
import LinkedAppsPanel from '@/app/components/base/linked-apps-panel'
|
import LinkedAppsPanel from '@/app/components/base/linked-apps-panel'
|
||||||
|
import useDocumentTitle from '@/hooks/use-document-title'
|
||||||
|
|
||||||
export type IAppDetailLayoutProps = {
|
export type IAppDetailLayoutProps = {
|
||||||
children: React.ReactNode
|
children: React.ReactNode
|
||||||
@ -58,6 +59,13 @@ const TargetSolidIcon = ({ className }: SVGProps<SVGElement>) => {
|
|||||||
</svg>
|
</svg>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const BookOpenIcon = ({ className }: SVGProps<SVGElement>) => {
|
||||||
|
return <svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg" className={className ?? ''}>
|
||||||
|
<path opacity="0.12" d="M1 3.1C1 2.53995 1 2.25992 1.10899 2.04601C1.20487 1.85785 1.35785 1.70487 1.54601 1.60899C1.75992 1.5 2.03995 1.5 2.6 1.5H2.8C3.9201 1.5 4.48016 1.5 4.90798 1.71799C5.28431 1.90973 5.59027 2.21569 5.78201 2.59202C6 3.01984 6 3.5799 6 4.7V10.5L5.94997 10.425C5.60265 9.90398 5.42899 9.64349 5.19955 9.45491C4.99643 9.28796 4.76238 9.1627 4.5108 9.0863C4.22663 9 3.91355 9 3.28741 9H2.6C2.03995 9 1.75992 9 1.54601 8.89101C1.35785 8.79513 1.20487 8.64215 1.10899 8.45399C1 8.24008 1 7.96005 1 7.4V3.1Z" fill="#155EEF" />
|
||||||
|
<path d="M6 10.5L5.94997 10.425C5.60265 9.90398 5.42899 9.64349 5.19955 9.45491C4.99643 9.28796 4.76238 9.1627 4.5108 9.0863C4.22663 9 3.91355 9 3.28741 9H2.6C2.03995 9 1.75992 9 1.54601 8.89101C1.35785 8.79513 1.20487 8.64215 1.10899 8.45399C1 8.24008 1 7.96005 1 7.4V3.1C1 2.53995 1 2.25992 1.10899 2.04601C1.20487 1.85785 1.35785 1.70487 1.54601 1.60899C1.75992 1.5 2.03995 1.5 2.6 1.5H2.8C3.9201 1.5 4.48016 1.5 4.90798 1.71799C5.28431 1.90973 5.59027 2.21569 5.78201 2.59202C6 3.01984 6 3.5799 6 4.7M6 10.5V4.7M6 10.5L6.05003 10.425C6.39735 9.90398 6.57101 9.64349 6.80045 9.45491C7.00357 9.28796 7.23762 9.1627 7.4892 9.0863C7.77337 9 8.08645 9 8.71259 9H9.4C9.96005 9 10.2401 9 10.454 8.89101C10.6422 8.79513 10.7951 8.64215 10.891 8.45399C11 8.24008 11 7.96005 11 7.4V3.1C11 2.53995 11 2.25992 10.891 2.04601C10.7951 1.85785 10.6422 1.70487 10.454 1.60899C10.2401 1.5 9.96005 1.5 9.4 1.5H9.2C8.07989 1.5 7.51984 1.5 7.09202 1.71799C6.71569 1.90973 6.40973 2.21569 6.21799 2.59202C6 3.01984 6 3.5799 6 4.7" stroke="#155EEF" strokeLinecap="round" strokeLinejoin="round" />
|
||||||
|
</svg>
|
||||||
|
}
|
||||||
|
|
||||||
type IExtraInfoProps = {
|
type IExtraInfoProps = {
|
||||||
isMobile: boolean
|
isMobile: boolean
|
||||||
relatedApps?: RelatedAppResponse
|
relatedApps?: RelatedAppResponse
|
||||||
@ -124,7 +132,7 @@ const ExtraInfo = ({ isMobile, relatedApps, expand }: IExtraInfoProps) => {
|
|||||||
}
|
}
|
||||||
target='_blank' rel='noopener noreferrer'
|
target='_blank' rel='noopener noreferrer'
|
||||||
>
|
>
|
||||||
<RiBookOpenLine className='mr-1 text-text-accent' />
|
<BookOpenIcon className='mr-1' />
|
||||||
{t('common.datasetMenus.viewDoc')}
|
{t('common.datasetMenus.viewDoc')}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
@ -179,11 +187,7 @@ const DatasetDetailLayout: FC<IAppDetailLayoutProps> = (props) => {
|
|||||||
}
|
}
|
||||||
return baseNavigation
|
return baseNavigation
|
||||||
}, [datasetRes?.provider, datasetId, t])
|
}, [datasetRes?.provider, datasetId, t])
|
||||||
|
useDocumentTitle(`${datasetRes?.name || 'Dataset'}`)
|
||||||
useEffect(() => {
|
|
||||||
if (datasetRes)
|
|
||||||
document.title = `${datasetRes.name || 'Dataset'} - Dify`
|
|
||||||
}, [datasetRes])
|
|
||||||
|
|
||||||
const setAppSiderbarExpand = useStore(state => state.setAppSiderbarExpand)
|
const setAppSiderbarExpand = useStore(state => state.setAppSiderbarExpand)
|
||||||
|
|
||||||
|
|||||||
@ -31,6 +31,8 @@ const ApiServer: FC<ApiServerProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
<SecretKeyButton
|
<SecretKeyButton
|
||||||
className='flex-shrink-0 !h-8 bg-white'
|
className='flex-shrink-0 !h-8 bg-white'
|
||||||
|
textCls='!text-gray-700 font-medium'
|
||||||
|
iconCls='stroke-[1.2px]'
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -29,9 +29,11 @@ import { useTabSearchParams } from '@/hooks/use-tab-searchparams'
|
|||||||
import { useStore as useTagStore } from '@/app/components/base/tag-management/store'
|
import { useStore as useTagStore } from '@/app/components/base/tag-management/store'
|
||||||
import { useAppContext } from '@/context/app-context'
|
import { useAppContext } from '@/context/app-context'
|
||||||
import { useExternalApiPanel } from '@/context/external-api-panel-context'
|
import { useExternalApiPanel } from '@/context/external-api-panel-context'
|
||||||
|
import { useGlobalPublicStore } from '@/context/global-public-context'
|
||||||
|
|
||||||
const Container = () => {
|
const Container = () => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
const { systemFeatures } = useGlobalPublicStore()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const { currentWorkspace, isCurrentWorkspaceOwner } = useAppContext()
|
const { currentWorkspace, isCurrentWorkspaceOwner } = useAppContext()
|
||||||
const showTagManagementModal = useTagStore(s => s.showTagManagementModal)
|
const showTagManagementModal = useTagStore(s => s.showTagManagementModal)
|
||||||
@ -123,7 +125,7 @@ const Container = () => {
|
|||||||
{activeTab === 'dataset' && (
|
{activeTab === 'dataset' && (
|
||||||
<>
|
<>
|
||||||
<Datasets containerRef={containerRef} tags={tagIDs} keywords={searchKeywords} includeAll={includeAll} />
|
<Datasets containerRef={containerRef} tags={tagIDs} keywords={searchKeywords} includeAll={includeAll} />
|
||||||
<DatasetFooter />
|
{!systemFeatures.branding.enabled && <DatasetFooter />}
|
||||||
{showTagManagementModal && (
|
{showTagManagementModal && (
|
||||||
<TagManagementModal type='knowledge' show={showTagManagementModal} />
|
<TagManagementModal type='knowledge' show={showTagManagementModal} />
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -192,7 +192,7 @@ const DatasetCard = ({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className='!hidden group-hover:!flex shrink-0 mx-1 w-[1px] h-[14px] bg-divider-regular' />
|
<div className='!hidden group-hover:!flex shrink-0 mx-1 w-[1px] h-[14px] bg-gray-200' />
|
||||||
<div className='!hidden group-hover:!flex shrink-0'>
|
<div className='!hidden group-hover:!flex shrink-0'>
|
||||||
<CustomPopover
|
<CustomPopover
|
||||||
htmlContent={<Operations showDelete={!isCurrentWorkspaceDatasetOperator} />}
|
htmlContent={<Operations showDelete={!isCurrentWorkspaceDatasetOperator} />}
|
||||||
|
|||||||
@ -3,7 +3,6 @@
|
|||||||
import { useEffect, useRef } from 'react'
|
import { useEffect, useRef } from 'react'
|
||||||
import useSWRInfinite from 'swr/infinite'
|
import useSWRInfinite from 'swr/infinite'
|
||||||
import { debounce } from 'lodash-es'
|
import { debounce } from 'lodash-es'
|
||||||
import { useTranslation } from 'react-i18next'
|
|
||||||
import NewDatasetCard from './NewDatasetCard'
|
import NewDatasetCard from './NewDatasetCard'
|
||||||
import DatasetCard from './DatasetCard'
|
import DatasetCard from './DatasetCard'
|
||||||
import type { DataSetListResponse, FetchDatasetsParams } from '@/models/datasets'
|
import type { DataSetListResponse, FetchDatasetsParams } from '@/models/datasets'
|
||||||
@ -57,11 +56,8 @@ const Datasets = ({
|
|||||||
const loadingStateRef = useRef(false)
|
const loadingStateRef = useRef(false)
|
||||||
const anchorRef = useRef<HTMLAnchorElement>(null)
|
const anchorRef = useRef<HTMLAnchorElement>(null)
|
||||||
|
|
||||||
const { t } = useTranslation()
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadingStateRef.current = isLoading
|
loadingStateRef.current = isLoading
|
||||||
document.title = `${t('dataset.knowledge')} - Dify`
|
|
||||||
}, [isLoading])
|
}, [isLoading])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -80,7 +76,7 @@ const Datasets = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<nav className='grid content-start grid-cols-1 gap-4 px-12 pt-2 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 grow shrink-0'>
|
<nav className='grid content-start grid-cols-1 gap-4 px-12 pt-2 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 grow shrink-0'>
|
||||||
{ isCurrentWorkspaceEditor && <NewDatasetCard ref={anchorRef} /> }
|
{isCurrentWorkspaceEditor && <NewDatasetCard ref={anchorRef} />}
|
||||||
{data?.map(({ data: datasets }) => datasets.map(dataset => (
|
{data?.map(({ data: datasets }) => datasets.map(dataset => (
|
||||||
<DatasetCard key={dataset.id} dataset={dataset} onSuccess={mutate} />),
|
<DatasetCard key={dataset.id} dataset={dataset} onSuccess={mutate} />),
|
||||||
))}
|
))}
|
||||||
|
|||||||
@ -1,11 +1,12 @@
|
|||||||
|
'use client'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
import Container from './Container'
|
import Container from './Container'
|
||||||
|
import useDocumentTitle from '@/hooks/use-document-title'
|
||||||
|
|
||||||
const AppList = async () => {
|
const AppList = () => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
useDocumentTitle(t('common.menus.datasets'))
|
||||||
return <Container />
|
return <Container />
|
||||||
}
|
}
|
||||||
|
|
||||||
export const metadata = {
|
|
||||||
title: 'Datasets - Dify',
|
|
||||||
}
|
|
||||||
|
|
||||||
export default AppList
|
export default AppList
|
||||||
|
|||||||
@ -6,7 +6,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty, PropertyInstructi
|
|||||||
<div>
|
<div>
|
||||||
### Authentication
|
### Authentication
|
||||||
|
|
||||||
Service API of Dify authenticates using an `API-Key`.
|
Service API authenticates using an `API-Key`.
|
||||||
|
|
||||||
It is suggested that developers store the `API-Key` in the backend instead of sharing or storing it in the client side to avoid the leakage of the `API-Key`, which may lead to property loss.
|
It is suggested that developers store the `API-Key` in the backend instead of sharing or storing it in the client side to avoid the leakage of the `API-Key`, which may lead to property loss.
|
||||||
|
|
||||||
|
|||||||
@ -6,7 +6,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty, PropertyInstructi
|
|||||||
<div>
|
<div>
|
||||||
### 鉴权
|
### 鉴权
|
||||||
|
|
||||||
Dify Service API 使用 `API-Key` 进行鉴权。
|
Service API 使用 `API-Key` 进行鉴权。
|
||||||
|
|
||||||
建议开发者把 `API-Key` 放在后端存储,而非分享或者放在客户端存储,以免 `API-Key` 泄露,导致财产损失。
|
建议开发者把 `API-Key` 放在后端存储,而非分享或者放在客户端存储,以免 `API-Key` 泄露,导致财产损失。
|
||||||
|
|
||||||
|
|||||||
@ -1,11 +1,13 @@
|
|||||||
import type { FC } from 'react'
|
'use client'
|
||||||
|
import type { FC, PropsWithChildren } from 'react'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
import ExploreClient from '@/app/components/explore'
|
import ExploreClient from '@/app/components/explore'
|
||||||
export type IAppDetail = {
|
import useDocumentTitle from '@/hooks/use-document-title'
|
||||||
children: React.ReactNode
|
|
||||||
}
|
|
||||||
|
|
||||||
const AppDetail: FC<IAppDetail> = ({ children }) => {
|
const ExploreLayout: FC<PropsWithChildren> = ({ children }) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
useDocumentTitle(t('common.menus.explore'))
|
||||||
return (
|
return (
|
||||||
<ExploreClient>
|
<ExploreClient>
|
||||||
{children}
|
{children}
|
||||||
@ -13,4 +15,4 @@ const AppDetail: FC<IAppDetail> = ({ children }) => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default React.memo(AppDetail)
|
export default React.memo(ExploreLayout)
|
||||||
|
|||||||
@ -30,9 +30,4 @@ const Layout = ({ children }: { children: ReactNode }) => {
|
|||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const metadata = {
|
|
||||||
title: 'Dify',
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Layout
|
export default Layout
|
||||||
|
|||||||
@ -1,22 +1,16 @@
|
|||||||
'use client'
|
'use client'
|
||||||
import type { FC } from 'react'
|
import type { FC } from 'react'
|
||||||
import { useRouter } from 'next/navigation'
|
import { useRouter } from 'next/navigation'
|
||||||
import { useTranslation } from 'react-i18next'
|
|
||||||
import React, { useEffect } from 'react'
|
import React, { useEffect } from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
import ToolProviderList from '@/app/components/tools/provider-list'
|
import ToolProviderList from '@/app/components/tools/provider-list'
|
||||||
import { useAppContext } from '@/context/app-context'
|
import { useAppContext } from '@/context/app-context'
|
||||||
|
import useDocumentTitle from '@/hooks/use-document-title'
|
||||||
const Layout: FC = () => {
|
const ToolsList: FC = () => {
|
||||||
const { t } = useTranslation()
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const { isCurrentWorkspaceDatasetOperator } = useAppContext()
|
const { isCurrentWorkspaceDatasetOperator } = useAppContext()
|
||||||
|
const { t } = useTranslation()
|
||||||
useEffect(() => {
|
useDocumentTitle(t('common.menus.tools'))
|
||||||
if (typeof window !== 'undefined')
|
|
||||||
document.title = `${t('tools.title')} - Dify`
|
|
||||||
if (isCurrentWorkspaceDatasetOperator)
|
|
||||||
return router.replace('/datasets')
|
|
||||||
}, [isCurrentWorkspaceDatasetOperator, router, t])
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isCurrentWorkspaceDatasetOperator)
|
if (isCurrentWorkspaceDatasetOperator)
|
||||||
@ -25,4 +19,4 @@ const Layout: FC = () => {
|
|||||||
|
|
||||||
return <ToolProviderList />
|
return <ToolProviderList />
|
||||||
}
|
}
|
||||||
export default React.memo(Layout)
|
export default React.memo(ToolsList)
|
||||||
|
|||||||
@ -16,6 +16,7 @@ import { ToastContext } from '@/app/components/base/toast'
|
|||||||
import AppIcon from '@/app/components/base/app-icon'
|
import AppIcon from '@/app/components/base/app-icon'
|
||||||
import { IS_CE_EDITION } from '@/config'
|
import { IS_CE_EDITION } from '@/config'
|
||||||
import Input from '@/app/components/base/input'
|
import Input from '@/app/components/base/input'
|
||||||
|
import { useGlobalPublicStore } from '@/context/global-public-context'
|
||||||
|
|
||||||
const titleClassName = `
|
const titleClassName = `
|
||||||
system-sm-semibold text-text-secondary
|
system-sm-semibold text-text-secondary
|
||||||
@ -28,7 +29,7 @@ const validPassword = /^(?=.*[a-zA-Z])(?=.*\d).{8,}$/
|
|||||||
|
|
||||||
export default function AccountPage() {
|
export default function AccountPage() {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { systemFeatures } = useAppContext()
|
const { systemFeatures } = useGlobalPublicStore()
|
||||||
const { mutateUserProfile, userProfile, apps } = useAppContext()
|
const { mutateUserProfile, userProfile, apps } = useAppContext()
|
||||||
const { notify } = useContext(ToastContext)
|
const { notify } = useContext(ToastContext)
|
||||||
const [editNameModalVisible, setEditNameModalVisible] = useState(false)
|
const [editNameModalVisible, setEditNameModalVisible] = useState(false)
|
||||||
@ -133,7 +134,7 @@ export default function AccountPage() {
|
|||||||
<h4 className='title-2xl-semi-bold text-text-primary'>{t('common.account.myAccount')}</h4>
|
<h4 className='title-2xl-semi-bold text-text-primary'>{t('common.account.myAccount')}</h4>
|
||||||
</div>
|
</div>
|
||||||
<div className='mb-8 p-6 rounded-xl flex items-center bg-gradient-to-r from-background-gradient-bg-fill-chat-bg-2 to-background-gradient-bg-fill-chat-bg-1'>
|
<div className='mb-8 p-6 rounded-xl flex items-center bg-gradient-to-r from-background-gradient-bg-fill-chat-bg-2 to-background-gradient-bg-fill-chat-bg-1'>
|
||||||
<AvatarWithEdit avatar={userProfile.avatar_url} name={userProfile.name} onSave={ mutateUserProfile } size={64} />
|
<AvatarWithEdit avatar={userProfile.avatar_url} name={userProfile.name} onSave={mutateUserProfile} size={64} />
|
||||||
<div className='ml-4'>
|
<div className='ml-4'>
|
||||||
<p className='system-xl-semibold text-text-primary'>{userProfile.name}</p>
|
<p className='system-xl-semibold text-text-primary'>{userProfile.name}</p>
|
||||||
<p className='system-xs-regular text-text-tertiary'>{userProfile.email}</p>
|
<p className='system-xs-regular text-text-tertiary'>{userProfile.email}</p>
|
||||||
|
|||||||
@ -5,9 +5,11 @@ import { useRouter } from 'next/navigation'
|
|||||||
import Button from '../components/base/button'
|
import Button from '../components/base/button'
|
||||||
import Avatar from './avatar'
|
import Avatar from './avatar'
|
||||||
import LogoSite from '@/app/components/base/logo/logo-site'
|
import LogoSite from '@/app/components/base/logo/logo-site'
|
||||||
|
import { useGlobalPublicStore } from '@/context/global-public-context'
|
||||||
|
|
||||||
const Header = () => {
|
const Header = () => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
const { systemFeatures } = useGlobalPublicStore()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
const back = () => {
|
const back = () => {
|
||||||
@ -25,7 +27,7 @@ const Header = () => {
|
|||||||
<div className='flex items-center flex-shrink-0 gap-3'>
|
<div className='flex items-center flex-shrink-0 gap-3'>
|
||||||
<Button className='gap-2 py-2 px-3 system-sm-medium' onClick={back}>
|
<Button className='gap-2 py-2 px-3 system-sm-medium' onClick={back}>
|
||||||
<RiRobot2Line className='w-4 h-4' />
|
<RiRobot2Line className='w-4 h-4' />
|
||||||
<p>{t('common.account.studio')}</p>
|
<p>{!systemFeatures.branding.enabled && 'Dify '}{t('common.account.studio')}</p>
|
||||||
<RiArrowRightUpLine className='w-4 h-4' />
|
<RiArrowRightUpLine className='w-4 h-4' />
|
||||||
</Button>
|
</Button>
|
||||||
<div className='w-[1px] h-4 bg-divider-regular' />
|
<div className='w-[1px] h-4 bg-divider-regular' />
|
||||||
|
|||||||
@ -32,9 +32,4 @@ const Layout = ({ children }: { children: ReactNode }) => {
|
|||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const metadata = {
|
|
||||||
title: 'Dify',
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Layout
|
export default Layout
|
||||||
|
|||||||
@ -1,6 +1,11 @@
|
|||||||
|
'use client'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
import AccountPage from './account-page'
|
import AccountPage from './account-page'
|
||||||
|
import useDocumentTitle from '@/hooks/use-document-title'
|
||||||
|
|
||||||
export default function Account() {
|
export default function Account() {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
useDocumentTitle(t('common.menus.account'))
|
||||||
return <div className='max-w-[640px] w-full mx-auto pt-12 px-6'>
|
return <div className='max-w-[640px] w-full mx-auto pt-12 px-6'>
|
||||||
<AccountPage />
|
<AccountPage />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -7,8 +7,10 @@ import Button from '@/app/components/base/button'
|
|||||||
|
|
||||||
import { invitationCheck } from '@/service/common'
|
import { invitationCheck } from '@/service/common'
|
||||||
import Loading from '@/app/components/base/loading'
|
import Loading from '@/app/components/base/loading'
|
||||||
|
import useDocumentTitle from '@/hooks/use-document-title'
|
||||||
|
|
||||||
const ActivateForm = () => {
|
const ActivateForm = () => {
|
||||||
|
useDocumentTitle('')
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const searchParams = useSearchParams()
|
const searchParams = useSearchParams()
|
||||||
|
|||||||
@ -1,10 +1,13 @@
|
|||||||
|
'use client'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import Header from '../signin/_header'
|
import Header from '../signin/_header'
|
||||||
import style from '../signin/page.module.css'
|
import style from '../signin/page.module.css'
|
||||||
import ActivateForm from './activateForm'
|
import ActivateForm from './activateForm'
|
||||||
import cn from '@/utils/classnames'
|
import cn from '@/utils/classnames'
|
||||||
|
import { useGlobalPublicStore } from '@/context/global-public-context'
|
||||||
|
|
||||||
const Activate = () => {
|
const Activate = () => {
|
||||||
|
const { systemFeatures } = useGlobalPublicStore()
|
||||||
return (
|
return (
|
||||||
<div className={cn(
|
<div className={cn(
|
||||||
style.background,
|
style.background,
|
||||||
@ -21,9 +24,9 @@ const Activate = () => {
|
|||||||
}>
|
}>
|
||||||
<Header />
|
<Header />
|
||||||
<ActivateForm />
|
<ActivateForm />
|
||||||
<div className='px-8 py-6 text-sm font-normal text-gray-500'>
|
{!systemFeatures.branding.enabled && <div className='px-8 py-6 text-sm font-normal text-gray-500'>
|
||||||
© {new Date().getFullYear()} LangGenius, Inc. All rights reserved.
|
© {new Date().getFullYear()} LangGenius, Inc. All rights reserved.
|
||||||
</div>
|
</div>}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -1,18 +1,18 @@
|
|||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { useRouter } from 'next/navigation'
|
import { useRouter } from 'next/navigation'
|
||||||
import { useContext, useContextSelector } from 'use-context-selector'
|
import { useContext, useContextSelector } from 'use-context-selector'
|
||||||
|
import { RiArrowDownSLine } from '@remixicon/react'
|
||||||
import React, { useCallback, useState } from 'react'
|
import React, { useCallback, useState } from 'react'
|
||||||
import {
|
|
||||||
RiDeleteBinLine,
|
|
||||||
RiEditLine,
|
|
||||||
RiEqualizer2Line,
|
|
||||||
RiFileCopy2Line,
|
|
||||||
RiFileDownloadLine,
|
|
||||||
RiFileUploadLine,
|
|
||||||
} from '@remixicon/react'
|
|
||||||
import AppIcon from '../base/app-icon'
|
import AppIcon from '../base/app-icon'
|
||||||
import SwitchAppModal from '../app/switch-app-modal'
|
import SwitchAppModal from '../app/switch-app-modal'
|
||||||
|
import s from './style.module.css'
|
||||||
import cn from '@/utils/classnames'
|
import cn from '@/utils/classnames'
|
||||||
|
import {
|
||||||
|
PortalToFollowElem,
|
||||||
|
PortalToFollowElemContent,
|
||||||
|
PortalToFollowElemTrigger,
|
||||||
|
} from '@/app/components/base/portal-to-follow-elem'
|
||||||
|
import Divider from '@/app/components/base/divider'
|
||||||
import Confirm from '@/app/components/base/confirm'
|
import Confirm from '@/app/components/base/confirm'
|
||||||
import { useStore as useAppStore } from '@/app/components/app/store'
|
import { useStore as useAppStore } from '@/app/components/app/store'
|
||||||
import { ToastContext } from '@/app/components/base/toast'
|
import { ToastContext } from '@/app/components/base/toast'
|
||||||
@ -22,6 +22,8 @@ import { copyApp, deleteApp, exportAppConfig, updateAppInfo } from '@/service/ap
|
|||||||
import DuplicateAppModal from '@/app/components/app/duplicate-modal'
|
import DuplicateAppModal from '@/app/components/app/duplicate-modal'
|
||||||
import type { DuplicateAppModalProps } from '@/app/components/app/duplicate-modal'
|
import type { DuplicateAppModalProps } from '@/app/components/app/duplicate-modal'
|
||||||
import CreateAppModal from '@/app/components/explore/create-app-modal'
|
import CreateAppModal from '@/app/components/explore/create-app-modal'
|
||||||
|
import { AiText, ChatBot, CuteRobot } from '@/app/components/base/icons/src/vender/solid/communication'
|
||||||
|
import { Route } from '@/app/components/base/icons/src/vender/solid/mapsAndTravel'
|
||||||
import type { CreateAppModalProps } from '@/app/components/explore/create-app-modal'
|
import type { CreateAppModalProps } from '@/app/components/explore/create-app-modal'
|
||||||
import { NEED_REFRESH_APP_LIST_KEY } from '@/config'
|
import { NEED_REFRESH_APP_LIST_KEY } from '@/config'
|
||||||
import { getRedirection } from '@/utils/app-redirection'
|
import { getRedirection } from '@/utils/app-redirection'
|
||||||
@ -29,9 +31,6 @@ import UpdateDSLModal from '@/app/components/workflow/update-dsl-modal'
|
|||||||
import type { EnvironmentVariable } from '@/app/components/workflow/types'
|
import type { EnvironmentVariable } from '@/app/components/workflow/types'
|
||||||
import DSLExportConfirmModal from '@/app/components/workflow/dsl-export-confirm-modal'
|
import DSLExportConfirmModal from '@/app/components/workflow/dsl-export-confirm-modal'
|
||||||
import { fetchWorkflowDraft } from '@/service/workflow'
|
import { fetchWorkflowDraft } from '@/service/workflow'
|
||||||
import ContentDialog from '@/app/components/base/content-dialog'
|
|
||||||
import Button from '@/app/components/base/button'
|
|
||||||
import CardView from '@/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/cardView'
|
|
||||||
|
|
||||||
export type IAppInfoProps = {
|
export type IAppInfoProps = {
|
||||||
expand: boolean
|
expand: boolean
|
||||||
@ -48,6 +47,7 @@ const AppInfo = ({ expand }: IAppInfoProps) => {
|
|||||||
const [showEditModal, setShowEditModal] = useState(false)
|
const [showEditModal, setShowEditModal] = useState(false)
|
||||||
const [showDuplicateModal, setShowDuplicateModal] = useState(false)
|
const [showDuplicateModal, setShowDuplicateModal] = useState(false)
|
||||||
const [showConfirmDelete, setShowConfirmDelete] = useState(false)
|
const [showConfirmDelete, setShowConfirmDelete] = useState(false)
|
||||||
|
const [showSwitchTip, setShowSwitchTip] = useState<string>('')
|
||||||
const [showSwitchModal, setShowSwitchModal] = useState<boolean>(false)
|
const [showSwitchModal, setShowSwitchModal] = useState<boolean>(false)
|
||||||
const [showImportDSLModal, setShowImportDSLModal] = useState<boolean>(false)
|
const [showImportDSLModal, setShowImportDSLModal] = useState<boolean>(false)
|
||||||
const [secretEnvList, setSecretEnvList] = useState<EnvironmentVariable[]>([])
|
const [secretEnvList, setSecretEnvList] = useState<EnvironmentVariable[]>([])
|
||||||
@ -183,199 +183,291 @@ const AppInfo = ({ expand }: IAppInfoProps) => {
|
|||||||
return null
|
return null
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<PortalToFollowElem
|
||||||
<button
|
open={open}
|
||||||
onClick={() => {
|
onOpenChange={setOpen}
|
||||||
if (isCurrentWorkspaceEditor)
|
placement='bottom-start'
|
||||||
setOpen(v => !v)
|
offset={4}
|
||||||
}}
|
>
|
||||||
className='block w-full'
|
<div className='relative'>
|
||||||
>
|
<PortalToFollowElemTrigger
|
||||||
<div className={cn('flex rounded-lg', expand ? 'p-2 pb-2.5 flex-col gap-2' : 'p-1 gap-1 justify-center items-start', open && 'bg-state-base-hover', isCurrentWorkspaceEditor && 'hover:bg-state-base-hover cursor-pointer')}>
|
onClick={() => {
|
||||||
<div className={`flex items-center self-stretch ${expand ? 'justify-between' : 'flex-col gap-1'}`}>
|
if (isCurrentWorkspaceEditor)
|
||||||
<AppIcon
|
setOpen(v => !v)
|
||||||
size={expand ? 'large' : 'small'}
|
}}
|
||||||
iconType={appDetail.icon_type}
|
className='block'
|
||||||
icon={appDetail.icon}
|
>
|
||||||
background={appDetail.icon_background}
|
<div className={cn('flex p-1 rounded-lg', open && 'bg-gray-100', isCurrentWorkspaceEditor && 'hover:bg-gray-100 cursor-pointer')}>
|
||||||
imageUrl={appDetail.icon_url}
|
<div className='relative shrink-0 mr-2'>
|
||||||
/>
|
<AppIcon
|
||||||
<div className='flex p-0.5 justify-center items-center rounded-md'>
|
size={expand ? 'large' : 'small'}
|
||||||
<div className='flex w-5 h-5 justify-center items-center'>
|
iconType={appDetail.icon_type}
|
||||||
<RiEqualizer2Line className='w-4 h-4 text-text-tertiary' />
|
icon={appDetail.icon}
|
||||||
</div>
|
background={appDetail.icon_background}
|
||||||
|
imageUrl={appDetail.icon_url}
|
||||||
|
/>
|
||||||
|
<span className={cn(
|
||||||
|
'absolute bottom-[-3px] right-[-3px] w-4 h-4 p-0.5 bg-white rounded border-[0.5px] border-[rgba(0,0,0,0.02)] shadow-sm',
|
||||||
|
!expand && '!w-3.5 !h-3.5 !bottom-[-2px] !right-[-2px]',
|
||||||
|
)}>
|
||||||
|
{appDetail.mode === 'advanced-chat' && (
|
||||||
|
<ChatBot className={cn('w-3 h-3 text-[#1570EF]', !expand && '!w-2.5 !h-2.5')} />
|
||||||
|
)}
|
||||||
|
{appDetail.mode === 'agent-chat' && (
|
||||||
|
<CuteRobot className={cn('w-3 h-3 text-indigo-600', !expand && '!w-2.5 !h-2.5')} />
|
||||||
|
)}
|
||||||
|
{appDetail.mode === 'chat' && (
|
||||||
|
<ChatBot className={cn('w-3 h-3 text-[#1570EF]', !expand && '!w-2.5 !h-2.5')} />
|
||||||
|
)}
|
||||||
|
{appDetail.mode === 'completion' && (
|
||||||
|
<AiText className={cn('w-3 h-3 text-[#0E9384]', !expand && '!w-2.5 !h-2.5')} />
|
||||||
|
)}
|
||||||
|
{appDetail.mode === 'workflow' && (
|
||||||
|
<Route className={cn('w-3 h-3 text-[#f79009]', !expand && '!w-2.5 !h-2.5')} />
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
{expand && (
|
||||||
{
|
<div className="grow w-0">
|
||||||
expand && (
|
<div className='flex justify-between items-center text-sm leading-5 font-medium text-text-secondary'>
|
||||||
<div className='flex flex-col items-start gap-1'>
|
<div className='truncate' title={appDetail.name}>{appDetail.name}</div>
|
||||||
<div className='flex w-full'>
|
{isCurrentWorkspaceEditor && <RiArrowDownSLine className='shrink-0 ml-[2px] w-3 h-3 text-gray-500' />}
|
||||||
<div className='text-text-secondary system-md-semibold truncate'>{appDetail.name}</div>
|
</div>
|
||||||
|
<div className='flex items-center text-[10px] leading-[18px] font-medium text-gray-500 gap-1'>
|
||||||
|
{appDetail.mode === 'advanced-chat' && (
|
||||||
|
<>
|
||||||
|
<div className='shrink-0 px-1 border bg-white border-[rgba(0,0,0,0.08)] rounded-[5px] truncate'>{t('app.types.chatbot').toUpperCase()}</div>
|
||||||
|
<div title={t('app.types.advanced') || ''} className='px-1 border bg-white border-[rgba(0,0,0,0.08)] rounded-[5px] truncate'>{t('app.types.advanced').toUpperCase()}</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{appDetail.mode === 'agent-chat' && (
|
||||||
|
<div className='shrink-0 px-1 border bg-white border-[rgba(0,0,0,0.08)] rounded-[5px] truncate'>{t('app.types.agent').toUpperCase()}</div>
|
||||||
|
)}
|
||||||
|
{appDetail.mode === 'chat' && (
|
||||||
|
<>
|
||||||
|
<div className='shrink-0 px-1 border bg-white border-[rgba(0,0,0,0.08)] rounded-[5px] truncate'>{t('app.types.chatbot').toUpperCase()}</div>
|
||||||
|
<div title={t('app.types.basic') || ''} className='px-1 border bg-white border-[rgba(0,0,0,0.08)] rounded-[5px] truncate'>{(t('app.types.basic').toUpperCase())}</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{appDetail.mode === 'completion' && (
|
||||||
|
<>
|
||||||
|
<div className='shrink-0 px-1 border bg-white border-[rgba(0,0,0,0.08)] rounded-[5px] truncate'>{t('app.types.completion').toUpperCase()}</div>
|
||||||
|
<div title={t('app.types.basic') || ''} className='px-1 border bg-white border-[rgba(0,0,0,0.08)] rounded-[5px] truncate'>{(t('app.types.basic').toUpperCase())}</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{appDetail.mode === 'workflow' && (
|
||||||
|
<div className='shrink-0 px-1 border bg-white border-[rgba(0,0,0,0.08)] rounded-[5px] truncate'>{t('app.types.workflow').toUpperCase()}</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className='text-text-tertiary system-2xs-medium-uppercase'>{appDetail.mode === 'advanced-chat' ? t('app.types.chatbot') : appDetail.mode === 'agent-chat' ? t('app.types.agent') : appDetail.mode === 'chat' ? t('app.types.chatbot') : appDetail.mode === 'completion' ? t('app.types.completion') : t('app.types.workflow')}</div>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)}
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
<ContentDialog
|
|
||||||
show={open}
|
|
||||||
onClose={() => setOpen(false)}
|
|
||||||
className='!p-0 flex flex-col absolute left-2 top-2 bottom-2 w-[420px] rounded-2xl'
|
|
||||||
>
|
|
||||||
<div className='flex p-4 flex-col justify-center items-start gap-3 self-stretch shrink-0'>
|
|
||||||
<div className='flex items-center gap-3 self-stretch'>
|
|
||||||
<AppIcon
|
|
||||||
size="large"
|
|
||||||
iconType={appDetail.icon_type}
|
|
||||||
icon={appDetail.icon}
|
|
||||||
background={appDetail.icon_background}
|
|
||||||
imageUrl={appDetail.icon_url}
|
|
||||||
/>
|
|
||||||
<div className='flex flex-col justify-center items-start grow w-full'>
|
|
||||||
<div className='text-text-secondary system-md-semibold truncate w-full'>{appDetail.name}</div>
|
|
||||||
<div className='text-text-tertiary system-2xs-medium-uppercase'>{appDetail.mode === 'advanced-chat' ? t('app.types.chatbot') : appDetail.mode === 'agent-chat' ? t('app.types.agent') : appDetail.mode === 'chat' ? t('app.types.chatbot') : appDetail.mode === 'completion' ? t('app.types.completion') : t('app.types.workflow')}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
{/* description */}
|
</PortalToFollowElemTrigger>
|
||||||
{appDetail.description && (
|
<PortalToFollowElemContent className='z-[1002]'>
|
||||||
<div className='text-text-tertiary system-xs-regular'>{appDetail.description}</div>
|
<div className='relative w-[320px] bg-white rounded-2xl shadow-xl'>
|
||||||
)}
|
{/* header */}
|
||||||
{/* operations */}
|
<div className={cn('flex pl-4 pt-3 pr-3', !appDetail.description && 'pb-2')}>
|
||||||
<div className='flex items-center gap-1 self-stretch'>
|
<div className='relative shrink-0 mr-2'>
|
||||||
<Button
|
<AppIcon
|
||||||
size={'small'}
|
size="large"
|
||||||
variant={'secondary'}
|
iconType={appDetail.icon_type}
|
||||||
className='gap-[1px]'
|
icon={appDetail.icon}
|
||||||
onClick={() => {
|
background={appDetail.icon_background}
|
||||||
|
imageUrl={appDetail.icon_url}
|
||||||
|
/>
|
||||||
|
<span className='absolute bottom-[-3px] right-[-3px] w-4 h-4 p-0.5 bg-white rounded border-[0.5px] border-[rgba(0,0,0,0.02)] shadow-sm'>
|
||||||
|
{appDetail.mode === 'advanced-chat' && (
|
||||||
|
<ChatBot className='w-3 h-3 text-[#1570EF]' />
|
||||||
|
)}
|
||||||
|
{appDetail.mode === 'agent-chat' && (
|
||||||
|
<CuteRobot className='w-3 h-3 text-indigo-600' />
|
||||||
|
)}
|
||||||
|
{appDetail.mode === 'chat' && (
|
||||||
|
<ChatBot className='w-3 h-3 text-[#1570EF]' />
|
||||||
|
)}
|
||||||
|
{appDetail.mode === 'completion' && (
|
||||||
|
<AiText className='w-3 h-3 text-[#0E9384]' />
|
||||||
|
)}
|
||||||
|
{appDetail.mode === 'workflow' && (
|
||||||
|
<Route className='w-3 h-3 text-[#f79009]' />
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className='grow w-0'>
|
||||||
|
<div title={appDetail.name} className='flex justify-between items-center text-sm leading-5 font-medium text-gray-900 truncate'>{appDetail.name}</div>
|
||||||
|
<div className='flex items-center text-[10px] leading-[18px] font-medium text-gray-500 gap-1'>
|
||||||
|
{appDetail.mode === 'advanced-chat' && (
|
||||||
|
<>
|
||||||
|
<div className='shrink-0 px-1 border bg-white border-[rgba(0,0,0,0.08)] rounded-[5px] truncate'>{t('app.types.chatbot').toUpperCase()}</div>
|
||||||
|
<div title={t('app.types.advanced') || ''} className='px-1 border bg-white border-[rgba(0,0,0,0.08)] rounded-[5px] truncate'>{t('app.types.advanced').toUpperCase()}</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{appDetail.mode === 'agent-chat' && (
|
||||||
|
<div className='shrink-0 px-1 border bg-white border-[rgba(0,0,0,0.08)] rounded-[5px] truncate'>{t('app.types.agent').toUpperCase()}</div>
|
||||||
|
)}
|
||||||
|
{appDetail.mode === 'chat' && (
|
||||||
|
<>
|
||||||
|
<div className='shrink-0 px-1 border bg-white border-[rgba(0,0,0,0.08)] rounded-[5px] truncate'>{t('app.types.chatbot').toUpperCase()}</div>
|
||||||
|
<div title={t('app.types.basic') || ''} className='px-1 border bg-white border-[rgba(0,0,0,0.08)] rounded-[5px] truncate'>{(t('app.types.basic').toUpperCase())}</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{appDetail.mode === 'completion' && (
|
||||||
|
<>
|
||||||
|
<div className='shrink-0 px-1 border bg-white border-[rgba(0,0,0,0.08)] rounded-[5px] truncate'>{t('app.types.completion').toUpperCase()}</div>
|
||||||
|
<div title={t('app.types.basic') || ''} className='px-1 border bg-white border-[rgba(0,0,0,0.08)] rounded-[5px] truncate'>{(t('app.types.basic').toUpperCase())}</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{appDetail.mode === 'workflow' && (
|
||||||
|
<div className='shrink-0 px-1 border bg-white border-[rgba(0,0,0,0.08)] rounded-[5px] truncate'>{t('app.types.workflow').toUpperCase()}</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/* description */}
|
||||||
|
{appDetail.description && (
|
||||||
|
<div className='px-4 py-2 text-gray-500 text-xs leading-[18px]'>{appDetail.description}</div>
|
||||||
|
)}
|
||||||
|
{/* operations */}
|
||||||
|
<Divider className="!my-1" />
|
||||||
|
<div className="w-full py-1">
|
||||||
|
<div className='h-9 py-2 px-3 mx-1 flex items-center hover:bg-gray-50 rounded-lg cursor-pointer' onClick={() => {
|
||||||
setOpen(false)
|
setOpen(false)
|
||||||
setShowEditModal(true)
|
setShowEditModal(true)
|
||||||
}}
|
}}>
|
||||||
>
|
<span className='text-gray-700 text-sm leading-5'>{t('app.editApp')}</span>
|
||||||
<RiEditLine className='w-3.5 h-3.5 text-components-button-secondary-text' />
|
</div>
|
||||||
<span className='text-components-button-secondary-text system-xs-medium'>{t('app.editApp')}</span>
|
<div className='h-9 py-2 px-3 mx-1 flex items-center hover:bg-gray-50 rounded-lg cursor-pointer' onClick={() => {
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
size={'small'}
|
|
||||||
variant={'secondary'}
|
|
||||||
className='gap-[1px]'
|
|
||||||
onClick={() => {
|
|
||||||
setOpen(false)
|
setOpen(false)
|
||||||
setShowDuplicateModal(true)
|
setShowDuplicateModal(true)
|
||||||
}}
|
}}>
|
||||||
|
<span className='text-gray-700 text-sm leading-5'>{t('app.duplicate')}</span>
|
||||||
|
</div>
|
||||||
|
{(appDetail.mode === 'completion' || appDetail.mode === 'chat') && (
|
||||||
|
<>
|
||||||
|
<Divider className="!my-1" />
|
||||||
|
<div
|
||||||
|
className='h-9 py-2 px-3 mx-1 flex items-center hover:bg-gray-50 rounded-lg cursor-pointer'
|
||||||
|
onMouseEnter={() => setShowSwitchTip(appDetail.mode)}
|
||||||
|
onMouseLeave={() => setShowSwitchTip('')}
|
||||||
|
onClick={() => {
|
||||||
|
setOpen(false)
|
||||||
|
setShowSwitchModal(true)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span className='text-gray-700 text-sm leading-5'>{t('app.switch')}</span>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<Divider className="!my-1" />
|
||||||
|
<div className='h-9 py-2 px-3 mx-1 flex items-center hover:bg-gray-50 rounded-lg cursor-pointer' onClick={exportCheck}>
|
||||||
|
<span className='text-gray-700 text-sm leading-5'>{t('app.export')}</span>
|
||||||
|
</div>
|
||||||
|
{
|
||||||
|
(appDetail.mode === 'advanced-chat' || appDetail.mode === 'workflow') && (
|
||||||
|
<div
|
||||||
|
className='h-9 py-2 px-3 mx-1 flex items-center hover:bg-gray-50 rounded-lg cursor-pointer'
|
||||||
|
onClick={() => {
|
||||||
|
setOpen(false)
|
||||||
|
setShowImportDSLModal(true)
|
||||||
|
}}>
|
||||||
|
<span className='text-gray-700 text-sm leading-5'>{t('workflow.common.importDSL')}</span>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
<Divider className="!my-1" />
|
||||||
|
<div className='group h-9 py-2 px-3 mx-1 flex items-center hover:bg-red-50 rounded-lg cursor-pointer' onClick={() => {
|
||||||
|
setOpen(false)
|
||||||
|
setShowConfirmDelete(true)
|
||||||
|
}}>
|
||||||
|
<span className='text-gray-700 text-sm leading-5 group-hover:text-red-500'>
|
||||||
|
{t('common.operation.delete')}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/* switch tip */}
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'hidden absolute left-[324px] top-0 w-[376px] rounded-xl bg-white border-[0.5px] border-[rgba(0,0,0,0.05)] shadow-lg',
|
||||||
|
showSwitchTip && '!block',
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
<RiFileCopy2Line className='w-3.5 h-3.5 text-components-button-secondary-text' />
|
<div className={cn(
|
||||||
<span className='text-components-button-secondary-text system-xs-medium'>{t('app.duplicate')}</span>
|
'w-full h-[256px] bg-center bg-no-repeat bg-contain rounded-xl',
|
||||||
</Button>
|
showSwitchTip === 'chat' && s.expertPic,
|
||||||
<Button
|
showSwitchTip === 'completion' && s.completionPic,
|
||||||
size={'small'}
|
)} />
|
||||||
variant={'secondary'}
|
<div className='px-4 pb-2'>
|
||||||
className='gap-[1px]'
|
<div className='flex items-center gap-1 text-gray-700 text-md leading-6 font-semibold'>
|
||||||
onClick={exportCheck}
|
{showSwitchTip === 'chat' ? t('app.types.advanced') : t('app.types.workflow')}
|
||||||
>
|
<span className='px-1 rounded-[5px] bg-white border border-black/8 text-gray-500 text-[10px] leading-[18px] font-medium'>BETA</span>
|
||||||
<RiFileDownloadLine className='w-3.5 h-3.5 text-components-button-secondary-text' />
|
</div>
|
||||||
<span className='text-components-button-secondary-text system-xs-medium'>{t('app.export')}</span>
|
<div className='text-orange-500 text-xs leading-[18px] font-medium'>{t('app.newApp.advancedFor').toLocaleUpperCase()}</div>
|
||||||
</Button>
|
<div className='mt-1 text-gray-500 text-sm leading-5'>{t('app.newApp.advancedDescription')}</div>
|
||||||
{
|
</div>
|
||||||
(appDetail.mode === 'advanced-chat' || appDetail.mode === 'workflow') && (
|
</div>
|
||||||
<Button
|
|
||||||
size={'small'}
|
|
||||||
variant={'secondary'}
|
|
||||||
className='gap-[1px]'
|
|
||||||
onClick={() => {
|
|
||||||
setOpen(false)
|
|
||||||
setShowImportDSLModal(true)
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<RiFileUploadLine className='w-3.5 h-3.5 text-components-button-secondary-text' />
|
|
||||||
<span className='text-components-button-secondary-text system-xs-medium'>{t('workflow.common.importDSL')}</span>
|
|
||||||
</Button>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</PortalToFollowElemContent>
|
||||||
<div className='flex flex-1'>
|
{showSwitchModal && (
|
||||||
<CardView
|
<SwitchAppModal
|
||||||
appId={appDetail.id}
|
inAppDetail
|
||||||
isInPanel={true}
|
show={showSwitchModal}
|
||||||
className='flex flex-col px-2 py-1 gap-2 grow overflow-auto'
|
appDetail={appDetail}
|
||||||
|
onClose={() => setShowSwitchModal(false)}
|
||||||
|
onSuccess={() => setShowSwitchModal(false)}
|
||||||
/>
|
/>
|
||||||
</div>
|
)}
|
||||||
<div className='flex p-2 flex-col justify-center items-start gap-3 self-stretch border-t-[0.5px] border-divider-subtle shrink-0 min-h-fit'>
|
{showEditModal && (
|
||||||
<Button
|
<CreateAppModal
|
||||||
size={'medium'}
|
isEditModal
|
||||||
variant={'ghost'}
|
appName={appDetail.name}
|
||||||
className='gap-0.5'
|
appIconType={appDetail.icon_type}
|
||||||
onClick={() => {
|
appIcon={appDetail.icon}
|
||||||
setOpen(false)
|
appIconBackground={appDetail.icon_background}
|
||||||
setShowConfirmDelete(true)
|
appIconUrl={appDetail.icon_url}
|
||||||
}}
|
appDescription={appDetail.description}
|
||||||
>
|
appMode={appDetail.mode}
|
||||||
<RiDeleteBinLine className='w-4 h-4 text-text-tertiary' />
|
appUseIconAsAnswerIcon={appDetail.use_icon_as_answer_icon}
|
||||||
<span className='text-text-tertiary system-sm-medium'>{t('common.operation.deleteApp')}</span>
|
show={showEditModal}
|
||||||
</Button>
|
onConfirm={onEdit}
|
||||||
</div>
|
onHide={() => setShowEditModal(false)}
|
||||||
</ContentDialog>
|
/>
|
||||||
{showSwitchModal && (
|
)}
|
||||||
<SwitchAppModal
|
{showDuplicateModal && (
|
||||||
inAppDetail
|
<DuplicateAppModal
|
||||||
show={showSwitchModal}
|
appName={appDetail.name}
|
||||||
appDetail={appDetail}
|
icon_type={appDetail.icon_type}
|
||||||
onClose={() => setShowSwitchModal(false)}
|
icon={appDetail.icon}
|
||||||
onSuccess={() => setShowSwitchModal(false)}
|
icon_background={appDetail.icon_background}
|
||||||
/>
|
icon_url={appDetail.icon_url}
|
||||||
)}
|
show={showDuplicateModal}
|
||||||
{showEditModal && (
|
onConfirm={onCopy}
|
||||||
<CreateAppModal
|
onHide={() => setShowDuplicateModal(false)}
|
||||||
isEditModal
|
/>
|
||||||
appName={appDetail.name}
|
)}
|
||||||
appIconType={appDetail.icon_type}
|
{showConfirmDelete && (
|
||||||
appIcon={appDetail.icon}
|
<Confirm
|
||||||
appIconBackground={appDetail.icon_background}
|
title={t('app.deleteAppConfirmTitle')}
|
||||||
appIconUrl={appDetail.icon_url}
|
content={t('app.deleteAppConfirmContent')}
|
||||||
appDescription={appDetail.description}
|
isShow={showConfirmDelete}
|
||||||
appMode={appDetail.mode}
|
onConfirm={onConfirmDelete}
|
||||||
appUseIconAsAnswerIcon={appDetail.use_icon_as_answer_icon}
|
onCancel={() => setShowConfirmDelete(false)}
|
||||||
show={showEditModal}
|
/>
|
||||||
onConfirm={onEdit}
|
)}
|
||||||
onHide={() => setShowEditModal(false)}
|
{showImportDSLModal && (
|
||||||
/>
|
<UpdateDSLModal
|
||||||
)}
|
onCancel={() => setShowImportDSLModal(false)}
|
||||||
{showDuplicateModal && (
|
onBackup={exportCheck}
|
||||||
<DuplicateAppModal
|
/>
|
||||||
appName={appDetail.name}
|
)}
|
||||||
icon_type={appDetail.icon_type}
|
{secretEnvList.length > 0 && (
|
||||||
icon={appDetail.icon}
|
<DSLExportConfirmModal
|
||||||
icon_background={appDetail.icon_background}
|
envList={secretEnvList}
|
||||||
icon_url={appDetail.icon_url}
|
onConfirm={onExport}
|
||||||
show={showDuplicateModal}
|
onClose={() => setSecretEnvList([])}
|
||||||
onConfirm={onCopy}
|
/>
|
||||||
onHide={() => setShowDuplicateModal(false)}
|
)}
|
||||||
/>
|
</div>
|
||||||
)}
|
</PortalToFollowElem>
|
||||||
{showConfirmDelete && (
|
|
||||||
<Confirm
|
|
||||||
title={t('app.deleteAppConfirmTitle')}
|
|
||||||
content={t('app.deleteAppConfirmContent')}
|
|
||||||
isShow={showConfirmDelete}
|
|
||||||
onConfirm={onConfirmDelete}
|
|
||||||
onCancel={() => setShowConfirmDelete(false)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{showImportDSLModal && (
|
|
||||||
<UpdateDSLModal
|
|
||||||
onCancel={() => setShowImportDSLModal(false)}
|
|
||||||
onBackup={exportCheck}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{secretEnvList.length > 0 && (
|
|
||||||
<DSLExportConfirmModal
|
|
||||||
envList={secretEnvList}
|
|
||||||
onConfirm={onExport}
|
|
||||||
onClose={() => setSecretEnvList([])}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -58,7 +58,7 @@ export default function AppBasic({ icon, icon_background, name, isExternal, type
|
|||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center grow">
|
<div className="flex items-start p-1">
|
||||||
{icon && icon_background && iconType === 'app' && (
|
{icon && icon_background && iconType === 'app' && (
|
||||||
<div className='flex-shrink-0 mr-3'>
|
<div className='flex-shrink-0 mr-3'>
|
||||||
<AppIcon icon={icon} background={icon_background} />
|
<AppIcon icon={icon} background={icon_background} />
|
||||||
@ -71,10 +71,8 @@ export default function AppBasic({ icon, icon_background, name, isExternal, type
|
|||||||
|
|
||||||
}
|
}
|
||||||
{mode === 'expand' && <div className="group">
|
{mode === 'expand' && <div className="group">
|
||||||
<div className={`flex flex-row items-center system-md-semibold text-text-secondary group-hover:text-text-primary ${textStyle?.main ?? ''}`}>
|
<div className={`flex flex-row items-center text-sm font-semibold text-gray-700 group-hover:text-gray-900 break-all ${textStyle?.main ?? ''}`}>
|
||||||
<div className="max-w-[180px] truncate">
|
{name}
|
||||||
{name}
|
|
||||||
</div>
|
|
||||||
{hoverTip
|
{hoverTip
|
||||||
&& <Tooltip
|
&& <Tooltip
|
||||||
popupContent={
|
popupContent={
|
||||||
@ -88,6 +86,7 @@ export default function AppBasic({ icon, icon_background, name, isExternal, type
|
|||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
<div className={`text-xs font-normal text-gray-500 group-hover:text-gray-700 break-all ${textStyle?.extra ?? ''}`}>{type}</div>
|
||||||
<div className='text-text-tertiary system-2xs-medium-uppercase'>{isExternal ? t('dataset.externalTag') : ''}</div>
|
<div className='text-text-tertiary system-2xs-medium-uppercase'>{isExternal ? t('dataset.externalTag') : ''}</div>
|
||||||
</div>}
|
</div>}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -57,7 +57,7 @@ const AppDetailNav = ({ title, desc, isExternal, icon, icon_background, navigati
|
|||||||
<div
|
<div
|
||||||
className={`
|
className={`
|
||||||
shrink-0
|
shrink-0
|
||||||
${expand ? 'p-2' : 'p-1'}
|
${expand ? 'p-3' : 'p-2'}
|
||||||
`}
|
`}
|
||||||
>
|
>
|
||||||
{iconType === 'app' && (
|
{iconType === 'app' && (
|
||||||
|
|||||||
@ -44,7 +44,7 @@ export default function NavLink({
|
|||||||
key={name}
|
key={name}
|
||||||
href={href}
|
href={href}
|
||||||
className={classNames(
|
className={classNames(
|
||||||
isActive ? 'bg-state-accent-active text-text-accent font-semibold' : 'text-components-menu-item-text hover:bg-state-base-hover hover:text-components-menu-item-text-hover',
|
isActive ? 'bg-state-accent-active text-text-accent font-semibold' : 'text-components-menu-item-text hover:bg-gray-100 hover:text-components-menu-item-text-hover',
|
||||||
'group flex items-center h-9 rounded-md py-2 text-sm font-normal',
|
'group flex items-center h-9 rounded-md py-2 text-sm font-normal',
|
||||||
mode === 'expand' ? 'px-3' : 'px-2.5',
|
mode === 'expand' ? 'px-3' : 'px-2.5',
|
||||||
)}
|
)}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user