mirror of
https://github.com/langgenius/dify.git
synced 2026-01-27 23:35:51 +08:00
Compare commits
19 Commits
feat/webap
...
0.15.7
| Author | SHA1 | Date | |
|---|---|---|---|
| 5f7f851b17 | |||
| 559ab46ee1 | |||
| df98223c8c | |||
| 144f9507f8 | |||
| 2e097a1ac0 | |||
| 9f7d8a981f | |||
| 40b31bafd5 | |||
| d38a2c95fb | |||
| 7d18e2a0ef | |||
| 024f242251 | |||
| bfdce78ca5 | |||
| 00c2258352 | |||
| a1b3d41712 | |||
| b26e20fe34 | |||
| 161ff432f1 | |||
| 99a9def623 | |||
| fe1846c437 | |||
| 8e75eb5c63 | |||
| 970508fcb6 |
6
.github/workflows/build-push.yml
vendored
6
.github/workflows/build-push.yml
vendored
@ -5,10 +5,8 @@ on:
|
||||
branches:
|
||||
- "main"
|
||||
- "deploy/dev"
|
||||
- "deploy/enterprise"
|
||||
- "e-0154"
|
||||
release:
|
||||
types: [published]
|
||||
tags:
|
||||
- "*"
|
||||
|
||||
concurrency:
|
||||
group: build-push-${{ github.head_ref || github.run_id }}
|
||||
|
||||
29
.github/workflows/deploy-enterprise.yml
vendored
29
.github/workflows/deploy-enterprise.yml
vendored
@ -1,29 +0,0 @@
|
||||
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 }}
|
||||
3
.markdownlint.json
Normal file
3
.markdownlint.json
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"MD024": false
|
||||
}
|
||||
32
CHANGELOG.md
Normal file
32
CHANGELOG.md
Normal file
@ -0,0 +1,32 @@
|
||||
# Changelog
|
||||
|
||||
All notable changes to Dify will be documented in this file.
|
||||
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [0.15.7] - 2025-04-27
|
||||
|
||||
### Added
|
||||
|
||||
- Added support for GPT-4.1 in model providers (#18912)
|
||||
- Added support for Amazon Bedrock DeepSeek-R1 model (#18908)
|
||||
- Added support for Amazon Bedrock Claude Sonnet 3.7 model (#18788)
|
||||
- Refined version compatibility logic in app DSL service
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed issue with creating apps from template categories (#18807, #18868)
|
||||
- Fixed DSL version check when creating apps from explore templates (#18872, #18878)
|
||||
|
||||
## [0.15.6] - 2025-04-22
|
||||
|
||||
### Security
|
||||
|
||||
- Fixed clickjacking vulnerability (#18552)
|
||||
- Fixed reset password security issue (#18366)
|
||||
- Updated reset password token when email code verification succeeds (#18362)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed Vertex AI Gemini 2.0 Flash 001 schema (#18405)
|
||||
@ -430,4 +430,7 @@ CREATE_TIDB_SERVICE_JOB_ENABLED=false
|
||||
# Maximum number of submitted thread count in a ThreadPool for parallel node execution
|
||||
MAX_SUBMIT_COUNT=100
|
||||
# Lockout duration in seconds
|
||||
LOGIN_LOCKOUT_DURATION=86400
|
||||
LOGIN_LOCKOUT_DURATION=86400
|
||||
|
||||
# Prevent Clickjacking
|
||||
ALLOW_EMBED=false
|
||||
@ -9,7 +9,7 @@ class PackagingInfo(BaseSettings):
|
||||
|
||||
CURRENT_VERSION: str = Field(
|
||||
description="Dify version",
|
||||
default="0.15.4",
|
||||
default="0.15.7",
|
||||
)
|
||||
|
||||
COMMIT_SHA: str = Field(
|
||||
|
||||
@ -2,28 +2,30 @@ import uuid
|
||||
from typing import cast
|
||||
|
||||
from flask_login import current_user # type: ignore
|
||||
from flask_restful import (Resource, inputs, marshal, # type: ignore
|
||||
marshal_with, reqparse)
|
||||
from flask_restful import Resource, inputs, marshal, marshal_with, reqparse # type: ignore
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.orm import Session
|
||||
from werkzeug.exceptions import BadRequest, Forbidden, abort
|
||||
|
||||
from controllers.console import api
|
||||
from controllers.console.app.wraps import get_app_model
|
||||
from controllers.console.wraps import (account_initialization_required,
|
||||
cloud_edition_billing_resource_check,
|
||||
enterprise_license_required,
|
||||
setup_required)
|
||||
from controllers.console.wraps import (
|
||||
account_initialization_required,
|
||||
cloud_edition_billing_resource_check,
|
||||
enterprise_license_required,
|
||||
setup_required,
|
||||
)
|
||||
from core.ops.ops_trace_manager import OpsTraceManager
|
||||
from extensions.ext_database import db
|
||||
from fields.app_fields import (app_detail_fields, app_detail_fields_with_site,
|
||||
app_pagination_fields)
|
||||
from fields.app_fields import (
|
||||
app_detail_fields,
|
||||
app_detail_fields_with_site,
|
||||
app_pagination_fields,
|
||||
)
|
||||
from libs.login import login_required
|
||||
from models import Account, App
|
||||
from services.app_dsl_service import AppDslService, ImportMode
|
||||
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"]
|
||||
|
||||
@ -65,17 +67,7 @@ class AppListApi(Resource):
|
||||
if not app_pagination:
|
||||
return {"data": [], "total": 0, "page": 1, "limit": 20, "has_more": False}
|
||||
|
||||
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
|
||||
return marshal(app_pagination, app_pagination_fields)
|
||||
|
||||
@setup_required
|
||||
@login_required
|
||||
@ -119,10 +111,6 @@ class AppApi(Resource):
|
||||
|
||||
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
|
||||
|
||||
@setup_required
|
||||
|
||||
@ -6,13 +6,9 @@ from flask_restful import Resource, reqparse # type: ignore
|
||||
|
||||
from constants.languages import languages
|
||||
from controllers.console import api
|
||||
from controllers.console.auth.error import (EmailCodeError, InvalidEmailError,
|
||||
InvalidTokenError,
|
||||
PasswordMismatchError)
|
||||
from controllers.console.error import (AccountInFreezeError, AccountNotFound,
|
||||
EmailSendIpLimitError)
|
||||
from controllers.console.wraps import (email_password_login_enabled,
|
||||
setup_required)
|
||||
from controllers.console.auth.error import EmailCodeError, InvalidEmailError, InvalidTokenError, PasswordMismatchError
|
||||
from controllers.console.error import AccountInFreezeError, AccountNotFound, EmailSendIpLimitError
|
||||
from controllers.console.wraps import email_password_login_enabled, setup_required
|
||||
from events.tenant_event import tenant_was_created
|
||||
from extensions.ext_database import db
|
||||
from libs.helper import email, extract_remote_ip
|
||||
|
||||
@ -23,9 +23,3 @@ class AppSuggestedQuestionsAfterAnswerDisabledError(BaseHTTPException):
|
||||
error_code = "app_suggested_questions_after_answer_disabled"
|
||||
description = "Function Suggested questions after answer disabled."
|
||||
code = 403
|
||||
|
||||
|
||||
class AppAccessDeniedError(BaseHTTPException):
|
||||
error_code = "access_denied"
|
||||
description = "App access denied."
|
||||
code = 403
|
||||
|
||||
@ -1,26 +1,20 @@
|
||||
import logging
|
||||
from datetime import UTC, datetime
|
||||
from typing import Any
|
||||
|
||||
from flask import request
|
||||
from flask_login import current_user # type: ignore
|
||||
from flask_restful import (Resource, inputs, marshal_with, # type: ignore
|
||||
reqparse)
|
||||
from flask_restful import Resource, inputs, marshal_with, reqparse # type: ignore
|
||||
from sqlalchemy import and_
|
||||
from werkzeug.exceptions import BadRequest, Forbidden, NotFound
|
||||
|
||||
from controllers.console import api
|
||||
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 fields.installed_app_fields import installed_app_list_fields
|
||||
from libs.login import login_required
|
||||
from models import App, InstalledApp, RecommendedApp
|
||||
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):
|
||||
@ -54,23 +48,6 @@ class InstalledAppsListApi(Resource):
|
||||
for installed_app in installed_apps
|
||||
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(
|
||||
key=lambda app: (
|
||||
-app["is_pinned"],
|
||||
|
||||
@ -4,14 +4,10 @@ from flask_login import current_user # type: ignore
|
||||
from flask_restful import Resource # type: ignore
|
||||
from werkzeug.exceptions import NotFound
|
||||
|
||||
from controllers.console.explore.error import AppAccessDeniedError
|
||||
from controllers.console.wraps import account_initialization_required
|
||||
from extensions.ext_database import db
|
||||
from libs.login import login_required
|
||||
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):
|
||||
@ -52,30 +48,6 @@ def installed_app_required(view=None):
|
||||
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):
|
||||
# must be reversed if there are multiple decorators
|
||||
|
||||
method_decorators = [user_allowed_to_access_app, installed_app_required, account_initialization_required, login_required]
|
||||
method_decorators = [installed_app_required, account_initialization_required, login_required]
|
||||
|
||||
@ -11,8 +11,7 @@ from models.model import DifySetup
|
||||
from services.feature_service import FeatureService, LicenseStatus
|
||||
from services.operation_service import OperationService
|
||||
|
||||
from .error import (NotInitValidateError, NotSetupError,
|
||||
UnauthorizedAndForceLogout)
|
||||
from .error import NotInitValidateError, NotSetupError, UnauthorizedAndForceLogout
|
||||
|
||||
|
||||
def account_initialization_required(view):
|
||||
@ -40,17 +39,6 @@ def only_edition_cloud(view):
|
||||
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):
|
||||
@wraps(view)
|
||||
def decorated(*args, **kwargs):
|
||||
|
||||
@ -5,5 +5,4 @@ from libs.external_api import ExternalApi
|
||||
bp = Blueprint("inner_api", __name__, url_prefix="/inner/api")
|
||||
api = ExternalApi(bp)
|
||||
|
||||
from . import mail
|
||||
from .workspace import workspace
|
||||
|
||||
@ -1,27 +0,0 @@
|
||||
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,16 +1,12 @@
|
||||
|
||||
from flask import request
|
||||
from flask_restful import Resource, marshal_with, reqparse # type: ignore
|
||||
from flask_restful import marshal_with # type: ignore
|
||||
|
||||
from controllers.common import fields
|
||||
from controllers.common import helpers as controller_helpers
|
||||
from controllers.web import api
|
||||
from controllers.web.error import AppUnavailableError
|
||||
from controllers.web.wraps import WebApiResource
|
||||
from libs.passport import PassportService
|
||||
from models.model import App, AppMode
|
||||
from services.app_service import AppService
|
||||
from services.enterprise.enterprise_service import EnterpriseService
|
||||
|
||||
|
||||
class AppParameterApi(WebApiResource):
|
||||
@ -46,51 +42,5 @@ class AppMeta(WebApiResource):
|
||||
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(AppMeta, "/meta")
|
||||
# webapp auth apis
|
||||
api.add_resource(AppAccessMode, "/webapp/access-mode")
|
||||
api.add_resource(AppWebAuthPermission, "/webapp/permission")
|
||||
|
||||
@ -121,15 +121,9 @@ class UnsupportedFileTypeError(BaseHTTPException):
|
||||
code = 415
|
||||
|
||||
|
||||
class WebAppAuthRequiredError(BaseHTTPException):
|
||||
class WebSSOAuthRequiredError(BaseHTTPException):
|
||||
error_code = "web_sso_auth_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."
|
||||
description = "Web SSO authentication required."
|
||||
code = 401
|
||||
|
||||
|
||||
|
||||
@ -1,121 +0,0 @@
|
||||
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 controllers.web import api
|
||||
from controllers.web.error import WebAppAuthRequiredError
|
||||
from controllers.web.error import WebSSOAuthRequiredError
|
||||
from extensions.ext_database import db
|
||||
from libs.passport import PassportService
|
||||
from models.model import App, EndUser, Site
|
||||
@ -22,10 +22,10 @@ class PassportResource(Resource):
|
||||
if app_code is None:
|
||||
raise Unauthorized("X-App-Code header is missing.")
|
||||
|
||||
if system_features.webapp_auth.enabled:
|
||||
app_settings = EnterpriseService.WebAppAuth.get_app_access_mode_by_code(app_code=app_code)
|
||||
if not app_settings or not app_settings.access_mode == "public":
|
||||
raise WebAppAuthRequiredError()
|
||||
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:
|
||||
raise WebSSOAuthRequiredError()
|
||||
|
||||
# get site from db and check if it is normal
|
||||
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 werkzeug.exceptions import BadRequest, NotFound, Unauthorized
|
||||
|
||||
from controllers.web.error import WebAppAuthAccessDeniedError, WebAppAuthRequiredError
|
||||
from controllers.web.error import WebSSOAuthRequiredError
|
||||
from extensions.ext_database import db
|
||||
from libs.passport import PassportService
|
||||
from models.model import App, EndUser, Site
|
||||
@ -57,53 +57,35 @@ def decode_jwt_token():
|
||||
if not end_user:
|
||||
raise NotFound()
|
||||
|
||||
# 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)
|
||||
_validate_web_sso_token(decoded, system_features, app_code)
|
||||
|
||||
return app_model, end_user
|
||||
except Unauthorized as e:
|
||||
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"
|
||||
)
|
||||
if app_web_auth_enabled:
|
||||
raise WebAppAuthRequiredError()
|
||||
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:
|
||||
raise WebSSOAuthRequiredError()
|
||||
|
||||
raise Unauthorized(e.description)
|
||||
|
||||
|
||||
def _validate_webapp_token(decoded, app_web_auth_enabled: bool, system_webapp_auth_enabled: bool):
|
||||
# Check if authentication is enforced for web app, and if the token source is not webapp,
|
||||
# raise an error and redirect to login
|
||||
if system_webapp_auth_enabled and app_web_auth_enabled:
|
||||
source = decoded.get("token_source")
|
||||
if not source or source != "webapp":
|
||||
raise WebAppAuthRequiredError()
|
||||
def _validate_web_sso_token(decoded, system_features, app_code):
|
||||
app_web_sso_enabled = False
|
||||
|
||||
# Check if authentication is not enforced for web, and if the token source is webapp,
|
||||
# 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_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_webapp_auth_enabled or not app_web_auth_enabled:
|
||||
if not system_features.sso_enforced_for_web or not app_web_sso_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()
|
||||
if source and source == "sso":
|
||||
raise Unauthorized("sso token expired.")
|
||||
|
||||
|
||||
class WebApiResource(Resource):
|
||||
|
||||
@ -104,7 +104,6 @@ class CotAgentRunner(BaseAgentRunner, ABC):
|
||||
|
||||
# recalc llm max tokens
|
||||
prompt_messages = self._organize_prompt_messages()
|
||||
self.recalc_llm_max_tokens(self.model_config, prompt_messages)
|
||||
# invoke model
|
||||
chunks = model_instance.invoke_llm(
|
||||
prompt_messages=prompt_messages,
|
||||
|
||||
@ -84,7 +84,6 @@ class FunctionCallAgentRunner(BaseAgentRunner):
|
||||
|
||||
# recalc llm max tokens
|
||||
prompt_messages = self._organize_prompt_messages()
|
||||
self.recalc_llm_max_tokens(self.model_config, prompt_messages)
|
||||
# invoke model
|
||||
chunks: Union[Generator[LLMResultChunk, None, None], LLMResult] = model_instance.invoke_llm(
|
||||
prompt_messages=prompt_messages,
|
||||
|
||||
@ -55,20 +55,6 @@ class AgentChatAppRunner(AppRunner):
|
||||
query = application_generate_entity.query
|
||||
files = application_generate_entity.files
|
||||
|
||||
# Pre-calculate the number of tokens of the prompt messages,
|
||||
# and return the rest number of tokens by model context token size limit and max token size limit.
|
||||
# If the rest number of tokens is not enough, raise exception.
|
||||
# Include: prompt template, inputs, query(optional), files(optional)
|
||||
# Not Include: memory, external data, dataset context
|
||||
self.get_pre_calculate_rest_tokens(
|
||||
app_record=app_record,
|
||||
model_config=application_generate_entity.model_conf,
|
||||
prompt_template_entity=app_config.prompt_template,
|
||||
inputs=inputs,
|
||||
files=files,
|
||||
query=query,
|
||||
)
|
||||
|
||||
memory = None
|
||||
if application_generate_entity.conversation_id:
|
||||
# get memory of conversation (read-only)
|
||||
|
||||
@ -15,10 +15,8 @@ from core.app.features.annotation_reply.annotation_reply import AnnotationReplyF
|
||||
from core.app.features.hosting_moderation.hosting_moderation import HostingModerationFeature
|
||||
from core.external_data_tool.external_data_fetch import ExternalDataFetch
|
||||
from core.memory.token_buffer_memory import TokenBufferMemory
|
||||
from core.model_manager import ModelInstance
|
||||
from core.model_runtime.entities.llm_entities import LLMResult, LLMResultChunk, LLMResultChunkDelta, LLMUsage
|
||||
from core.model_runtime.entities.message_entities import AssistantPromptMessage, PromptMessage
|
||||
from core.model_runtime.entities.model_entities import ModelPropertyKey
|
||||
from core.model_runtime.errors.invoke import InvokeBadRequestError
|
||||
from core.moderation.input_moderation import InputModeration
|
||||
from core.prompt.advanced_prompt_transform import AdvancedPromptTransform
|
||||
@ -31,106 +29,6 @@ if TYPE_CHECKING:
|
||||
|
||||
|
||||
class AppRunner:
|
||||
def get_pre_calculate_rest_tokens(
|
||||
self,
|
||||
app_record: App,
|
||||
model_config: ModelConfigWithCredentialsEntity,
|
||||
prompt_template_entity: PromptTemplateEntity,
|
||||
inputs: Mapping[str, str],
|
||||
files: Sequence["File"],
|
||||
query: Optional[str] = None,
|
||||
) -> int:
|
||||
"""
|
||||
Get pre calculate rest tokens
|
||||
:param app_record: app record
|
||||
:param model_config: model config entity
|
||||
:param prompt_template_entity: prompt template entity
|
||||
:param inputs: inputs
|
||||
:param files: files
|
||||
:param query: query
|
||||
:return:
|
||||
"""
|
||||
# Invoke model
|
||||
model_instance = ModelInstance(
|
||||
provider_model_bundle=model_config.provider_model_bundle, model=model_config.model
|
||||
)
|
||||
|
||||
model_context_tokens = model_config.model_schema.model_properties.get(ModelPropertyKey.CONTEXT_SIZE)
|
||||
|
||||
max_tokens = 0
|
||||
for parameter_rule in model_config.model_schema.parameter_rules:
|
||||
if parameter_rule.name == "max_tokens" or (
|
||||
parameter_rule.use_template and parameter_rule.use_template == "max_tokens"
|
||||
):
|
||||
max_tokens = (
|
||||
model_config.parameters.get(parameter_rule.name)
|
||||
or model_config.parameters.get(parameter_rule.use_template or "")
|
||||
) or 0
|
||||
|
||||
if model_context_tokens is None:
|
||||
return -1
|
||||
|
||||
if max_tokens is None:
|
||||
max_tokens = 0
|
||||
|
||||
# get prompt messages without memory and context
|
||||
prompt_messages, stop = self.organize_prompt_messages(
|
||||
app_record=app_record,
|
||||
model_config=model_config,
|
||||
prompt_template_entity=prompt_template_entity,
|
||||
inputs=inputs,
|
||||
files=files,
|
||||
query=query,
|
||||
)
|
||||
|
||||
prompt_tokens = model_instance.get_llm_num_tokens(prompt_messages)
|
||||
|
||||
rest_tokens: int = model_context_tokens - max_tokens - prompt_tokens
|
||||
if rest_tokens < 0:
|
||||
raise InvokeBadRequestError(
|
||||
"Query or prefix prompt is too long, you can reduce the prefix prompt, "
|
||||
"or shrink the max token, or switch to a llm with a larger token limit size."
|
||||
)
|
||||
|
||||
return rest_tokens
|
||||
|
||||
def recalc_llm_max_tokens(
|
||||
self, model_config: ModelConfigWithCredentialsEntity, prompt_messages: list[PromptMessage]
|
||||
):
|
||||
# recalc max_tokens if sum(prompt_token + max_tokens) over model token limit
|
||||
model_instance = ModelInstance(
|
||||
provider_model_bundle=model_config.provider_model_bundle, model=model_config.model
|
||||
)
|
||||
|
||||
model_context_tokens = model_config.model_schema.model_properties.get(ModelPropertyKey.CONTEXT_SIZE)
|
||||
|
||||
max_tokens = 0
|
||||
for parameter_rule in model_config.model_schema.parameter_rules:
|
||||
if parameter_rule.name == "max_tokens" or (
|
||||
parameter_rule.use_template and parameter_rule.use_template == "max_tokens"
|
||||
):
|
||||
max_tokens = (
|
||||
model_config.parameters.get(parameter_rule.name)
|
||||
or model_config.parameters.get(parameter_rule.use_template or "")
|
||||
) or 0
|
||||
|
||||
if model_context_tokens is None:
|
||||
return -1
|
||||
|
||||
if max_tokens is None:
|
||||
max_tokens = 0
|
||||
|
||||
prompt_tokens = model_instance.get_llm_num_tokens(prompt_messages)
|
||||
|
||||
if prompt_tokens + max_tokens > model_context_tokens:
|
||||
max_tokens = max(model_context_tokens - prompt_tokens, 16)
|
||||
|
||||
for parameter_rule in model_config.model_schema.parameter_rules:
|
||||
if parameter_rule.name == "max_tokens" or (
|
||||
parameter_rule.use_template and parameter_rule.use_template == "max_tokens"
|
||||
):
|
||||
model_config.parameters[parameter_rule.name] = max_tokens
|
||||
|
||||
def organize_prompt_messages(
|
||||
self,
|
||||
app_record: App,
|
||||
|
||||
@ -50,20 +50,6 @@ class ChatAppRunner(AppRunner):
|
||||
query = application_generate_entity.query
|
||||
files = application_generate_entity.files
|
||||
|
||||
# Pre-calculate the number of tokens of the prompt messages,
|
||||
# and return the rest number of tokens by model context token size limit and max token size limit.
|
||||
# If the rest number of tokens is not enough, raise exception.
|
||||
# Include: prompt template, inputs, query(optional), files(optional)
|
||||
# Not Include: memory, external data, dataset context
|
||||
self.get_pre_calculate_rest_tokens(
|
||||
app_record=app_record,
|
||||
model_config=application_generate_entity.model_conf,
|
||||
prompt_template_entity=app_config.prompt_template,
|
||||
inputs=inputs,
|
||||
files=files,
|
||||
query=query,
|
||||
)
|
||||
|
||||
memory = None
|
||||
if application_generate_entity.conversation_id:
|
||||
# get memory of conversation (read-only)
|
||||
@ -194,9 +180,6 @@ class ChatAppRunner(AppRunner):
|
||||
if hosting_moderation_result:
|
||||
return
|
||||
|
||||
# Re-calculate the max tokens if sum(prompt_token + max_tokens) over model token limit
|
||||
self.recalc_llm_max_tokens(model_config=application_generate_entity.model_conf, prompt_messages=prompt_messages)
|
||||
|
||||
# Invoke model
|
||||
model_instance = ModelInstance(
|
||||
provider_model_bundle=application_generate_entity.model_conf.provider_model_bundle,
|
||||
|
||||
@ -43,20 +43,6 @@ class CompletionAppRunner(AppRunner):
|
||||
query = application_generate_entity.query
|
||||
files = application_generate_entity.files
|
||||
|
||||
# Pre-calculate the number of tokens of the prompt messages,
|
||||
# and return the rest number of tokens by model context token size limit and max token size limit.
|
||||
# If the rest number of tokens is not enough, raise exception.
|
||||
# Include: prompt template, inputs, query(optional), files(optional)
|
||||
# Not Include: memory, external data, dataset context
|
||||
self.get_pre_calculate_rest_tokens(
|
||||
app_record=app_record,
|
||||
model_config=application_generate_entity.model_conf,
|
||||
prompt_template_entity=app_config.prompt_template,
|
||||
inputs=inputs,
|
||||
files=files,
|
||||
query=query,
|
||||
)
|
||||
|
||||
# organize all inputs and template to prompt messages
|
||||
# Include: prompt template, inputs, query(optional), files(optional)
|
||||
prompt_messages, stop = self.organize_prompt_messages(
|
||||
@ -152,9 +138,6 @@ class CompletionAppRunner(AppRunner):
|
||||
if hosting_moderation_result:
|
||||
return
|
||||
|
||||
# Re-calculate the max tokens if sum(prompt_token + max_tokens) over model token limit
|
||||
self.recalc_llm_max_tokens(model_config=application_generate_entity.model_conf, prompt_messages=prompt_messages)
|
||||
|
||||
# Invoke model
|
||||
model_instance = ModelInstance(
|
||||
provider_model_bundle=application_generate_entity.model_conf.provider_model_bundle,
|
||||
|
||||
@ -26,7 +26,7 @@ class TokenBufferMemory:
|
||||
self.model_instance = model_instance
|
||||
|
||||
def get_history_prompt_messages(
|
||||
self, max_token_limit: int = 2000, message_limit: Optional[int] = None
|
||||
self, max_token_limit: int = 100000, message_limit: Optional[int] = None
|
||||
) -> Sequence[PromptMessage]:
|
||||
"""
|
||||
Get history prompt messages.
|
||||
|
||||
@ -0,0 +1,115 @@
|
||||
model: us.anthropic.claude-3-7-sonnet-20250219-v1:0
|
||||
label:
|
||||
en_US: Claude 3.7 Sonnet(US.Cross Region Inference)
|
||||
icon: icon_s_en.svg
|
||||
model_type: llm
|
||||
features:
|
||||
- agent-thought
|
||||
- vision
|
||||
- tool-call
|
||||
- stream-tool-call
|
||||
model_properties:
|
||||
mode: chat
|
||||
context_size: 200000
|
||||
# docs: https://docs.aws.amazon.com/bedrock/latest/userguide/model-parameters-anthropic-claude-messages.html
|
||||
parameter_rules:
|
||||
- name: enable_cache
|
||||
label:
|
||||
zh_Hans: 启用提示缓存
|
||||
en_US: Enable Prompt Cache
|
||||
type: boolean
|
||||
required: false
|
||||
default: true
|
||||
help:
|
||||
zh_Hans: 启用提示缓存可以提高性能并降低成本。Claude 3.7 Sonnet支持在system、messages和tools字段中使用缓存检查点。
|
||||
en_US: Enable prompt caching to improve performance and reduce costs. Claude 3.7 Sonnet supports cache checkpoints in system, messages, and tools fields.
|
||||
- name: reasoning_type
|
||||
label:
|
||||
zh_Hans: 推理配置
|
||||
en_US: Reasoning Type
|
||||
type: boolean
|
||||
required: false
|
||||
default: false
|
||||
placeholder:
|
||||
zh_Hans: 设置推理配置
|
||||
en_US: Set reasoning configuration
|
||||
help:
|
||||
zh_Hans: 控制模型的推理能力。启用时,temperature将固定为1且top_p将被禁用。
|
||||
en_US: Controls the model's reasoning capability. When enabled, temperature will be fixed to 1 and top_p will be disabled.
|
||||
- name: reasoning_budget
|
||||
show_on:
|
||||
- variable: reasoning_type
|
||||
value: true
|
||||
label:
|
||||
zh_Hans: 推理预算
|
||||
en_US: Reasoning Budget
|
||||
type: int
|
||||
default: 1024
|
||||
min: 0
|
||||
max: 128000
|
||||
help:
|
||||
zh_Hans: 推理的预算限制(最小1024),必须小于max_tokens。仅在推理类型为enabled时可用。
|
||||
en_US: Budget limit for reasoning (minimum 1024), must be less than max_tokens. Only available when reasoning type is enabled.
|
||||
|
||||
- name: max_tokens
|
||||
use_template: max_tokens
|
||||
required: true
|
||||
label:
|
||||
zh_Hans: 最大token数
|
||||
en_US: Max Tokens
|
||||
type: int
|
||||
default: 8192
|
||||
min: 1
|
||||
max: 128000
|
||||
help:
|
||||
zh_Hans: 停止前生成的最大令牌数。请注意,Anthropic Claude 模型可能会在达到 max_tokens 的值之前停止生成令牌。不同的 Anthropic Claude 模型对此参数具有不同的最大值。
|
||||
en_US: The maximum number of tokens to generate before stopping. Note that Anthropic Claude models might stop generating tokens before reaching the value of max_tokens. Different Anthropic Claude models have different maximum values for this parameter.
|
||||
- name: temperature
|
||||
use_template: temperature
|
||||
required: false
|
||||
label:
|
||||
zh_Hans: 模型温度
|
||||
en_US: Model Temperature
|
||||
type: float
|
||||
default: 1
|
||||
min: 0.0
|
||||
max: 1.0
|
||||
help:
|
||||
zh_Hans: 生成内容的随机性。当推理功能启用时,该值将被固定为1。
|
||||
en_US: The amount of randomness injected into the response. When reasoning is enabled, this value will be fixed to 1.
|
||||
- name: top_p
|
||||
show_on:
|
||||
- variable: reasoning_type
|
||||
value: disabled
|
||||
use_template: top_p
|
||||
label:
|
||||
zh_Hans: Top P
|
||||
en_US: Top P
|
||||
required: false
|
||||
type: float
|
||||
default: 0.999
|
||||
min: 0.000
|
||||
max: 1.000
|
||||
help:
|
||||
zh_Hans: 在核采样中的概率阈值。当推理功能启用时,该参数将被禁用。
|
||||
en_US: The probability threshold in nucleus sampling. When reasoning is enabled, this parameter will be disabled.
|
||||
- name: top_k
|
||||
label:
|
||||
zh_Hans: 取样数量
|
||||
en_US: Top k
|
||||
required: false
|
||||
type: int
|
||||
default: 0
|
||||
min: 0
|
||||
# tip docs from aws has error, max value is 500
|
||||
max: 500
|
||||
help:
|
||||
zh_Hans: 对于每个后续标记,仅从前 K 个选项中进行采样。使用 top_k 删除长尾低概率响应。
|
||||
en_US: Only sample from the top K options for each subsequent token. Use top_k to remove long tail low probability responses.
|
||||
- name: response_format
|
||||
use_template: response_format
|
||||
pricing:
|
||||
input: '0.003'
|
||||
output: '0.015'
|
||||
unit: '0.001'
|
||||
currency: USD
|
||||
@ -58,6 +58,7 @@ class BedrockLargeLanguageModel(LargeLanguageModel):
|
||||
# TODO There is invoke issue: context limit on Cohere Model, will add them after fixed.
|
||||
CONVERSE_API_ENABLED_MODEL_INFO = [
|
||||
{"prefix": "anthropic.claude-v2", "support_system_prompts": True, "support_tool_use": False},
|
||||
{"prefix": "us.deepseek", "support_system_prompts": True, "support_tool_use": False},
|
||||
{"prefix": "anthropic.claude-v1", "support_system_prompts": True, "support_tool_use": False},
|
||||
{"prefix": "us.anthropic.claude-3", "support_system_prompts": True, "support_tool_use": True},
|
||||
{"prefix": "eu.anthropic.claude-3", "support_system_prompts": True, "support_tool_use": True},
|
||||
|
||||
@ -0,0 +1,63 @@
|
||||
model: us.deepseek.r1-v1:0
|
||||
label:
|
||||
en_US: DeepSeek-R1(US.Cross Region Inference)
|
||||
icon: icon_s_en.svg
|
||||
model_type: llm
|
||||
features:
|
||||
- agent-thought
|
||||
- vision
|
||||
- tool-call
|
||||
- stream-tool-call
|
||||
model_properties:
|
||||
mode: chat
|
||||
context_size: 32768
|
||||
parameter_rules:
|
||||
- name: max_tokens
|
||||
use_template: max_tokens
|
||||
required: true
|
||||
label:
|
||||
zh_Hans: 最大token数
|
||||
en_US: Max Tokens
|
||||
type: int
|
||||
default: 8192
|
||||
min: 1
|
||||
max: 128000
|
||||
help:
|
||||
zh_Hans: 停止前生成的最大令牌数。
|
||||
en_US: The maximum number of tokens to generate before stopping.
|
||||
- name: temperature
|
||||
use_template: temperature
|
||||
required: false
|
||||
label:
|
||||
zh_Hans: 模型温度
|
||||
en_US: Model Temperature
|
||||
type: float
|
||||
default: 1
|
||||
min: 0.0
|
||||
max: 1.0
|
||||
help:
|
||||
zh_Hans: 生成内容的随机性。当推理功能启用时,该值将被固定为1。
|
||||
en_US: The amount of randomness injected into the response. When reasoning is enabled, this value will be fixed to 1.
|
||||
- name: top_p
|
||||
show_on:
|
||||
- variable: reasoning_type
|
||||
value: disabled
|
||||
use_template: top_p
|
||||
label:
|
||||
zh_Hans: Top P
|
||||
en_US: Top P
|
||||
required: false
|
||||
type: float
|
||||
default: 0.999
|
||||
min: 0.000
|
||||
max: 1.000
|
||||
help:
|
||||
zh_Hans: 在核采样中的概率阈值。当推理功能启用时,该参数将被禁用。
|
||||
en_US: The probability threshold in nucleus sampling. When reasoning is enabled, this parameter will be disabled.
|
||||
- name: response_format
|
||||
use_template: response_format
|
||||
pricing:
|
||||
input: '0.001'
|
||||
output: '0.005'
|
||||
unit: '0.001'
|
||||
currency: USD
|
||||
@ -19,8 +19,8 @@ class GoogleProvider(ModelProvider):
|
||||
try:
|
||||
model_instance = self.get_model_instance(ModelType.LLM)
|
||||
|
||||
# Use `gemini-pro` model for validate,
|
||||
model_instance.validate_credentials(model="gemini-pro", credentials=credentials)
|
||||
# Use `gemini-2.0-flash` model for validate,
|
||||
model_instance.validate_credentials(model="gemini-2.0-flash", credentials=credentials)
|
||||
except CredentialsValidateFailedError as ex:
|
||||
raise ex
|
||||
except Exception as ex:
|
||||
|
||||
@ -19,5 +19,3 @@
|
||||
- gemini-exp-1206
|
||||
- gemini-exp-1121
|
||||
- gemini-exp-1114
|
||||
- gemini-pro
|
||||
- gemini-pro-vision
|
||||
|
||||
@ -1,35 +0,0 @@
|
||||
model: gemini-pro-vision
|
||||
label:
|
||||
en_US: Gemini Pro Vision
|
||||
model_type: llm
|
||||
features:
|
||||
- vision
|
||||
model_properties:
|
||||
mode: chat
|
||||
context_size: 12288
|
||||
parameter_rules:
|
||||
- name: temperature
|
||||
use_template: temperature
|
||||
- name: top_p
|
||||
use_template: top_p
|
||||
- name: top_k
|
||||
label:
|
||||
zh_Hans: 取样数量
|
||||
en_US: Top k
|
||||
type: int
|
||||
help:
|
||||
zh_Hans: 仅从每个后续标记的前 K 个选项中采样。
|
||||
en_US: Only sample from the top K options for each subsequent token.
|
||||
required: false
|
||||
- name: max_tokens_to_sample
|
||||
use_template: max_tokens
|
||||
required: true
|
||||
default: 4096
|
||||
min: 1
|
||||
max: 4096
|
||||
pricing:
|
||||
input: '0.00'
|
||||
output: '0.00'
|
||||
unit: '0.000001'
|
||||
currency: USD
|
||||
deprecated: true
|
||||
@ -1,39 +0,0 @@
|
||||
model: gemini-pro
|
||||
label:
|
||||
en_US: Gemini Pro
|
||||
model_type: llm
|
||||
features:
|
||||
- agent-thought
|
||||
- tool-call
|
||||
- stream-tool-call
|
||||
model_properties:
|
||||
mode: chat
|
||||
context_size: 30720
|
||||
parameter_rules:
|
||||
- name: temperature
|
||||
use_template: temperature
|
||||
- name: top_p
|
||||
use_template: top_p
|
||||
- name: top_k
|
||||
label:
|
||||
zh_Hans: 取样数量
|
||||
en_US: Top k
|
||||
type: int
|
||||
help:
|
||||
zh_Hans: 仅从每个后续标记的前 K 个选项中采样。
|
||||
en_US: Only sample from the top K options for each subsequent token.
|
||||
required: false
|
||||
- name: max_tokens_to_sample
|
||||
use_template: max_tokens
|
||||
required: true
|
||||
default: 2048
|
||||
min: 1
|
||||
max: 2048
|
||||
- name: response_format
|
||||
use_template: response_format
|
||||
pricing:
|
||||
input: '0.00'
|
||||
output: '0.00'
|
||||
unit: '0.000001'
|
||||
currency: USD
|
||||
deprecated: true
|
||||
@ -1,3 +1,4 @@
|
||||
- gpt-4.1
|
||||
- o1
|
||||
- o1-2024-12-17
|
||||
- o1-mini
|
||||
|
||||
@ -0,0 +1,60 @@
|
||||
model: gpt-4.1
|
||||
label:
|
||||
zh_Hans: gpt-4.1
|
||||
en_US: gpt-4.1
|
||||
model_type: llm
|
||||
features:
|
||||
- multi-tool-call
|
||||
- agent-thought
|
||||
- stream-tool-call
|
||||
- vision
|
||||
model_properties:
|
||||
mode: chat
|
||||
context_size: 1047576
|
||||
parameter_rules:
|
||||
- name: temperature
|
||||
use_template: temperature
|
||||
- name: top_p
|
||||
use_template: top_p
|
||||
- name: presence_penalty
|
||||
use_template: presence_penalty
|
||||
- name: frequency_penalty
|
||||
use_template: frequency_penalty
|
||||
- name: max_tokens
|
||||
use_template: max_tokens
|
||||
default: 512
|
||||
min: 1
|
||||
max: 32768
|
||||
- name: reasoning_effort
|
||||
label:
|
||||
zh_Hans: 推理工作
|
||||
en_US: Reasoning Effort
|
||||
type: string
|
||||
help:
|
||||
zh_Hans: 限制推理模型的推理工作
|
||||
en_US: Constrains effort on reasoning for reasoning models
|
||||
required: false
|
||||
options:
|
||||
- low
|
||||
- medium
|
||||
- high
|
||||
- name: response_format
|
||||
label:
|
||||
zh_Hans: 回复格式
|
||||
en_US: Response Format
|
||||
type: string
|
||||
help:
|
||||
zh_Hans: 指定模型必须输出的格式
|
||||
en_US: specifying the format that the model must output
|
||||
required: false
|
||||
options:
|
||||
- text
|
||||
- json_object
|
||||
- json_schema
|
||||
- name: json_schema
|
||||
use_template: json_schema
|
||||
pricing:
|
||||
input: '2.00'
|
||||
output: '8.00'
|
||||
unit: '0.000001'
|
||||
currency: USD
|
||||
@ -1057,7 +1057,7 @@ class OpenAILargeLanguageModel(_CommonOpenAI, LargeLanguageModel):
|
||||
model = "gpt-4o"
|
||||
|
||||
try:
|
||||
encoding = tiktoken.encoding_for_model(model)
|
||||
encoding = tiktoken.get_encoding(model)
|
||||
except KeyError:
|
||||
logger.warning("Warning: model not found. Using cl100k_base encoding.")
|
||||
model = "cl100k_base"
|
||||
|
||||
@ -5,11 +5,6 @@ model_type: llm
|
||||
features:
|
||||
- agent-thought
|
||||
- vision
|
||||
- tool-call
|
||||
- stream-tool-call
|
||||
- document
|
||||
- video
|
||||
- audio
|
||||
model_properties:
|
||||
mode: chat
|
||||
context_size: 1048576
|
||||
@ -20,20 +15,21 @@ parameter_rules:
|
||||
use_template: top_p
|
||||
- name: top_k
|
||||
label:
|
||||
zh_Hans: 取样数量
|
||||
en_US: Top k
|
||||
type: int
|
||||
help:
|
||||
zh_Hans: 仅从每个后续标记的前 K 个选项中采样。
|
||||
en_US: Only sample from the top K options for each subsequent token.
|
||||
required: false
|
||||
- name: presence_penalty
|
||||
use_template: presence_penalty
|
||||
- name: frequency_penalty
|
||||
use_template: frequency_penalty
|
||||
- name: max_output_tokens
|
||||
use_template: max_tokens
|
||||
required: true
|
||||
default: 8192
|
||||
min: 1
|
||||
max: 8192
|
||||
- name: json_schema
|
||||
use_template: json_schema
|
||||
pricing:
|
||||
input: '0.00'
|
||||
output: '0.00'
|
||||
|
||||
@ -195,7 +195,7 @@ class CodeNode(BaseNode[CodeNodeData]):
|
||||
if output_config.type == "object":
|
||||
# check if output is object
|
||||
if not isinstance(result.get(output_name), dict):
|
||||
if isinstance(result.get(output_name), type(None)):
|
||||
if result.get(output_name) is None:
|
||||
transformed_result[output_name] = None
|
||||
else:
|
||||
raise OutputValidationError(
|
||||
@ -223,7 +223,7 @@ class CodeNode(BaseNode[CodeNodeData]):
|
||||
elif output_config.type == "array[number]":
|
||||
# check if array of number available
|
||||
if not isinstance(result[output_name], list):
|
||||
if isinstance(result[output_name], type(None)):
|
||||
if result[output_name] is None:
|
||||
transformed_result[output_name] = None
|
||||
else:
|
||||
raise OutputValidationError(
|
||||
@ -244,7 +244,7 @@ class CodeNode(BaseNode[CodeNodeData]):
|
||||
elif output_config.type == "array[string]":
|
||||
# check if array of string available
|
||||
if not isinstance(result[output_name], list):
|
||||
if isinstance(result[output_name], type(None)):
|
||||
if result[output_name] is None:
|
||||
transformed_result[output_name] = None
|
||||
else:
|
||||
raise OutputValidationError(
|
||||
@ -265,7 +265,7 @@ class CodeNode(BaseNode[CodeNodeData]):
|
||||
elif output_config.type == "array[object]":
|
||||
# check if array of object available
|
||||
if not isinstance(result[output_name], list):
|
||||
if isinstance(result[output_name], type(None)):
|
||||
if result[output_name] is None:
|
||||
transformed_result[output_name] = None
|
||||
else:
|
||||
raise OutputValidationError(
|
||||
|
||||
@ -968,14 +968,12 @@ def _handle_memory_chat_mode(
|
||||
*,
|
||||
memory: TokenBufferMemory | None,
|
||||
memory_config: MemoryConfig | None,
|
||||
model_config: ModelConfigWithCredentialsEntity,
|
||||
model_config: ModelConfigWithCredentialsEntity, # TODO(-LAN-): Needs to remove
|
||||
) -> Sequence[PromptMessage]:
|
||||
memory_messages: Sequence[PromptMessage] = []
|
||||
# Get messages from memory for chat model
|
||||
if memory and memory_config:
|
||||
rest_tokens = _calculate_rest_token(prompt_messages=[], model_config=model_config)
|
||||
memory_messages = memory.get_history_prompt_messages(
|
||||
max_token_limit=rest_tokens,
|
||||
message_limit=memory_config.window.size if memory_config.window.enabled else None,
|
||||
)
|
||||
return memory_messages
|
||||
|
||||
@ -63,7 +63,6 @@ app_detail_fields = {
|
||||
"created_at": TimestampField,
|
||||
"updated_by": fields.String,
|
||||
"updated_at": TimestampField,
|
||||
"access_mode": fields.String,
|
||||
}
|
||||
|
||||
prompt_config_fields = {
|
||||
@ -99,7 +98,6 @@ app_partial_fields = {
|
||||
"updated_by": fields.String,
|
||||
"updated_at": TimestampField,
|
||||
"tags": fields.List(fields.Nested(tag_fields)),
|
||||
"access_mode": fields.String,
|
||||
}
|
||||
|
||||
|
||||
@ -172,7 +170,6 @@ app_detail_fields_with_site = {
|
||||
"updated_by": fields.String,
|
||||
"updated_at": TimestampField,
|
||||
"deleted_tools": fields.List(fields.String),
|
||||
"access_mode": fields.String,
|
||||
}
|
||||
|
||||
app_site_fields = {
|
||||
|
||||
66
api/poetry.lock
generated
66
api/poetry.lock
generated
@ -10473,44 +10473,44 @@ client = ["SQLAlchemy (>=1.4,<3)"]
|
||||
|
||||
[[package]]
|
||||
name = "tiktoken"
|
||||
version = "0.8.0"
|
||||
version = "0.9.0"
|
||||
description = "tiktoken is a fast BPE tokeniser for use with OpenAI's models"
|
||||
optional = false
|
||||
python-versions = ">=3.9"
|
||||
groups = ["main"]
|
||||
markers = "python_version == \"3.11\" or python_version >= \"3.12\""
|
||||
files = [
|
||||
{file = "tiktoken-0.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b07e33283463089c81ef1467180e3e00ab00d46c2c4bbcef0acab5f771d6695e"},
|
||||
{file = "tiktoken-0.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9269348cb650726f44dd3bbb3f9110ac19a8dcc8f54949ad3ef652ca22a38e21"},
|
||||
{file = "tiktoken-0.8.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25e13f37bc4ef2d012731e93e0fef21dc3b7aea5bb9009618de9a4026844e560"},
|
||||
{file = "tiktoken-0.8.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f13d13c981511331eac0d01a59b5df7c0d4060a8be1e378672822213da51e0a2"},
|
||||
{file = "tiktoken-0.8.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:6b2ddbc79a22621ce8b1166afa9f9a888a664a579350dc7c09346a3b5de837d9"},
|
||||
{file = "tiktoken-0.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:d8c2d0e5ba6453a290b86cd65fc51fedf247e1ba170191715b049dac1f628005"},
|
||||
{file = "tiktoken-0.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d622d8011e6d6f239297efa42a2657043aaed06c4f68833550cac9e9bc723ef1"},
|
||||
{file = "tiktoken-0.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2efaf6199717b4485031b4d6edb94075e4d79177a172f38dd934d911b588d54a"},
|
||||
{file = "tiktoken-0.8.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5637e425ce1fc49cf716d88df3092048359a4b3bbb7da762840426e937ada06d"},
|
||||
{file = "tiktoken-0.8.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fb0e352d1dbe15aba082883058b3cce9e48d33101bdaac1eccf66424feb5b47"},
|
||||
{file = "tiktoken-0.8.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:56edfefe896c8f10aba372ab5706b9e3558e78db39dd497c940b47bf228bc419"},
|
||||
{file = "tiktoken-0.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:326624128590def898775b722ccc327e90b073714227175ea8febbc920ac0a99"},
|
||||
{file = "tiktoken-0.8.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:881839cfeae051b3628d9823b2e56b5cc93a9e2efb435f4cf15f17dc45f21586"},
|
||||
{file = "tiktoken-0.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fe9399bdc3f29d428f16a2f86c3c8ec20be3eac5f53693ce4980371c3245729b"},
|
||||
{file = "tiktoken-0.8.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9a58deb7075d5b69237a3ff4bb51a726670419db6ea62bdcd8bd80c78497d7ab"},
|
||||
{file = "tiktoken-0.8.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2908c0d043a7d03ebd80347266b0e58440bdef5564f84f4d29fb235b5df3b04"},
|
||||
{file = "tiktoken-0.8.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:294440d21a2a51e12d4238e68a5972095534fe9878be57d905c476017bff99fc"},
|
||||
{file = "tiktoken-0.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:d8f3192733ac4d77977432947d563d7e1b310b96497acd3c196c9bddb36ed9db"},
|
||||
{file = "tiktoken-0.8.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:02be1666096aff7da6cbd7cdaa8e7917bfed3467cd64b38b1f112e96d3b06a24"},
|
||||
{file = "tiktoken-0.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c94ff53c5c74b535b2cbf431d907fc13c678bbd009ee633a2aca269a04389f9a"},
|
||||
{file = "tiktoken-0.8.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6b231f5e8982c245ee3065cd84a4712d64692348bc609d84467c57b4b72dcbc5"},
|
||||
{file = "tiktoken-0.8.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4177faa809bd55f699e88c96d9bb4635d22e3f59d635ba6fd9ffedf7150b9953"},
|
||||
{file = "tiktoken-0.8.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5376b6f8dc4753cd81ead935c5f518fa0fbe7e133d9e25f648d8c4dabdd4bad7"},
|
||||
{file = "tiktoken-0.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:18228d624807d66c87acd8f25fc135665617cab220671eb65b50f5d70fa51f69"},
|
||||
{file = "tiktoken-0.8.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7e17807445f0cf1f25771c9d86496bd8b5c376f7419912519699f3cc4dc5c12e"},
|
||||
{file = "tiktoken-0.8.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:886f80bd339578bbdba6ed6d0567a0d5c6cfe198d9e587ba6c447654c65b8edc"},
|
||||
{file = "tiktoken-0.8.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6adc8323016d7758d6de7313527f755b0fc6c72985b7d9291be5d96d73ecd1e1"},
|
||||
{file = "tiktoken-0.8.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b591fb2b30d6a72121a80be24ec7a0e9eb51c5500ddc7e4c2496516dd5e3816b"},
|
||||
{file = "tiktoken-0.8.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:845287b9798e476b4d762c3ebda5102be87ca26e5d2c9854002825d60cdb815d"},
|
||||
{file = "tiktoken-0.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:1473cfe584252dc3fa62adceb5b1c763c1874e04511b197da4e6de51d6ce5a02"},
|
||||
{file = "tiktoken-0.8.0.tar.gz", hash = "sha256:9ccbb2740f24542534369c5635cfd9b2b3c2490754a78ac8831d99f89f94eeb2"},
|
||||
{file = "tiktoken-0.9.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:586c16358138b96ea804c034b8acf3f5d3f0258bd2bc3b0227af4af5d622e382"},
|
||||
{file = "tiktoken-0.9.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d9c59ccc528c6c5dd51820b3474402f69d9a9e1d656226848ad68a8d5b2e5108"},
|
||||
{file = "tiktoken-0.9.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f0968d5beeafbca2a72c595e8385a1a1f8af58feaebb02b227229b69ca5357fd"},
|
||||
{file = "tiktoken-0.9.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:92a5fb085a6a3b7350b8fc838baf493317ca0e17bd95e8642f95fc69ecfed1de"},
|
||||
{file = "tiktoken-0.9.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:15a2752dea63d93b0332fb0ddb05dd909371ededa145fe6a3242f46724fa7990"},
|
||||
{file = "tiktoken-0.9.0-cp310-cp310-win_amd64.whl", hash = "sha256:26113fec3bd7a352e4b33dbaf1bd8948de2507e30bd95a44e2b1156647bc01b4"},
|
||||
{file = "tiktoken-0.9.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:f32cc56168eac4851109e9b5d327637f15fd662aa30dd79f964b7c39fbadd26e"},
|
||||
{file = "tiktoken-0.9.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:45556bc41241e5294063508caf901bf92ba52d8ef9222023f83d2483a3055348"},
|
||||
{file = "tiktoken-0.9.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:03935988a91d6d3216e2ec7c645afbb3d870b37bcb67ada1943ec48678e7ee33"},
|
||||
{file = "tiktoken-0.9.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8b3d80aad8d2c6b9238fc1a5524542087c52b860b10cbf952429ffb714bc1136"},
|
||||
{file = "tiktoken-0.9.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b2a21133be05dc116b1d0372af051cd2c6aa1d2188250c9b553f9fa49301b336"},
|
||||
{file = "tiktoken-0.9.0-cp311-cp311-win_amd64.whl", hash = "sha256:11a20e67fdf58b0e2dea7b8654a288e481bb4fc0289d3ad21291f8d0849915fb"},
|
||||
{file = "tiktoken-0.9.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e88f121c1c22b726649ce67c089b90ddda8b9662545a8aeb03cfef15967ddd03"},
|
||||
{file = "tiktoken-0.9.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a6600660f2f72369acb13a57fb3e212434ed38b045fd8cc6cdd74947b4b5d210"},
|
||||
{file = "tiktoken-0.9.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:95e811743b5dfa74f4b227927ed86cbc57cad4df859cb3b643be797914e41794"},
|
||||
{file = "tiktoken-0.9.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:99376e1370d59bcf6935c933cb9ba64adc29033b7e73f5f7569f3aad86552b22"},
|
||||
{file = "tiktoken-0.9.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:badb947c32739fb6ddde173e14885fb3de4d32ab9d8c591cbd013c22b4c31dd2"},
|
||||
{file = "tiktoken-0.9.0-cp312-cp312-win_amd64.whl", hash = "sha256:5a62d7a25225bafed786a524c1b9f0910a1128f4232615bf3f8257a73aaa3b16"},
|
||||
{file = "tiktoken-0.9.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2b0e8e05a26eda1249e824156d537015480af7ae222ccb798e5234ae0285dbdb"},
|
||||
{file = "tiktoken-0.9.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:27d457f096f87685195eea0165a1807fae87b97b2161fe8c9b1df5bd74ca6f63"},
|
||||
{file = "tiktoken-0.9.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cf8ded49cddf825390e36dd1ad35cd49589e8161fdcb52aa25f0583e90a3e01"},
|
||||
{file = "tiktoken-0.9.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc156cb314119a8bb9748257a2eaebd5cc0753b6cb491d26694ed42fc7cb3139"},
|
||||
{file = "tiktoken-0.9.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:cd69372e8c9dd761f0ab873112aba55a0e3e506332dd9f7522ca466e817b1b7a"},
|
||||
{file = "tiktoken-0.9.0-cp313-cp313-win_amd64.whl", hash = "sha256:5ea0edb6f83dc56d794723286215918c1cde03712cbbafa0348b33448faf5b95"},
|
||||
{file = "tiktoken-0.9.0-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:c6386ca815e7d96ef5b4ac61e0048cd32ca5a92d5781255e13b31381d28667dc"},
|
||||
{file = "tiktoken-0.9.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:75f6d5db5bc2c6274b674ceab1615c1778e6416b14705827d19b40e6355f03e0"},
|
||||
{file = "tiktoken-0.9.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e15b16f61e6f4625a57a36496d28dd182a8a60ec20a534c5343ba3cafa156ac7"},
|
||||
{file = "tiktoken-0.9.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ebcec91babf21297022882344c3f7d9eed855931466c3311b1ad6b64befb3df"},
|
||||
{file = "tiktoken-0.9.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:e5fd49e7799579240f03913447c0cdfa1129625ebd5ac440787afc4345990427"},
|
||||
{file = "tiktoken-0.9.0-cp39-cp39-win_amd64.whl", hash = "sha256:26242ca9dc8b58e875ff4ca078b9a94d2f0813e6a535dcd2205df5d49d927cc7"},
|
||||
{file = "tiktoken-0.9.0.tar.gz", hash = "sha256:d02a5ca6a938e0490e1ff957bc48c8b078c88cb83977be1625b1fd8aac792c5d"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@ -12389,4 +12389,4 @@ cffi = ["cffi (>=1.11)"]
|
||||
[metadata]
|
||||
lock-version = "2.1"
|
||||
python-versions = ">=3.11,<3.13"
|
||||
content-hash = "d197cdff507a70323c1d6aca11609188f54970f67715af744fe6def15b7776fd"
|
||||
content-hash = "0df8aef68385b6596306fd18af317a835023d648eb5028cd57ec463f176e4c0f"
|
||||
|
||||
@ -85,7 +85,7 @@ sentry-sdk = { version = "~1.44.1", extras = ["flask"] }
|
||||
sqlalchemy = "~2.0.29"
|
||||
starlette = "0.41.0"
|
||||
tencentcloud-sdk-python-hunyuan = "~3.0.1294"
|
||||
tiktoken = "~0.8.0"
|
||||
tiktoken = "^0.9.0"
|
||||
tokenizers = "~0.15.0"
|
||||
transformers = "~4.35.0"
|
||||
unstructured = { version = "~0.16.1", extras = ["docx", "epub", "md", "msg", "ppt", "pptx"] }
|
||||
|
||||
@ -55,13 +55,19 @@ def _check_version_compatibility(imported_version: str) -> ImportStatus:
|
||||
except version.InvalidVersion:
|
||||
return ImportStatus.FAILED
|
||||
|
||||
# Compare major version and minor version
|
||||
if current_ver.major != imported_ver.major or current_ver.minor != imported_ver.minor:
|
||||
# If imported version is newer than current, always return PENDING
|
||||
if imported_ver > current_ver:
|
||||
return ImportStatus.PENDING
|
||||
|
||||
if current_ver.micro != imported_ver.micro:
|
||||
# If imported version is older than current's major, return PENDING
|
||||
if imported_ver.major < current_ver.major:
|
||||
return ImportStatus.PENDING
|
||||
|
||||
# If imported version is older than current's minor, return COMPLETED_WITH_WARNINGS
|
||||
if imported_ver.minor < current_ver.minor:
|
||||
return ImportStatus.COMPLETED_WITH_WARNINGS
|
||||
|
||||
# If imported version equals or is older than current's micro, return COMPLETED
|
||||
return ImportStatus.COMPLETED
|
||||
|
||||
|
||||
|
||||
@ -19,10 +19,8 @@ from core.tools.utils.configuration import ToolParameterConfigurationManager
|
||||
from events.app_event import app_was_created
|
||||
from extensions.ext_database import db
|
||||
from models.account import Account
|
||||
from models.model import App, AppMode, AppModelConfig, Site
|
||||
from models.model import App, AppMode, AppModelConfig
|
||||
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 tasks.remove_app_and_related_data_task import remove_app_and_related_data_task
|
||||
|
||||
@ -154,10 +152,6 @@ class AppService:
|
||||
|
||||
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
|
||||
|
||||
def get_app(self, app: App) -> App:
|
||||
@ -314,10 +308,6 @@ class AppService:
|
||||
db.session.delete(app)
|
||||
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
|
||||
remove_app_and_related_data_task.delay(tenant_id=app.tenant_id, app_id=app.id)
|
||||
|
||||
@ -384,15 +374,3 @@ class AppService:
|
||||
meta["tool_icons"][tool_name] = {"background": "#252525", "content": "\ud83d\ude01"}
|
||||
|
||||
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,87 +1,11 @@
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
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:
|
||||
@classmethod
|
||||
def get_info(cls):
|
||||
return EnterpriseRequest.send_request("GET", "/info")
|
||||
|
||||
class WebAppAuth:
|
||||
@classmethod
|
||||
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)
|
||||
@classmethod
|
||||
def get_app_web_sso_enabled(cls, app_code):
|
||||
return EnterpriseRequest.send_request("GET", f"/app-sso-setting?appCode={app_code}")
|
||||
|
||||
@ -1,18 +0,0 @@
|
||||
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,26 +36,6 @@ class LicenseModel(BaseModel):
|
||||
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):
|
||||
billing: BillingModel = BillingModel()
|
||||
members: LimitationModel = LimitationModel(size=0, limit=1)
|
||||
@ -67,7 +47,6 @@ class FeatureModel(BaseModel):
|
||||
can_replace_logo: bool = False
|
||||
model_load_balancing_enabled: bool = False
|
||||
dataset_operator_enabled: bool = False
|
||||
webapp_copyright_enabled: bool = False
|
||||
|
||||
# pydantic configs
|
||||
model_config = ConfigDict(protected_namespaces=())
|
||||
@ -76,6 +55,9 @@ class FeatureModel(BaseModel):
|
||||
class SystemFeatureModel(BaseModel):
|
||||
sso_enforced_for_signin: bool = False
|
||||
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_password_login: bool = True
|
||||
enable_social_oauth_login: bool = False
|
||||
@ -83,8 +65,6 @@ class SystemFeatureModel(BaseModel):
|
||||
is_allow_create_workspace: bool = False
|
||||
is_email_setup: bool = False
|
||||
license: LicenseModel = LicenseModel()
|
||||
branding: BrandingModel = BrandingModel()
|
||||
webapp_auth: WebAppAuthModel = WebAppAuthModel()
|
||||
|
||||
|
||||
class FeatureService:
|
||||
@ -97,9 +77,6 @@ class FeatureService:
|
||||
if dify_config.BILLING_ENABLED and tenant_id:
|
||||
cls._fulfill_params_from_billing_api(features, tenant_id)
|
||||
|
||||
if dify_config.ENTERPRISE_ENABLED:
|
||||
features.webapp_copyright_enabled = True
|
||||
|
||||
return features
|
||||
|
||||
@classmethod
|
||||
@ -109,8 +86,8 @@ class FeatureService:
|
||||
cls._fulfill_system_params_from_env(system_features)
|
||||
|
||||
if dify_config.ENTERPRISE_ENABLED:
|
||||
system_features.branding.enabled = True
|
||||
system_features.webapp_auth.enabled = True
|
||||
system_features.enable_web_sso_switch_component = True
|
||||
|
||||
cls._fulfill_params_from_enterprise(system_features)
|
||||
|
||||
return system_features
|
||||
@ -138,9 +115,6 @@ class FeatureService:
|
||||
features.billing.subscription.plan = billing_info["subscription"]["plan"]
|
||||
features.billing.subscription.interval = billing_info["subscription"]["interval"]
|
||||
|
||||
if features.billing.subscription.plan != "sandbox":
|
||||
features.webapp_copyright_enabled = True
|
||||
|
||||
if "members" in billing_info:
|
||||
features.members.size = billing_info["members"]["size"]
|
||||
features.members.limit = billing_info["members"]["limit"]
|
||||
@ -171,45 +145,38 @@ class FeatureService:
|
||||
features.model_load_balancing_enabled = billing_info["model_load_balancing_enabled"]
|
||||
|
||||
@classmethod
|
||||
def _fulfill_params_from_enterprise(cls, features: SystemFeatureModel):
|
||||
def _fulfill_params_from_enterprise(cls, features):
|
||||
enterprise_info = EnterpriseService.get_info()
|
||||
|
||||
if "SSOEnforcedForSignin" in enterprise_info:
|
||||
features.sso_enforced_for_signin = enterprise_info["SSOEnforcedForSignin"]
|
||||
if "sso_enforced_for_signin" in enterprise_info:
|
||||
features.sso_enforced_for_signin = enterprise_info["sso_enforced_for_signin"]
|
||||
|
||||
if "EnableEmailCodeLogin" in enterprise_info:
|
||||
features.enable_email_code_login = enterprise_info["EnableEmailCodeLogin"]
|
||||
if "sso_enforced_for_signin_protocol" in enterprise_info:
|
||||
features.sso_enforced_for_signin_protocol = enterprise_info["sso_enforced_for_signin_protocol"]
|
||||
|
||||
if "EnableEmailPasswordLogin" in enterprise_info:
|
||||
features.enable_email_password_login = enterprise_info["EnableEmailPasswordLogin"]
|
||||
if "sso_enforced_for_web" in enterprise_info:
|
||||
features.sso_enforced_for_web = enterprise_info["sso_enforced_for_web"]
|
||||
|
||||
if "IsAllowRegister" in enterprise_info:
|
||||
features.is_allow_register = enterprise_info["IsAllowRegister"]
|
||||
if "sso_enforced_for_web_protocol" in enterprise_info:
|
||||
features.sso_enforced_for_web_protocol = enterprise_info["sso_enforced_for_web_protocol"]
|
||||
|
||||
if "IsAllowCreateWorkspace" in enterprise_info:
|
||||
features.is_allow_create_workspace = enterprise_info["IsAllowCreateWorkspace"]
|
||||
if "enable_email_code_login" in enterprise_info:
|
||||
features.enable_email_code_login = enterprise_info["enable_email_code_login"]
|
||||
|
||||
if "Branding" in enterprise_info:
|
||||
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 "enable_email_password_login" in enterprise_info:
|
||||
features.enable_email_password_login = enterprise_info["enable_email_password_login"]
|
||||
|
||||
if "WebAppAuth" in enterprise_info:
|
||||
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_register" in enterprise_info:
|
||||
features.is_allow_register = enterprise_info["is_allow_register"]
|
||||
|
||||
if "License" in enterprise_info:
|
||||
license_info = enterprise_info["License"]
|
||||
if "is_allow_create_workspace" in enterprise_info:
|
||||
features.is_allow_create_workspace = enterprise_info["is_allow_create_workspace"]
|
||||
|
||||
if "license" in enterprise_info:
|
||||
license_info = enterprise_info["license"]
|
||||
|
||||
if "status" in license_info:
|
||||
features.license.status = LicenseStatus(license_info.get("status", LicenseStatus.INACTIVE))
|
||||
|
||||
if "expiredAt" in license_info:
|
||||
features.license.expired_at = license_info["expiredAt"]
|
||||
if "expired_at" in license_info:
|
||||
features.license.expired_at = license_info["expired_at"]
|
||||
|
||||
@ -1,137 +0,0 @@
|
||||
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,7 +6,6 @@ from celery import shared_task # type: ignore
|
||||
from flask import render_template
|
||||
|
||||
from extensions.ext_mail import mail
|
||||
from services.feature_service import FeatureService
|
||||
|
||||
|
||||
@shared_task(queue="mail")
|
||||
@ -26,24 +25,10 @@ def send_email_code_login_mail_task(language: str, to: str, code: str):
|
||||
# send email code login mail using different languages
|
||||
try:
|
||||
if language == "zh-Hans":
|
||||
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)
|
||||
html_content = render_template("email_code_login_mail_template_zh-CN.html", to=to, code=code)
|
||||
mail.send(to=to, subject="邮箱验证码", html=html_content)
|
||||
else:
|
||||
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)
|
||||
html_content = render_template("email_code_login_mail_template_en-US.html", to=to, code=code)
|
||||
mail.send(to=to, subject="Email Code", html=html_content)
|
||||
|
||||
end_at = time.perf_counter()
|
||||
|
||||
@ -1,33 +0,0 @@
|
||||
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,7 +7,6 @@ from flask import render_template
|
||||
|
||||
from configs import dify_config
|
||||
from extensions.ext_mail import mail
|
||||
from services.feature_service import FeatureService
|
||||
|
||||
|
||||
@shared_task(queue="mail")
|
||||
@ -34,45 +33,23 @@ def send_invite_member_mail_task(language: str, to: str, token: str, inviter_nam
|
||||
try:
|
||||
url = f"{dify_config.CONSOLE_WEB_URL}/activate?token={token}"
|
||||
if language == "zh-Hans":
|
||||
template = "invite_member_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/invite_member_mail_template_zh-CN.html"
|
||||
html_content = render_template(
|
||||
template,
|
||||
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)
|
||||
html_content = render_template(
|
||||
"invite_member_mail_template_zh-CN.html",
|
||||
to=to,
|
||||
inviter_name=inviter_name,
|
||||
workspace_name=workspace_name,
|
||||
url=url,
|
||||
)
|
||||
mail.send(to=to, subject="立即加入 Dify 工作空间", html=html_content)
|
||||
else:
|
||||
template = "invite_member_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/invite_member_mail_template_en-US.html"
|
||||
html_content = render_template(
|
||||
template,
|
||||
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)
|
||||
html_content = render_template(
|
||||
"invite_member_mail_template_en-US.html",
|
||||
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()
|
||||
logging.info(
|
||||
|
||||
@ -6,7 +6,6 @@ from celery import shared_task # type: ignore
|
||||
from flask import render_template
|
||||
|
||||
from extensions.ext_mail import mail
|
||||
from services.feature_service import FeatureService
|
||||
|
||||
|
||||
@shared_task(queue="mail")
|
||||
@ -26,27 +25,11 @@ def send_reset_password_mail_task(language: str, to: str, code: str):
|
||||
# send reset password mail using different languages
|
||||
try:
|
||||
if language == "zh-Hans":
|
||||
template = "reset_password_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/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)
|
||||
html_content = render_template("reset_password_mail_template_zh-CN.html", to=to, code=code)
|
||||
mail.send(to=to, subject="设置您的 Dify 密码", html=html_content)
|
||||
else:
|
||||
template = "reset_password_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/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)
|
||||
html_content = render_template("reset_password_mail_template_en-US.html", to=to, code=code)
|
||||
mail.send(to=to, subject="Set Your Dify Password", html=html_content)
|
||||
|
||||
end_at = time.perf_counter()
|
||||
logging.info(
|
||||
|
||||
@ -1,70 +0,0 @@
|
||||
<!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>
|
||||
@ -1,70 +0,0 @@
|
||||
<!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>
|
||||
@ -1,69 +0,0 @@
|
||||
<!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>
|
||||
@ -1,69 +0,0 @@
|
||||
<!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>
|
||||
@ -1,70 +0,0 @@
|
||||
<!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>
|
||||
@ -1,70 +0,0 @@
|
||||
<!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>
|
||||
@ -932,3 +932,6 @@ MAX_SUBMIT_COUNT=100
|
||||
|
||||
# The maximum number of top-k value for RAG.
|
||||
TOP_K_MAX_VALUE=10
|
||||
|
||||
# Prevent Clickjacking
|
||||
ALLOW_EMBED=false
|
||||
@ -1,8 +1,8 @@
|
||||
x-shared-env: &shared-api-worker-env
|
||||
x-shared-env: &shared-api-worker-env
|
||||
services:
|
||||
# API service
|
||||
api:
|
||||
image: langgenius/dify-api:0.15.4
|
||||
image: langgenius/dify-api:0.15.7
|
||||
restart: always
|
||||
environment:
|
||||
# Use the shared environment variables.
|
||||
@ -25,7 +25,7 @@ services:
|
||||
# worker service
|
||||
# The Celery worker for processing the queue.
|
||||
worker:
|
||||
image: langgenius/dify-api:0.15.4
|
||||
image: langgenius/dify-api:0.15.7
|
||||
restart: always
|
||||
environment:
|
||||
# Use the shared environment variables.
|
||||
@ -47,7 +47,7 @@ services:
|
||||
|
||||
# Frontend web application.
|
||||
web:
|
||||
image: langgenius/dify-web:0.15.4
|
||||
image: langgenius/dify-web:0.15.7
|
||||
restart: always
|
||||
environment:
|
||||
CONSOLE_API_URL: ${CONSOLE_API_URL:-}
|
||||
@ -56,6 +56,7 @@ services:
|
||||
NEXT_TELEMETRY_DISABLED: ${NEXT_TELEMETRY_DISABLED:-0}
|
||||
TEXT_GENERATION_TIMEOUT_MS: ${TEXT_GENERATION_TIMEOUT_MS:-60000}
|
||||
CSP_WHITELIST: ${CSP_WHITELIST:-}
|
||||
ALLOW_EMBED: ${ALLOW_EMBED:-false}
|
||||
TOP_K_MAX_VALUE: ${TOP_K_MAX_VALUE:-}
|
||||
INDEXING_MAX_SEGMENTATION_TOKENS_LENGTH: ${INDEXING_MAX_SEGMENTATION_TOKENS_LENGTH:-}
|
||||
|
||||
|
||||
@ -389,11 +389,12 @@ x-shared-env: &shared-api-worker-env
|
||||
CREATE_TIDB_SERVICE_JOB_ENABLED: ${CREATE_TIDB_SERVICE_JOB_ENABLED:-false}
|
||||
MAX_SUBMIT_COUNT: ${MAX_SUBMIT_COUNT:-100}
|
||||
TOP_K_MAX_VALUE: ${TOP_K_MAX_VALUE:-10}
|
||||
ALLOW_EMBED: ${ALLOW_EMBED:-false}
|
||||
|
||||
services:
|
||||
# API service
|
||||
api:
|
||||
image: langgenius/dify-api:0.15.4
|
||||
image: langgenius/dify-api:0.15.7
|
||||
restart: always
|
||||
environment:
|
||||
# Use the shared environment variables.
|
||||
@ -416,7 +417,7 @@ services:
|
||||
# worker service
|
||||
# The Celery worker for processing the queue.
|
||||
worker:
|
||||
image: langgenius/dify-api:0.15.4
|
||||
image: langgenius/dify-api:0.15.7
|
||||
restart: always
|
||||
environment:
|
||||
# Use the shared environment variables.
|
||||
@ -438,7 +439,7 @@ services:
|
||||
|
||||
# Frontend web application.
|
||||
web:
|
||||
image: langgenius/dify-web:0.15.4
|
||||
image: langgenius/dify-web:0.15.7
|
||||
restart: always
|
||||
environment:
|
||||
CONSOLE_API_URL: ${CONSOLE_API_URL:-}
|
||||
@ -447,6 +448,7 @@ services:
|
||||
NEXT_TELEMETRY_DISABLED: ${NEXT_TELEMETRY_DISABLED:-0}
|
||||
TEXT_GENERATION_TIMEOUT_MS: ${TEXT_GENERATION_TIMEOUT_MS:-60000}
|
||||
CSP_WHITELIST: ${CSP_WHITELIST:-}
|
||||
ALLOW_EMBED: ${ALLOW_EMBED:-false}
|
||||
TOP_K_MAX_VALUE: ${TOP_K_MAX_VALUE:-}
|
||||
INDEXING_MAX_SEGMENTATION_TOKENS_LENGTH: ${INDEXING_MAX_SEGMENTATION_TOKENS_LENGTH:-}
|
||||
|
||||
|
||||
@ -31,3 +31,6 @@ NEXT_PUBLIC_TOP_K_MAX_VALUE=10
|
||||
|
||||
# The maximum number of tokens for segmentation
|
||||
NEXT_PUBLIC_INDEXING_MAX_SEGMENTATION_TOKENS_LENGTH=4000
|
||||
|
||||
# Default is not allow to embed into iframe to prevent Clickjacking: https://owasp.org/www-community/attacks/Clickjacking
|
||||
NEXT_PUBLIC_ALLOW_EMBED=
|
||||
|
||||
@ -15,17 +15,17 @@ import {
|
||||
} from '@remixicon/react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useShallow } from 'zustand/react/shallow'
|
||||
import { useContextSelector } from 'use-context-selector'
|
||||
import s from './style.module.css'
|
||||
import cn from '@/utils/classnames'
|
||||
import { useStore } from '@/app/components/app/store'
|
||||
import AppSideBar from '@/app/components/app-sidebar'
|
||||
import type { NavIcon } from '@/app/components/app-sidebar/navLink'
|
||||
import { fetchAppDetail, fetchAppSSO } from '@/service/apps'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import AppContext, { useAppContext } from '@/context/app-context'
|
||||
import Loading from '@/app/components/base/loading'
|
||||
import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
|
||||
import type { App } from '@/types/app'
|
||||
import { useGlobalPublicStore } from '@/context/global-public-context'
|
||||
|
||||
export type IAppDetailLayoutProps = {
|
||||
children: React.ReactNode
|
||||
@ -56,7 +56,7 @@ const AppDetailLayout: FC<IAppDetailLayoutProps> = (props) => {
|
||||
icon: NavIcon
|
||||
selectedIcon: NavIcon
|
||||
}>>([])
|
||||
const { systemFeatures } = useGlobalPublicStore()
|
||||
const systemFeatures = useContextSelector(AppContext, state => state.systemFeatures)
|
||||
|
||||
const getNavigations = useCallback((appId: string, isCurrentWorkspaceEditor: boolean, mode: string) => {
|
||||
const navs = [
|
||||
@ -98,11 +98,7 @@ const AppDetailLayout: FC<IAppDetailLayoutProps> = (props) => {
|
||||
|
||||
useEffect(() => {
|
||||
if (appDetail) {
|
||||
if (systemFeatures.branding.enabled)
|
||||
document.title = `${(appDetail.name || 'App')} - ${systemFeatures.branding.application_title}`
|
||||
else
|
||||
document.title = `${(appDetail.name || 'App')} - Dify`
|
||||
|
||||
document.title = `${(appDetail.name || 'App')} - Dify`
|
||||
const localeMode = localStorage.getItem('app-detail-collapse-or-expand') || 'expand'
|
||||
const mode = isMobile ? 'collapse' : 'expand'
|
||||
setAppSiderbarExpand(isMobile ? mode : localeMode)
|
||||
@ -110,7 +106,7 @@ const AppDetailLayout: FC<IAppDetailLayoutProps> = (props) => {
|
||||
// if ((appDetail.mode === 'advanced-chat' || appDetail.mode === 'workflow') && (pathname).endsWith('workflow'))
|
||||
// setAppSiderbarExpand('collapse')
|
||||
}
|
||||
}, [appDetail, isMobile, pathname, setAppSiderbarExpand, systemFeatures])
|
||||
}, [appDetail, isMobile])
|
||||
|
||||
useEffect(() => {
|
||||
setAppDetail()
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import { useContext, useContextSelector } from 'use-context-selector'
|
||||
import AppCard from '@/app/components/app/overview/appCard'
|
||||
import Loading from '@/app/components/base/loading'
|
||||
import { ToastContext } from '@/app/components/base/toast'
|
||||
@ -20,7 +20,7 @@ import { asyncRunSafe } from '@/utils'
|
||||
import { NEED_REFRESH_APP_LIST_KEY } from '@/config'
|
||||
import type { IAppCardProps } from '@/app/components/app/overview/appCard'
|
||||
import { useStore as useAppStore } from '@/app/components/app/store'
|
||||
import { useGlobalPublicStore } from '@/context/global-public-context'
|
||||
import AppContext from '@/context/app-context'
|
||||
|
||||
export type ICardViewProps = {
|
||||
appId: string
|
||||
@ -31,7 +31,7 @@ const CardView: FC<ICardViewProps> = ({ appId }) => {
|
||||
const { notify } = useContext(ToastContext)
|
||||
const appDetail = useAppStore(state => state.appDetail)
|
||||
const setAppDetail = useAppStore(state => state.setAppDetail)
|
||||
const { systemFeatures } = useGlobalPublicStore()
|
||||
const systemFeatures = useContextSelector(AppContext, state => state.systemFeatures)
|
||||
|
||||
const updateAppDetail = async () => {
|
||||
try {
|
||||
|
||||
@ -2,9 +2,7 @@
|
||||
import type { FC } from 'react'
|
||||
import React, { useEffect } from 'react'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import useDocumentTitle from '@/hooks/use-document-title'
|
||||
|
||||
export type IAppDetail = {
|
||||
children: React.ReactNode
|
||||
@ -13,13 +11,11 @@ export type IAppDetail = {
|
||||
const AppDetail: FC<IAppDetail> = ({ children }) => {
|
||||
const router = useRouter()
|
||||
const { isCurrentWorkspaceDatasetOperator } = useAppContext()
|
||||
const { t } = useTranslation()
|
||||
useDocumentTitle(t('common.menus.appDetail'))
|
||||
|
||||
useEffect(() => {
|
||||
if (isCurrentWorkspaceDatasetOperator)
|
||||
return router.replace('/datasets')
|
||||
}, [isCurrentWorkspaceDatasetOperator, router])
|
||||
}, [isCurrentWorkspaceDatasetOperator])
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
@ -85,6 +85,7 @@ const Apps = () => {
|
||||
]
|
||||
|
||||
useEffect(() => {
|
||||
document.title = `${t('common.menus.apps')} - Dify`
|
||||
if (localStorage.getItem(NEED_REFRESH_APP_LIST_KEY) === '1') {
|
||||
localStorage.removeItem(NEED_REFRESH_APP_LIST_KEY)
|
||||
mutate()
|
||||
|
||||
@ -1,20 +1,21 @@
|
||||
'use client'
|
||||
import { useContextSelector } from 'use-context-selector'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { RiDiscordFill, RiGithubFill } from '@remixicon/react'
|
||||
import Link from 'next/link'
|
||||
import style from '../list.module.css'
|
||||
import Apps from './Apps'
|
||||
import { useGlobalPublicStore } from '@/context/global-public-context'
|
||||
import useDocumentTitle from '@/hooks/use-document-title'
|
||||
import AppContext from '@/context/app-context'
|
||||
import { LicenseStatus } from '@/types/feature'
|
||||
|
||||
const AppList = () => {
|
||||
const { t } = useTranslation()
|
||||
const { systemFeatures } = useGlobalPublicStore()
|
||||
useDocumentTitle(t('common.menus.apps'))
|
||||
const systemFeatures = useContextSelector(AppContext, v => v.systemFeatures)
|
||||
|
||||
return (
|
||||
<div className='relative flex flex-col overflow-y-auto bg-background-body shrink-0 h-0 grow'>
|
||||
<Apps />
|
||||
{!systemFeatures.branding.enabled && <footer className='px-12 py-6 grow-0 shrink-0'>
|
||||
{systemFeatures.license.status === LicenseStatus.NONE && <footer className='px-12 py-6 grow-0 shrink-0'>
|
||||
<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>
|
||||
<div className='flex items-center gap-2 mt-3'>
|
||||
|
||||
@ -31,7 +31,6 @@ import { getLocaleOnClient } from '@/i18n'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import LinkedAppsPanel from '@/app/components/base/linked-apps-panel'
|
||||
import useDocumentTitle from '@/hooks/use-document-title'
|
||||
|
||||
export type IAppDetailLayoutProps = {
|
||||
children: React.ReactNode
|
||||
@ -187,7 +186,11 @@ const DatasetDetailLayout: FC<IAppDetailLayoutProps> = (props) => {
|
||||
}
|
||||
return baseNavigation
|
||||
}, [datasetRes?.provider, datasetId, t])
|
||||
useDocumentTitle(`${datasetRes?.name || 'Dataset'}`)
|
||||
|
||||
useEffect(() => {
|
||||
if (datasetRes)
|
||||
document.title = `${datasetRes.name || 'Dataset'} - Dify`
|
||||
}, [datasetRes])
|
||||
|
||||
const setAppSiderbarExpand = useStore(state => state.setAppSiderbarExpand)
|
||||
|
||||
|
||||
@ -29,11 +29,9 @@ import { useTabSearchParams } from '@/hooks/use-tab-searchparams'
|
||||
import { useStore as useTagStore } from '@/app/components/base/tag-management/store'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import { useExternalApiPanel } from '@/context/external-api-panel-context'
|
||||
import { useGlobalPublicStore } from '@/context/global-public-context'
|
||||
|
||||
const Container = () => {
|
||||
const { t } = useTranslation()
|
||||
const { systemFeatures } = useGlobalPublicStore()
|
||||
const router = useRouter()
|
||||
const { currentWorkspace, isCurrentWorkspaceOwner } = useAppContext()
|
||||
const showTagManagementModal = useTagStore(s => s.showTagManagementModal)
|
||||
@ -125,7 +123,7 @@ const Container = () => {
|
||||
{activeTab === 'dataset' && (
|
||||
<>
|
||||
<Datasets containerRef={containerRef} tags={tagIDs} keywords={searchKeywords} includeAll={includeAll} />
|
||||
{!systemFeatures.branding.enabled && <DatasetFooter />}
|
||||
<DatasetFooter />
|
||||
{showTagManagementModal && (
|
||||
<TagManagementModal type='knowledge' show={showTagManagementModal} />
|
||||
)}
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
import { useEffect, useRef } from 'react'
|
||||
import useSWRInfinite from 'swr/infinite'
|
||||
import { debounce } from 'lodash-es'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import NewDatasetCard from './NewDatasetCard'
|
||||
import DatasetCard from './DatasetCard'
|
||||
import type { DataSetListResponse, FetchDatasetsParams } from '@/models/datasets'
|
||||
@ -56,8 +57,11 @@ const Datasets = ({
|
||||
const loadingStateRef = useRef(false)
|
||||
const anchorRef = useRef<HTMLAnchorElement>(null)
|
||||
|
||||
const { t } = useTranslation()
|
||||
|
||||
useEffect(() => {
|
||||
loadingStateRef.current = isLoading
|
||||
document.title = `${t('dataset.knowledge')} - Dify`
|
||||
}, [isLoading])
|
||||
|
||||
useEffect(() => {
|
||||
@ -76,7 +80,7 @@ const Datasets = ({
|
||||
|
||||
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'>
|
||||
{isCurrentWorkspaceEditor && <NewDatasetCard ref={anchorRef} />}
|
||||
{ isCurrentWorkspaceEditor && <NewDatasetCard ref={anchorRef} /> }
|
||||
{data?.map(({ data: datasets }) => datasets.map(dataset => (
|
||||
<DatasetCard key={dataset.id} dataset={dataset} onSuccess={mutate} />),
|
||||
))}
|
||||
|
||||
@ -1,12 +1,11 @@
|
||||
'use client'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Container from './Container'
|
||||
import useDocumentTitle from '@/hooks/use-document-title'
|
||||
|
||||
const AppList = () => {
|
||||
const { t } = useTranslation()
|
||||
useDocumentTitle(t('common.menus.datasets'))
|
||||
const AppList = async () => {
|
||||
return <Container />
|
||||
}
|
||||
|
||||
export const metadata = {
|
||||
title: 'Datasets - Dify',
|
||||
}
|
||||
|
||||
export default AppList
|
||||
|
||||
@ -6,7 +6,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty, PropertyInstructi
|
||||
<div>
|
||||
### Authentication
|
||||
|
||||
Service API authenticates using an `API-Key`.
|
||||
Service API of Dify 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.
|
||||
|
||||
|
||||
@ -6,7 +6,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty, PropertyInstructi
|
||||
<div>
|
||||
### 鉴权
|
||||
|
||||
Service API 使用 `API-Key` 进行鉴权。
|
||||
Dify Service API 使用 `API-Key` 进行鉴权。
|
||||
|
||||
建议开发者把 `API-Key` 放在后端存储,而非分享或者放在客户端存储,以免 `API-Key` 泄露,导致财产损失。
|
||||
|
||||
|
||||
@ -1,13 +1,11 @@
|
||||
'use client'
|
||||
import type { FC, PropsWithChildren } from 'react'
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import ExploreClient from '@/app/components/explore'
|
||||
import useDocumentTitle from '@/hooks/use-document-title'
|
||||
export type IAppDetail = {
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
const ExploreLayout: FC<PropsWithChildren> = ({ children }) => {
|
||||
const { t } = useTranslation()
|
||||
useDocumentTitle(t('common.menus.explore'))
|
||||
const AppDetail: FC<IAppDetail> = ({ children }) => {
|
||||
return (
|
||||
<ExploreClient>
|
||||
{children}
|
||||
@ -15,4 +13,4 @@ const ExploreLayout: FC<PropsWithChildren> = ({ children }) => {
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(ExploreLayout)
|
||||
export default React.memo(AppDetail)
|
||||
|
||||
@ -30,4 +30,9 @@ const Layout = ({ children }: { children: ReactNode }) => {
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export const metadata = {
|
||||
title: 'Dify',
|
||||
}
|
||||
|
||||
export default Layout
|
||||
|
||||
@ -1,16 +1,22 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import React, { useEffect } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import React, { useEffect } from 'react'
|
||||
import ToolProviderList from '@/app/components/tools/provider-list'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import useDocumentTitle from '@/hooks/use-document-title'
|
||||
const ToolsList: FC = () => {
|
||||
|
||||
const Layout: FC = () => {
|
||||
const { t } = useTranslation()
|
||||
const router = useRouter()
|
||||
const { isCurrentWorkspaceDatasetOperator } = useAppContext()
|
||||
const { t } = useTranslation()
|
||||
useDocumentTitle(t('common.menus.tools'))
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof window !== 'undefined')
|
||||
document.title = `${t('tools.title')} - Dify`
|
||||
if (isCurrentWorkspaceDatasetOperator)
|
||||
return router.replace('/datasets')
|
||||
}, [isCurrentWorkspaceDatasetOperator, router, t])
|
||||
|
||||
useEffect(() => {
|
||||
if (isCurrentWorkspaceDatasetOperator)
|
||||
@ -19,4 +25,4 @@ const ToolsList: FC = () => {
|
||||
|
||||
return <ToolProviderList />
|
||||
}
|
||||
export default React.memo(ToolsList)
|
||||
export default React.memo(Layout)
|
||||
|
||||
@ -16,7 +16,6 @@ import { ToastContext } from '@/app/components/base/toast'
|
||||
import AppIcon from '@/app/components/base/app-icon'
|
||||
import { IS_CE_EDITION } from '@/config'
|
||||
import Input from '@/app/components/base/input'
|
||||
import { useGlobalPublicStore } from '@/context/global-public-context'
|
||||
|
||||
const titleClassName = `
|
||||
system-sm-semibold text-text-secondary
|
||||
@ -29,7 +28,7 @@ const validPassword = /^(?=.*[a-zA-Z])(?=.*\d).{8,}$/
|
||||
|
||||
export default function AccountPage() {
|
||||
const { t } = useTranslation()
|
||||
const { systemFeatures } = useGlobalPublicStore()
|
||||
const { systemFeatures } = useAppContext()
|
||||
const { mutateUserProfile, userProfile, apps } = useAppContext()
|
||||
const { notify } = useContext(ToastContext)
|
||||
const [editNameModalVisible, setEditNameModalVisible] = useState(false)
|
||||
@ -134,7 +133,7 @@ export default function AccountPage() {
|
||||
<h4 className='title-2xl-semi-bold text-text-primary'>{t('common.account.myAccount')}</h4>
|
||||
</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'>
|
||||
<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'>
|
||||
<p className='system-xl-semibold text-text-primary'>{userProfile.name}</p>
|
||||
<p className='system-xs-regular text-text-tertiary'>{userProfile.email}</p>
|
||||
|
||||
@ -5,11 +5,9 @@ import { useRouter } from 'next/navigation'
|
||||
import Button from '../components/base/button'
|
||||
import Avatar from './avatar'
|
||||
import LogoSite from '@/app/components/base/logo/logo-site'
|
||||
import { useGlobalPublicStore } from '@/context/global-public-context'
|
||||
|
||||
const Header = () => {
|
||||
const { t } = useTranslation()
|
||||
const { systemFeatures } = useGlobalPublicStore()
|
||||
const router = useRouter()
|
||||
|
||||
const back = () => {
|
||||
@ -27,7 +25,7 @@ const Header = () => {
|
||||
<div className='flex items-center flex-shrink-0 gap-3'>
|
||||
<Button className='gap-2 py-2 px-3 system-sm-medium' onClick={back}>
|
||||
<RiRobot2Line className='w-4 h-4' />
|
||||
<p>{!systemFeatures.branding.enabled && 'Dify '}{t('common.account.studio')}</p>
|
||||
<p>{t('common.account.studio')}</p>
|
||||
<RiArrowRightUpLine className='w-4 h-4' />
|
||||
</Button>
|
||||
<div className='w-[1px] h-4 bg-divider-regular' />
|
||||
|
||||
@ -32,4 +32,9 @@ const Layout = ({ children }: { children: ReactNode }) => {
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export const metadata = {
|
||||
title: 'Dify',
|
||||
}
|
||||
|
||||
export default Layout
|
||||
|
||||
@ -1,11 +1,6 @@
|
||||
'use client'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import AccountPage from './account-page'
|
||||
import useDocumentTitle from '@/hooks/use-document-title'
|
||||
|
||||
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'>
|
||||
<AccountPage />
|
||||
</div>
|
||||
|
||||
@ -7,10 +7,8 @@ import Button from '@/app/components/base/button'
|
||||
|
||||
import { invitationCheck } from '@/service/common'
|
||||
import Loading from '@/app/components/base/loading'
|
||||
import useDocumentTitle from '@/hooks/use-document-title'
|
||||
|
||||
const ActivateForm = () => {
|
||||
useDocumentTitle('')
|
||||
const router = useRouter()
|
||||
const { t } = useTranslation()
|
||||
const searchParams = useSearchParams()
|
||||
|
||||
@ -1,13 +1,10 @@
|
||||
'use client'
|
||||
import React from 'react'
|
||||
import Header from '../signin/_header'
|
||||
import style from '../signin/page.module.css'
|
||||
import ActivateForm from './activateForm'
|
||||
import cn from '@/utils/classnames'
|
||||
import { useGlobalPublicStore } from '@/context/global-public-context'
|
||||
|
||||
const Activate = () => {
|
||||
const { systemFeatures } = useGlobalPublicStore()
|
||||
return (
|
||||
<div className={cn(
|
||||
style.background,
|
||||
@ -24,9 +21,9 @@ const Activate = () => {
|
||||
}>
|
||||
<Header />
|
||||
<ActivateForm />
|
||||
{!systemFeatures.branding.enabled && <div className='px-8 py-6 text-sm font-normal text-gray-500'>
|
||||
<div className='px-8 py-6 text-sm font-normal text-gray-500'>
|
||||
© {new Date().getFullYear()} LangGenius, Inc. All rights reserved.
|
||||
</div>}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@ -186,15 +186,17 @@ const Apps = ({
|
||||
<div className='w-[180px] h-8'></div>
|
||||
</div>
|
||||
<div className='relative flex flex-1 overflow-y-auto'>
|
||||
{!searchKeywords && <div className='w-[200px] h-full p-4'>
|
||||
<Sidebar current={currCategory as AppCategories} onClick={(category) => { setCurrCategory(category) }} onCreateFromBlank={onCreateFromBlank} />
|
||||
{!searchKeywords && <div className='h-full w-[200px] p-4'>
|
||||
<Sidebar current={currCategory as AppCategories} categories={categories} onClick={(category) => { setCurrCategory(category) }} onCreateFromBlank={onCreateFromBlank} />
|
||||
</div>}
|
||||
<div className='flex-1 h-full overflow-auto shrink-0 grow p-6 pt-2 border-l border-divider-burn'>
|
||||
{searchFilteredList && searchFilteredList.length > 0 && <>
|
||||
<div className='pt-4 pb-1'>
|
||||
{searchKeywords
|
||||
? <p className='title-md-semi-bold text-text-tertiary'>{searchFilteredList.length > 1 ? t('app.newApp.foundResults', { count: searchFilteredList.length }) : t('app.newApp.foundResult', { count: searchFilteredList.length })}</p>
|
||||
: <AppCategoryLabel category={currCategory as AppCategories} className='title-md-semi-bold text-text-primary' />}
|
||||
: <div className='flex h-[22px] items-center'>
|
||||
<AppCategoryLabel category={currCategory as AppCategories} className='title-md-semi-bold text-text-primary' />
|
||||
</div>}
|
||||
</div>
|
||||
<div
|
||||
className={cn(
|
||||
|
||||
@ -1,39 +1,29 @@
|
||||
'use client'
|
||||
import { RiAppsFill, RiChatSmileAiFill, RiExchange2Fill, RiPassPendingFill, RiQuillPenAiFill, RiSpeakAiFill, RiStickyNoteAddLine, RiTerminalBoxFill, RiThumbUpFill } from '@remixicon/react'
|
||||
import { RiStickyNoteAddLine, RiThumbUpLine } from '@remixicon/react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import classNames from '@/utils/classnames'
|
||||
import Divider from '@/app/components/base/divider'
|
||||
|
||||
export enum AppCategories {
|
||||
RECOMMENDED = 'Recommended',
|
||||
ASSISTANT = 'Assistant',
|
||||
AGENT = 'Agent',
|
||||
HR = 'HR',
|
||||
PROGRAMMING = 'Programming',
|
||||
WORKFLOW = 'Workflow',
|
||||
WRITING = 'Writing',
|
||||
}
|
||||
|
||||
type SidebarProps = {
|
||||
current: AppCategories
|
||||
onClick?: (category: AppCategories) => void
|
||||
current: AppCategories | string
|
||||
categories: string[]
|
||||
onClick?: (category: AppCategories | string) => void
|
||||
onCreateFromBlank?: () => void
|
||||
}
|
||||
|
||||
export default function Sidebar({ current, onClick, onCreateFromBlank }: SidebarProps) {
|
||||
export default function Sidebar({ current, categories, onClick, onCreateFromBlank }: SidebarProps) {
|
||||
const { t } = useTranslation()
|
||||
return <div className="w-full h-full flex flex-col">
|
||||
<ul>
|
||||
return <div className="flex h-full w-full flex-col">
|
||||
<ul className='pt-0.5'>
|
||||
<CategoryItem category={AppCategories.RECOMMENDED} active={current === AppCategories.RECOMMENDED} onClick={onClick} />
|
||||
</ul>
|
||||
<div className='px-3 pt-2 pb-1 system-xs-medium-uppercase text-text-tertiary'>{t('app.newAppFromTemplate.byCategories')}</div>
|
||||
<ul className='flex-grow flex flex-col gap-0.5'>
|
||||
<CategoryItem category={AppCategories.ASSISTANT} active={current === AppCategories.ASSISTANT} onClick={onClick} />
|
||||
<CategoryItem category={AppCategories.AGENT} active={current === AppCategories.AGENT} onClick={onClick} />
|
||||
<CategoryItem category={AppCategories.HR} active={current === AppCategories.HR} onClick={onClick} />
|
||||
<CategoryItem category={AppCategories.PROGRAMMING} active={current === AppCategories.PROGRAMMING} onClick={onClick} />
|
||||
<CategoryItem category={AppCategories.WORKFLOW} active={current === AppCategories.WORKFLOW} onClick={onClick} />
|
||||
<CategoryItem category={AppCategories.WRITING} active={current === AppCategories.WRITING} onClick={onClick} />
|
||||
<div className='system-xs-medium-uppercase mb-0.5 mt-3 px-3 pb-1 pt-2 text-text-tertiary'>{t('app.newAppFromTemplate.byCategories')}</div>
|
||||
<ul className='flex grow flex-col gap-0.5'>
|
||||
{categories.map(category => (<CategoryItem key={category} category={category} active={current === category} onClick={onClick} />))}
|
||||
</ul>
|
||||
<Divider bgStyle='gradient' />
|
||||
<div className='px-3 py-1 flex items-center gap-1 text-text-tertiary cursor-pointer' onClick={onCreateFromBlank}>
|
||||
@ -45,47 +35,26 @@ export default function Sidebar({ current, onClick, onCreateFromBlank }: Sidebar
|
||||
|
||||
type CategoryItemProps = {
|
||||
active: boolean
|
||||
category: AppCategories
|
||||
onClick?: (category: AppCategories) => void
|
||||
category: AppCategories | string
|
||||
onClick?: (category: AppCategories | string) => void
|
||||
}
|
||||
function CategoryItem({ category, active, onClick }: CategoryItemProps) {
|
||||
return <li
|
||||
className={classNames('p-1 pl-3 rounded-lg flex items-center gap-2 group cursor-pointer hover:bg-state-base-hover [&.active]:bg-state-base-active', active && 'active')}
|
||||
className={classNames('p-1 pl-3 h-8 rounded-lg flex items-center gap-2 group cursor-pointer hover:bg-state-base-hover [&.active]:bg-state-base-active', active && 'active')}
|
||||
onClick={() => { onClick?.(category) }}>
|
||||
<div className='w-5 h-5 inline-flex items-center justify-center rounded-md border border-divider-regular bg-components-icon-bg-midnight-solid group-[.active]:bg-components-icon-bg-blue-solid'>
|
||||
<AppCategoryIcon category={category} />
|
||||
</div>
|
||||
{category === AppCategories.RECOMMENDED && <div className='inline-flex h-5 w-5 items-center justify-center rounded-md'>
|
||||
<RiThumbUpLine className='h-4 w-4 text-components-menu-item-text group-[.active]:text-components-menu-item-text-active' />
|
||||
</div>}
|
||||
<AppCategoryLabel category={category}
|
||||
className={classNames('system-sm-medium text-components-menu-item-text group-[.active]:text-components-menu-item-text-active group-hover:text-components-menu-item-text-hover', active && 'system-sm-semibold')} />
|
||||
</li >
|
||||
}
|
||||
|
||||
type AppCategoryLabelProps = {
|
||||
category: AppCategories
|
||||
category: AppCategories | string
|
||||
className?: string
|
||||
}
|
||||
export function AppCategoryLabel({ category, className }: AppCategoryLabelProps) {
|
||||
const { t } = useTranslation()
|
||||
return <span className={className}>{t(`app.newAppFromTemplate.sidebar.${category}`)}</span>
|
||||
}
|
||||
|
||||
type AppCategoryIconProps = {
|
||||
category: AppCategories
|
||||
}
|
||||
function AppCategoryIcon({ category }: AppCategoryIconProps) {
|
||||
if (category === AppCategories.AGENT)
|
||||
return <RiSpeakAiFill className='w-3.5 h-3.5 text-components-avatar-shape-fill-stop-100' />
|
||||
if (category === AppCategories.ASSISTANT)
|
||||
return <RiChatSmileAiFill className='w-3.5 h-3.5 text-components-avatar-shape-fill-stop-100' />
|
||||
if (category === AppCategories.HR)
|
||||
return <RiPassPendingFill className='w-3.5 h-3.5 text-components-avatar-shape-fill-stop-100' />
|
||||
if (category === AppCategories.PROGRAMMING)
|
||||
return <RiTerminalBoxFill className='w-3.5 h-3.5 text-components-avatar-shape-fill-stop-100' />
|
||||
if (category === AppCategories.RECOMMENDED)
|
||||
return <RiThumbUpFill className='w-3.5 h-3.5 text-components-avatar-shape-fill-stop-100' />
|
||||
if (category === AppCategories.WRITING)
|
||||
return <RiQuillPenAiFill className='w-3.5 h-3.5 text-components-avatar-shape-fill-stop-100' />
|
||||
if (category === AppCategories.WORKFLOW)
|
||||
return <RiExchange2Fill className='w-3.5 h-3.5 text-components-avatar-shape-fill-stop-100' />
|
||||
return <RiAppsFill className='w-3.5 h-3.5 text-components-avatar-shape-fill-stop-100' />
|
||||
return <span className={className}>{category === AppCategories.RECOMMENDED ? t('app.newAppFromTemplate.sidebar.Recommended') : category}</span>
|
||||
}
|
||||
|
||||
@ -0,0 +1,46 @@
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Modal from '@/app/components/base/modal'
|
||||
import Button from '@/app/components/base/button'
|
||||
|
||||
type DSLConfirmModalProps = {
|
||||
versions?: {
|
||||
importedVersion: string
|
||||
systemVersion: string
|
||||
}
|
||||
onCancel: () => void
|
||||
onConfirm: () => void
|
||||
confirmDisabled?: boolean
|
||||
}
|
||||
const DSLConfirmModal = ({
|
||||
versions = { importedVersion: '', systemVersion: '' },
|
||||
onCancel,
|
||||
onConfirm,
|
||||
confirmDisabled = false,
|
||||
}: DSLConfirmModalProps) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isShow
|
||||
onClose={() => onCancel()}
|
||||
className='w-[480px]'
|
||||
>
|
||||
<div className='flex flex-col items-start gap-2 self-stretch pb-4'>
|
||||
<div className='title-2xl-semi-bold text-text-primary'>{t('app.newApp.appCreateDSLErrorTitle')}</div>
|
||||
<div className='system-md-regular flex grow flex-col text-text-secondary'>
|
||||
<div>{t('app.newApp.appCreateDSLErrorPart1')}</div>
|
||||
<div>{t('app.newApp.appCreateDSLErrorPart2')}</div>
|
||||
<br />
|
||||
<div>{t('app.newApp.appCreateDSLErrorPart3')}<span className='system-md-medium'>{versions.importedVersion}</span></div>
|
||||
<div>{t('app.newApp.appCreateDSLErrorPart4')}<span className='system-md-medium'>{versions.systemVersion}</span></div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex items-start justify-end gap-2 self-stretch pt-6'>
|
||||
<Button variant='secondary' onClick={() => onCancel()}>{t('app.newApp.Cancel')}</Button>
|
||||
<Button variant='primary' destructive onClick={onConfirm} disabled={confirmDisabled}>{t('app.newApp.Confirm')}</Button>
|
||||
</div>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
export default DSLConfirmModal
|
||||
@ -24,7 +24,7 @@ const OPTION_MAP = {
|
||||
iframe: {
|
||||
getContent: (url: string, token: string) =>
|
||||
`<iframe
|
||||
src="${url}/chatbot/${token}"
|
||||
src="${url}/chat/${token}"
|
||||
style="width: 100%; height: 100%; min-height: 700px"
|
||||
frameborder="0"
|
||||
allow="microphone">
|
||||
@ -35,12 +35,12 @@ const OPTION_MAP = {
|
||||
`<script>
|
||||
window.difyChatbotConfig = {
|
||||
token: '${token}'${isTestEnv
|
||||
? `,
|
||||
? `,
|
||||
isDev: true`
|
||||
: ''}${IS_CE_EDITION
|
||||
? `,
|
||||
: ''}${IS_CE_EDITION
|
||||
? `,
|
||||
baseUrl: '${url}'`
|
||||
: ''}
|
||||
: ''}
|
||||
}
|
||||
</script>
|
||||
<script
|
||||
|
||||
@ -4,7 +4,7 @@ import React, { useCallback, useEffect, useState } from 'react'
|
||||
import { RiArrowRightSLine, RiCloseLine } from '@remixicon/react'
|
||||
import Link from 'next/link'
|
||||
import { Trans, useTranslation } from 'react-i18next'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import { useContext, useContextSelector } from 'use-context-selector'
|
||||
import { SparklesSoft } from '@/app/components/base/icons/src/public/common'
|
||||
import Modal from '@/app/components/base/modal'
|
||||
import ActionButton from '@/app/components/base/action-button'
|
||||
@ -21,14 +21,13 @@ import type { AppIconType, AppSSO, Language } from '@/types/app'
|
||||
import { useToastContext } from '@/app/components/base/toast'
|
||||
import { LanguagesSupported, languages } from '@/i18n/language'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import AppContext, { useAppContext } from '@/context/app-context'
|
||||
import { useProviderContext } from '@/context/provider-context'
|
||||
import { useModalContext } from '@/context/modal-context'
|
||||
import type { AppIconSelection } from '@/app/components/base/app-icon-picker'
|
||||
import AppIconPicker from '@/app/components/base/app-icon-picker'
|
||||
import I18n from '@/context/i18n'
|
||||
import cn from '@/utils/classnames'
|
||||
import { useGlobalPublicStore } from '@/context/global-public-context'
|
||||
|
||||
export type ISettingsModalProps = {
|
||||
isChat: boolean
|
||||
@ -66,7 +65,7 @@ const SettingsModal: FC<ISettingsModalProps> = ({
|
||||
onClose,
|
||||
onSave,
|
||||
}) => {
|
||||
const { systemFeatures } = useGlobalPublicStore()
|
||||
const systemFeatures = useContextSelector(AppContext, state => state.systemFeatures)
|
||||
const { isCurrentWorkspaceEditor } = useAppContext()
|
||||
const { notify } = useToastContext()
|
||||
const [isShowMore, setIsShowMore] = useState(false)
|
||||
@ -111,7 +110,7 @@ const SettingsModal: FC<ISettingsModalProps> = ({
|
||||
: { type: 'emoji', icon, background: icon_background! },
|
||||
)
|
||||
|
||||
const { enableBilling, plan, webappCopyrightEnabled } = useProviderContext()
|
||||
const { enableBilling, plan } = useProviderContext()
|
||||
const { setShowPricingModal, setShowAccountSettingModal } = useModalContext()
|
||||
const isFreePlan = plan.type === 'sandbox'
|
||||
const handlePlanClick = useCallback(() => {
|
||||
@ -178,7 +177,7 @@ const SettingsModal: FC<ISettingsModalProps> = ({
|
||||
chat_color_theme: inputInfo.chatColorTheme,
|
||||
chat_color_theme_inverted: inputInfo.chatColorThemeInverted,
|
||||
prompt_public: false,
|
||||
copyright: !webappCopyrightEnabled
|
||||
copyright: isFreePlan
|
||||
? ''
|
||||
: inputInfo.copyrightSwitchValue
|
||||
? inputInfo.copyright
|
||||
@ -355,7 +354,7 @@ const SettingsModal: FC<ISettingsModalProps> = ({
|
||||
<div className={cn('py-1 text-text-secondary system-sm-semibold')}>{t(`${prefixSettings}.more.entry`)}</div>
|
||||
<p className={cn('pb-0.5 text-text-tertiary body-xs-regular')}>{t(`${prefixSettings}.more.copyRightPlaceholder`)} & {t(`${prefixSettings}.more.privacyPolicyPlaceholder`)}</p>
|
||||
</div>
|
||||
<RiArrowRightSLine className='shrink-0 ml-1 w-4 h-4 text-text-secondary' />
|
||||
<RiArrowRightSLine className='shrink-0 ml-1 w-4 h-4 text-text-secondary'/>
|
||||
</div>
|
||||
)}
|
||||
{/* more settings */}
|
||||
@ -381,14 +380,14 @@ const SettingsModal: FC<ISettingsModalProps> = ({
|
||||
)}
|
||||
</div>
|
||||
<Tooltip
|
||||
disabled={webappCopyrightEnabled}
|
||||
disabled={!isFreePlan}
|
||||
popupContent={
|
||||
<div className='w-[180px]'>{t(`${prefixSettings}.more.copyrightTooltip`)}</div>
|
||||
<div className='w-[260px]'>{t(`${prefixSettings}.more.copyrightTooltip`)}</div>
|
||||
}
|
||||
asChild={false}
|
||||
>
|
||||
<Switch
|
||||
disabled={!webappCopyrightEnabled}
|
||||
disabled={isFreePlan}
|
||||
defaultValue={inputInfo.copyrightSwitchValue}
|
||||
onChange={v => setInputInfo({ ...inputInfo, copyrightSwitchValue: v })}
|
||||
/>
|
||||
@ -440,22 +439,20 @@ const SettingsModal: FC<ISettingsModalProps> = ({
|
||||
<Button variant='primary' onClick={onClickSave} loading={saveLoading}>{t('common.operation.save')}</Button>
|
||||
</div>
|
||||
</Modal >
|
||||
{
|
||||
showAppIconPicker && (
|
||||
<AppIconPicker
|
||||
onSelect={(payload) => {
|
||||
setAppIcon(payload)
|
||||
setShowAppIconPicker(false)
|
||||
}}
|
||||
onClose={() => {
|
||||
setAppIcon(icon_type === 'image'
|
||||
? { type: 'image', url: icon_url!, fileId: icon }
|
||||
: { type: 'emoji', icon, background: icon_background! })
|
||||
setShowAppIconPicker(false)
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
{showAppIconPicker && (
|
||||
<AppIconPicker
|
||||
onSelect={(payload) => {
|
||||
setAppIcon(payload)
|
||||
setShowAppIconPicker(false)
|
||||
}}
|
||||
onClose={() => {
|
||||
setAppIcon(icon_type === 'image'
|
||||
? { type: 'image', url: icon_url!, fileId: icon }
|
||||
: { type: 'emoji', icon, background: icon_background! })
|
||||
setShowAppIconPicker(false)
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
|
||||
)
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
import { UUID_NIL } from './constants'
|
||||
import type { IChatItem } from './chat/type'
|
||||
import type { ChatItem, ChatItemInTree } from './types'
|
||||
import { addFileInfos, sortAgentSorts } from '../../tools/utils'
|
||||
import { getProcessedFilesFromResponse } from '../file-uploader/utils'
|
||||
|
||||
async function decodeBase64AndDecompress(base64String: string) {
|
||||
const binaryString = atob(base64String)
|
||||
@ -19,6 +21,60 @@ function getProcessedInputsFromUrlParams(): Record<string, any> {
|
||||
return inputs
|
||||
}
|
||||
|
||||
function appendQAToChatList(chatList: ChatItem[], item: any) {
|
||||
// we append answer first and then question since will reverse the whole chatList later
|
||||
const answerFiles = item.message_files?.filter((file: any) => file.belongs_to === 'assistant') || []
|
||||
chatList.push({
|
||||
id: item.id,
|
||||
content: item.answer,
|
||||
agent_thoughts: addFileInfos(item.agent_thoughts ? sortAgentSorts(item.agent_thoughts) : item.agent_thoughts, item.message_files),
|
||||
feedback: item.feedback,
|
||||
isAnswer: true,
|
||||
citation: item.retriever_resources,
|
||||
message_files: getProcessedFilesFromResponse(answerFiles.map((item: any) => ({ ...item, related_id: item.id }))),
|
||||
})
|
||||
const questionFiles = item.message_files?.filter((file: any) => file.belongs_to === 'user') || []
|
||||
chatList.push({
|
||||
id: `question-${item.id}`,
|
||||
content: item.query,
|
||||
isAnswer: false,
|
||||
message_files: getProcessedFilesFromResponse(questionFiles.map((item: any) => ({ ...item, related_id: item.id }))),
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes the latest thread messages from all messages of the conversation.
|
||||
* Same logic as backend codebase `api/core/prompt/utils/extract_thread_messages.py`
|
||||
*
|
||||
* @param fetchedMessages - The history chat list data from the backend, sorted by created_at in descending order. This includes all flattened history messages of the conversation.
|
||||
* @returns An array of ChatItems representing the latest thread.
|
||||
*/
|
||||
|
||||
|
||||
function getPrevChatList(fetchedMessages: any[]) {
|
||||
const ret: ChatItem[] = []
|
||||
let nextMessageId = null
|
||||
|
||||
for (const item of fetchedMessages) {
|
||||
if (!item.parent_message_id) {
|
||||
appendQAToChatList(ret, item)
|
||||
break
|
||||
}
|
||||
|
||||
if (!nextMessageId) {
|
||||
appendQAToChatList(ret, item)
|
||||
nextMessageId = item.parent_message_id
|
||||
}
|
||||
else {
|
||||
if (item.id === nextMessageId || nextMessageId === UUID_NIL) {
|
||||
appendQAToChatList(ret, item)
|
||||
nextMessageId = item.parent_message_id
|
||||
}
|
||||
}
|
||||
}
|
||||
return ret.reverse()
|
||||
}
|
||||
|
||||
function isValidGeneratedAnswer(item?: ChatItem | ChatItemInTree): boolean {
|
||||
return !!item && item.isAnswer && !item.id.startsWith('answer-placeholder-') && !item.isOpeningStatement
|
||||
}
|
||||
@ -100,7 +156,7 @@ function getThreadMessages(tree: ChatItemInTree[], targetMessageId?: string): Ch
|
||||
let targetNode: ChatItemInTree | undefined
|
||||
|
||||
// find path to the target message
|
||||
const stack = tree.slice().reverse().map(rootNode => ({
|
||||
const stack = tree.toReversed().map(rootNode => ({
|
||||
node: rootNode,
|
||||
path: [rootNode],
|
||||
}))
|
||||
@ -164,6 +220,7 @@ function getThreadMessages(tree: ChatItemInTree[], targetMessageId?: string): Ch
|
||||
export {
|
||||
getProcessedInputsFromUrlParams,
|
||||
isValidGeneratedAnswer,
|
||||
getPrevChatList,
|
||||
getLastAnswer,
|
||||
buildChatItemTree,
|
||||
getThreadMessages,
|
||||
|
||||
@ -2,7 +2,6 @@
|
||||
import type { FC } from 'react'
|
||||
import classNames from '@/utils/classnames'
|
||||
import { useSelector } from '@/context/app-context'
|
||||
import { useGlobalPublicStore } from '@/context/global-public-context'
|
||||
|
||||
type LogoSiteProps = {
|
||||
className?: string
|
||||
@ -11,17 +10,13 @@ type LogoSiteProps = {
|
||||
const LogoSite: FC<LogoSiteProps> = ({
|
||||
className,
|
||||
}) => {
|
||||
const { systemFeatures } = useGlobalPublicStore()
|
||||
const { theme } = useSelector((s) => {
|
||||
return {
|
||||
theme: s.theme,
|
||||
}
|
||||
})
|
||||
|
||||
let src = theme === 'light' ? '/logo/logo-site.png' : `/logo/logo-site-${theme}.png`
|
||||
if (systemFeatures.branding.enabled)
|
||||
src = systemFeatures.branding.workspace_logo
|
||||
|
||||
const src = theme === 'light' ? '/logo/logo-site.png' : `/logo/logo-site-${theme}.png`
|
||||
return (
|
||||
<img
|
||||
src={src}
|
||||
|
||||
@ -67,7 +67,6 @@ export type CurrentPlanInfoBackend = {
|
||||
can_replace_logo: boolean
|
||||
model_load_balancing_enabled: boolean
|
||||
dataset_operator_enabled: boolean
|
||||
webapp_copyright_enabled: boolean
|
||||
}
|
||||
|
||||
export type SubscriptionItem = {
|
||||
|
||||
@ -15,7 +15,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty } from '../md.tsx'
|
||||
### 鉴权
|
||||
|
||||
|
||||
Service API 使用 `API-Key` 进行鉴权。
|
||||
Dify Service API 使用 `API-Key` 进行鉴权。
|
||||
<i>**强烈建议开发者把 `API-Key` 放在后端存储,而非分享或者放在客户端存储,以免 `API-Key` 泄露,导致财产损失。**</i>
|
||||
所有 API 请求都应在 **`Authorization`** HTTP Header 中包含您的 `API-Key`,如下所示:
|
||||
|
||||
|
||||
@ -14,7 +14,7 @@ Workflow 应用无会话支持,适合用于翻译/文章写作/总结 AI 等
|
||||
|
||||
### Authentication
|
||||
|
||||
Service API 使用 `API-Key` 进行鉴权。
|
||||
Dify Service API 使用 `API-Key` 进行鉴权。
|
||||
<i>**强烈建议开发者把 `API-Key` 放在后端存储,而非分享或者放在客户端存储,以免 `API-Key` 泄露,导致财产损失。**</i>
|
||||
所有 API 请求都应在 **`Authorization`** HTTP Header 中包含您的 `API-Key`,如下所示:
|
||||
|
||||
|
||||
@ -1,12 +1,10 @@
|
||||
'use client'
|
||||
|
||||
import React, { useMemo, useState } from 'react'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import React, { useCallback, useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import useSWR from 'swr'
|
||||
import { useDebounceFn } from 'ahooks'
|
||||
import Toast from '../../base/toast'
|
||||
import s from './style.module.css'
|
||||
import cn from '@/utils/classnames'
|
||||
import ExploreContext from '@/context/explore-context'
|
||||
@ -14,17 +12,17 @@ import type { App } from '@/models/explore'
|
||||
import Category from '@/app/components/explore/category'
|
||||
import AppCard from '@/app/components/explore/app-card'
|
||||
import { fetchAppDetail, fetchAppList } from '@/service/explore'
|
||||
import { importDSL } from '@/service/apps'
|
||||
import { useTabSearchParams } from '@/hooks/use-tab-searchparams'
|
||||
import CreateAppModal from '@/app/components/explore/create-app-modal'
|
||||
import AppTypeSelector from '@/app/components/app/type-selector'
|
||||
import type { CreateAppModalProps } from '@/app/components/explore/create-app-modal'
|
||||
import Loading from '@/app/components/base/loading'
|
||||
import { NEED_REFRESH_APP_LIST_KEY } from '@/config'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import { getRedirection } from '@/utils/app-redirection'
|
||||
import Input from '@/app/components/base/input'
|
||||
import { DSLImportMode } from '@/models/app'
|
||||
import {
|
||||
DSLImportMode,
|
||||
} from '@/models/app'
|
||||
import { useImportDSL } from '@/hooks/use-import-dsl'
|
||||
import DSLConfirmModal from '@/app/components/app/create-from-dsl-modal/dsl-confirm-modal'
|
||||
|
||||
type AppsProps = {
|
||||
pageType?: PageType
|
||||
@ -41,8 +39,6 @@ const Apps = ({
|
||||
onSuccess,
|
||||
}: AppsProps) => {
|
||||
const { t } = useTranslation()
|
||||
const { isCurrentWorkspaceEditor } = useAppContext()
|
||||
const { push } = useRouter()
|
||||
const { hasEditPermission } = useContext(ExploreContext)
|
||||
const allCategoriesEn = t('explore.apps.allCategories', { lng: 'en' })
|
||||
|
||||
@ -117,6 +113,14 @@ const Apps = ({
|
||||
|
||||
const [currApp, setCurrApp] = React.useState<App | null>(null)
|
||||
const [isShowCreateModal, setIsShowCreateModal] = React.useState(false)
|
||||
|
||||
const {
|
||||
handleImportDSL,
|
||||
handleImportDSLConfirm,
|
||||
versions,
|
||||
isFetching,
|
||||
} = useImportDSL()
|
||||
const [showDSLConfirmModal, setShowDSLConfirmModal] = useState(false)
|
||||
const onCreate: CreateAppModalProps['onConfirm'] = async ({
|
||||
name,
|
||||
icon_type,
|
||||
@ -127,31 +131,31 @@ const Apps = ({
|
||||
const { export_data } = await fetchAppDetail(
|
||||
currApp?.app.id as string,
|
||||
)
|
||||
try {
|
||||
const app = await importDSL({
|
||||
mode: DSLImportMode.YAML_CONTENT,
|
||||
yaml_content: export_data,
|
||||
name,
|
||||
icon_type,
|
||||
icon,
|
||||
icon_background,
|
||||
description,
|
||||
})
|
||||
setIsShowCreateModal(false)
|
||||
Toast.notify({
|
||||
type: 'success',
|
||||
message: t('app.newApp.appCreated'),
|
||||
})
|
||||
if (onSuccess)
|
||||
onSuccess()
|
||||
localStorage.setItem(NEED_REFRESH_APP_LIST_KEY, '1')
|
||||
getRedirection(isCurrentWorkspaceEditor, { id: app.app_id }, push)
|
||||
}
|
||||
catch (e) {
|
||||
Toast.notify({ type: 'error', message: t('app.newApp.appCreateFailed') })
|
||||
const payload = {
|
||||
mode: DSLImportMode.YAML_CONTENT,
|
||||
yaml_content: export_data,
|
||||
name,
|
||||
icon_type,
|
||||
icon,
|
||||
icon_background,
|
||||
description,
|
||||
}
|
||||
await handleImportDSL(payload, {
|
||||
onSuccess: () => {
|
||||
setIsShowCreateModal(false)
|
||||
},
|
||||
onPending: () => {
|
||||
setShowDSLConfirmModal(true)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const onConfirmDSL = useCallback(async () => {
|
||||
await handleImportDSLConfirm({
|
||||
onSuccess,
|
||||
})
|
||||
}, [handleImportDSLConfirm, onSuccess])
|
||||
|
||||
if (!categories || categories.length === 0) {
|
||||
return (
|
||||
<div className="flex h-full items-center">
|
||||
@ -234,9 +238,20 @@ const Apps = ({
|
||||
appDescription={currApp?.app.description || ''}
|
||||
show={isShowCreateModal}
|
||||
onConfirm={onCreate}
|
||||
confirmDisabled={isFetching}
|
||||
onHide={() => setIsShowCreateModal(false)}
|
||||
/>
|
||||
)}
|
||||
{
|
||||
showDSLConfirmModal && (
|
||||
<DSLConfirmModal
|
||||
versions={versions}
|
||||
onCancel={() => setShowDSLConfirmModal(false)}
|
||||
onConfirm={onConfirmDSL}
|
||||
confirmDisabled={isFetching}
|
||||
/>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@ -33,6 +33,7 @@ export type CreateAppModalProps = {
|
||||
description: string
|
||||
use_icon_as_answer_icon?: boolean
|
||||
}) => Promise<void>
|
||||
confirmDisabled?: boolean
|
||||
onHide: () => void
|
||||
}
|
||||
|
||||
@ -48,6 +49,7 @@ const CreateAppModal = ({
|
||||
appMode,
|
||||
appUseIconAsAnswerIcon,
|
||||
onConfirm,
|
||||
confirmDisabled,
|
||||
onHide,
|
||||
}: CreateAppModalProps) => {
|
||||
const { t } = useTranslation()
|
||||
@ -145,7 +147,7 @@ const CreateAppModal = ({
|
||||
{!isEditModal && isAppsFull && <AppsFull loc='app-explore-create' />}
|
||||
</div>
|
||||
<div className='flex flex-row-reverse'>
|
||||
<Button disabled={!isEditModal && isAppsFull} className='w-24 ml-2' variant='primary' onClick={submit}>{!isEditModal ? t('common.operation.create') : t('common.operation.save')}</Button>
|
||||
<Button disabled={(!isEditModal && isAppsFull) || !name.trim() || confirmDisabled} className='w-24 ml-2' variant='primary' onClick={submit}>{!isEditModal ? t('common.operation.create') : t('common.operation.save')}</Button>
|
||||
<Button className='w-24' onClick={onHide}>{t('common.operation.cancel')}</Button>
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
import type { FC } from 'react'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import ExploreContext from '@/context/explore-context'
|
||||
import Sidebar from '@/app/components/explore/sidebar'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
@ -15,6 +16,7 @@ export type IExploreProps = {
|
||||
const Explore: FC<IExploreProps> = ({
|
||||
children,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const router = useRouter()
|
||||
const [controlUpdateInstalledApps, setControlUpdateInstalledApps] = useState(0)
|
||||
const { userProfile, isCurrentWorkspaceDatasetOperator } = useAppContext()
|
||||
@ -22,6 +24,7 @@ const Explore: FC<IExploreProps> = ({
|
||||
const [installedApps, setInstalledApps] = useState<InstalledApp[]>([])
|
||||
|
||||
useEffect(() => {
|
||||
document.title = `${t('explore.title')} - Dify`;
|
||||
(async () => {
|
||||
const { accounts } = await fetchMembers({ url: '/workspaces/current/members', params: {} })
|
||||
if (!accounts)
|
||||
|
||||
@ -20,7 +20,6 @@ import { useModalContext } from '@/context/modal-context'
|
||||
import { LanguagesSupported } from '@/i18n/language'
|
||||
import { useProviderContext } from '@/context/provider-context'
|
||||
import { Plan } from '@/app/components/billing/type'
|
||||
import { useGlobalPublicStore } from '@/context/global-public-context'
|
||||
|
||||
export type IAppSelector = {
|
||||
isMobile: boolean
|
||||
@ -33,7 +32,6 @@ export default function AppSelector({ isMobile }: IAppSelector) {
|
||||
`
|
||||
const router = useRouter()
|
||||
const [aboutVisible, setAboutVisible] = useState(false)
|
||||
const { systemFeatures } = useGlobalPublicStore()
|
||||
|
||||
const { locale } = useContext(I18n)
|
||||
const { t } = useTranslation()
|
||||
@ -124,80 +122,78 @@ export default function AppSelector({ isMobile }: IAppSelector) {
|
||||
<div>{t('common.userProfile.settings')}</div>
|
||||
</div>}
|
||||
</Menu.Item>
|
||||
{!systemFeatures.branding.enabled && <>
|
||||
{canEmailSupport && <Menu.Item>
|
||||
{({ active }) => <a
|
||||
className={classNames(itemClassName, 'group justify-between',
|
||||
{canEmailSupport && <Menu.Item>
|
||||
{({ active }) => <a
|
||||
className={classNames(itemClassName, 'group justify-between',
|
||||
active && 'bg-state-base-hover',
|
||||
)}
|
||||
href={mailToSupport(userProfile.email, plan.type, langeniusVersionInfo.current_version)}
|
||||
target='_blank' rel='noopener noreferrer'>
|
||||
<div>{t('common.userProfile.emailSupport')}</div>
|
||||
<ArrowUpRight className='hidden w-[14px] h-[14px] text-text-tertiary group-hover:flex' />
|
||||
</a>}
|
||||
</Menu.Item>}
|
||||
<Menu.Item>
|
||||
{({ active }) => <Link
|
||||
className={classNames(itemClassName, 'group justify-between',
|
||||
active && 'bg-state-base-hover',
|
||||
)}
|
||||
href='https://github.com/langgenius/dify/discussions/categories/feedbacks'
|
||||
target='_blank' rel='noopener noreferrer'>
|
||||
<div>{t('common.userProfile.communityFeedback')}</div>
|
||||
<ArrowUpRight className='hidden w-[14px] h-[14px] text-text-tertiary group-hover:flex' />
|
||||
</Link>}
|
||||
</Menu.Item>
|
||||
<Menu.Item>
|
||||
{({ active }) => <Link
|
||||
className={classNames(itemClassName, 'group justify-between',
|
||||
active && 'bg-state-base-hover',
|
||||
)}
|
||||
href='https://discord.gg/5AEfbxcd9k'
|
||||
target='_blank' rel='noopener noreferrer'>
|
||||
<div>{t('common.userProfile.community')}</div>
|
||||
<ArrowUpRight className='hidden w-[14px] h-[14px] text-text-tertiary group-hover:flex' />
|
||||
</Link>}
|
||||
</Menu.Item>
|
||||
<Menu.Item>
|
||||
{({ active }) => <Link
|
||||
className={classNames(itemClassName, 'group justify-between',
|
||||
active && 'bg-state-base-hover',
|
||||
)}
|
||||
href={
|
||||
locale !== LanguagesSupported[1] ? 'https://docs.dify.ai/' : `https://docs.dify.ai/v/${locale.toLowerCase()}/`
|
||||
}
|
||||
target='_blank' rel='noopener noreferrer'>
|
||||
<div>{t('common.userProfile.helpCenter')}</div>
|
||||
<ArrowUpRight className='hidden w-[14px] h-[14px] text-text-tertiary group-hover:flex' />
|
||||
</Link>}
|
||||
</Menu.Item>
|
||||
<Menu.Item>
|
||||
{({ active }) => <Link
|
||||
className={classNames(itemClassName, 'group justify-between',
|
||||
active && 'bg-state-base-hover',
|
||||
)}
|
||||
href='https://roadmap.dify.ai'
|
||||
target='_blank' rel='noopener noreferrer'>
|
||||
<div>{t('common.userProfile.roadmap')}</div>
|
||||
<ArrowUpRight className='hidden w-[14px] h-[14px] text-text-tertiary group-hover:flex' />
|
||||
</Link>}
|
||||
</Menu.Item>
|
||||
{
|
||||
document?.body?.getAttribute('data-public-site-about') !== 'hide' && (
|
||||
<Menu.Item>
|
||||
{({ active }) => <div className={classNames(itemClassName, 'justify-between',
|
||||
active && 'bg-state-base-hover',
|
||||
)}
|
||||
href={mailToSupport(userProfile.email, plan.type, langeniusVersionInfo.current_version)}
|
||||
target='_blank' rel='noopener noreferrer'>
|
||||
<div>{t('common.userProfile.emailSupport')}</div>
|
||||
<ArrowUpRight className='hidden w-[14px] h-[14px] text-text-tertiary group-hover:flex' />
|
||||
</a>}
|
||||
</Menu.Item>}
|
||||
<Menu.Item>
|
||||
{({ active }) => <Link
|
||||
className={classNames(itemClassName, 'group justify-between',
|
||||
active && 'bg-state-base-hover',
|
||||
)}
|
||||
href='https://github.com/langgenius/dify/discussions/categories/feedbacks'
|
||||
target='_blank' rel='noopener noreferrer'>
|
||||
<div>{t('common.userProfile.communityFeedback')}</div>
|
||||
<ArrowUpRight className='hidden w-[14px] h-[14px] text-text-tertiary group-hover:flex' />
|
||||
</Link>}
|
||||
</Menu.Item>
|
||||
<Menu.Item>
|
||||
{({ active }) => <Link
|
||||
className={classNames(itemClassName, 'group justify-between',
|
||||
active && 'bg-state-base-hover',
|
||||
)}
|
||||
href='https://discord.gg/5AEfbxcd9k'
|
||||
target='_blank' rel='noopener noreferrer'>
|
||||
<div>{t('common.userProfile.community')}</div>
|
||||
<ArrowUpRight className='hidden w-[14px] h-[14px] text-text-tertiary group-hover:flex' />
|
||||
</Link>}
|
||||
</Menu.Item>
|
||||
<Menu.Item>
|
||||
{({ active }) => <Link
|
||||
className={classNames(itemClassName, 'group justify-between',
|
||||
active && 'bg-state-base-hover',
|
||||
)}
|
||||
href={
|
||||
locale !== LanguagesSupported[1] ? 'https://docs.dify.ai/' : `https://docs.dify.ai/v/${locale.toLowerCase()}/`
|
||||
}
|
||||
target='_blank' rel='noopener noreferrer'>
|
||||
<div>{t('common.userProfile.helpCenter')}</div>
|
||||
<ArrowUpRight className='hidden w-[14px] h-[14px] text-text-tertiary group-hover:flex' />
|
||||
</Link>}
|
||||
</Menu.Item>
|
||||
<Menu.Item>
|
||||
{({ active }) => <Link
|
||||
className={classNames(itemClassName, 'group justify-between',
|
||||
active && 'bg-state-base-hover',
|
||||
)}
|
||||
href='https://roadmap.dify.ai'
|
||||
target='_blank' rel='noopener noreferrer'>
|
||||
<div>{t('common.userProfile.roadmap')}</div>
|
||||
<ArrowUpRight className='hidden w-[14px] h-[14px] text-text-tertiary group-hover:flex' />
|
||||
</Link>}
|
||||
</Menu.Item>
|
||||
{
|
||||
document?.body?.getAttribute('data-public-site-about') !== 'hide' && (
|
||||
<Menu.Item>
|
||||
{({ active }) => <div className={classNames(itemClassName, 'justify-between',
|
||||
active && 'bg-state-base-hover',
|
||||
)} onClick={() => setAboutVisible(true)}>
|
||||
<div>{t('common.userProfile.about')}</div>
|
||||
<div className='flex items-center'>
|
||||
<div className='mr-2 system-xs-regular text-text-tertiary'>{langeniusVersionInfo.current_version}</div>
|
||||
<Indicator color={langeniusVersionInfo.current_version === langeniusVersionInfo.latest_version ? 'green' : 'orange'} />
|
||||
</div>
|
||||
</div>}
|
||||
</Menu.Item>
|
||||
)
|
||||
}
|
||||
</>}
|
||||
)} onClick={() => setAboutVisible(true)}>
|
||||
<div>{t('common.userProfile.about')}</div>
|
||||
<div className='flex items-center'>
|
||||
<div className='mr-2 system-xs-regular text-text-tertiary'>{langeniusVersionInfo.current_version}</div>
|
||||
<Indicator color={langeniusVersionInfo.current_version === langeniusVersionInfo.latest_version ? 'green' : 'orange'} />
|
||||
</div>
|
||||
</div>}
|
||||
</Menu.Item>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
<Menu.Item>
|
||||
{({ active }) => <div className='p-1' onClick={() => handleLogout()}>
|
||||
|
||||
@ -21,7 +21,6 @@ import { Plan } from '@/app/components/billing/type'
|
||||
import UpgradeBtn from '@/app/components/billing/upgrade-btn'
|
||||
import { NUM_INFINITE } from '@/app/components/billing/config'
|
||||
import { LanguagesSupported } from '@/i18n/language'
|
||||
import { useGlobalPublicStore } from '@/context/global-public-context'
|
||||
dayjs.extend(relativeTime)
|
||||
|
||||
const MembersPage = () => {
|
||||
@ -35,8 +34,7 @@ const MembersPage = () => {
|
||||
}
|
||||
const { locale } = useContext(I18n)
|
||||
|
||||
const { userProfile, currentWorkspace, isCurrentWorkspaceOwner, isCurrentWorkspaceManager } = useAppContext()
|
||||
const { systemFeatures } = useGlobalPublicStore()
|
||||
const { userProfile, currentWorkspace, isCurrentWorkspaceOwner, isCurrentWorkspaceManager, systemFeatures } = useAppContext()
|
||||
const { data, mutate } = useSWR({ url: '/workspaces/current/members' }, fetchMembers)
|
||||
const [inviteModalVisible, setInviteModalVisible] = useState(false)
|
||||
const [invitationResults, setInvitationResults] = useState<InvitationResult[]>([])
|
||||
|
||||
@ -4,6 +4,7 @@ import Link from 'next/link'
|
||||
import { useBoolean } from 'ahooks'
|
||||
import { useSelectedLayoutSegment } from 'next/navigation'
|
||||
import { Bars3Icon } from '@heroicons/react/20/solid'
|
||||
import { useContextSelector } from 'use-context-selector'
|
||||
import HeaderBillingBtn from '../billing/header-billing-btn'
|
||||
import AccountDropdown from './account-dropdown'
|
||||
import AppNav from './app-nav'
|
||||
@ -14,13 +15,12 @@ import ToolsNav from './tools-nav'
|
||||
import GithubStar from './github-star'
|
||||
import LicenseNav from './license-env'
|
||||
import { WorkspaceProvider } from '@/context/workspace-context'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import AppContext, { useAppContext } from '@/context/app-context'
|
||||
import LogoSite from '@/app/components/base/logo/logo-site'
|
||||
import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
|
||||
import { useProviderContext } from '@/context/provider-context'
|
||||
import { useModalContext } from '@/context/modal-context'
|
||||
import { LicenseStatus } from '@/types/feature'
|
||||
import { useGlobalPublicStore } from '@/context/global-public-context'
|
||||
|
||||
const navClassName = `
|
||||
flex items-center relative mr-0 sm:mr-3 px-3 h-8 rounded-xl
|
||||
@ -30,7 +30,7 @@ const navClassName = `
|
||||
|
||||
const Header = () => {
|
||||
const { isCurrentWorkspaceEditor, isCurrentWorkspaceDatasetOperator } = useAppContext()
|
||||
const { systemFeatures } = useGlobalPublicStore()
|
||||
const systemFeatures = useContextSelector(AppContext, v => v.systemFeatures)
|
||||
const selectedSegment = useSelectedLayoutSegment()
|
||||
const media = useBreakpoints()
|
||||
const isMobile = media === MediaType.mobile
|
||||
|
||||
@ -5,15 +5,14 @@ import { LicenseStatus } from '@/types/feature'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useContextSelector } from 'use-context-selector'
|
||||
import dayjs from 'dayjs'
|
||||
import { useGlobalPublicStore } from '@/context/global-public-context'
|
||||
|
||||
const LicenseNav = () => {
|
||||
const { t } = useTranslation()
|
||||
const { systemFeatures } = useGlobalPublicStore()
|
||||
const systemFeatures = useContextSelector(AppContext, s => s.systemFeatures)
|
||||
|
||||
if (systemFeatures.license?.status === LicenseStatus.EXPIRING) {
|
||||
const expiredAt = systemFeatures.license?.expired_at
|
||||
const count = dayjs(expiredAt).diff(dayjs(), 'day')
|
||||
const count = dayjs(expiredAt).diff(dayjs(), 'days')
|
||||
return <div className='px-2 py-1 mr-4 rounded-full bg-util-colors-orange-orange-50 border-util-colors-orange-orange-100 system-xs-medium text-util-colors-orange-orange-600'>
|
||||
{count <= 1 && <span>{t('common.license.expiring', { count })}</span>}
|
||||
{count > 1 && <span>{t('common.license.expiring_plural', { count })}</span>}
|
||||
|
||||
@ -17,11 +17,9 @@ import ProviderCard from '@/app/components/tools/provider/card'
|
||||
import ProviderDetail from '@/app/components/tools/provider/detail'
|
||||
import Empty from '@/app/components/tools/add-tool-modal/empty'
|
||||
import { fetchCollectionList } from '@/service/tools'
|
||||
import { useGlobalPublicStore } from '@/context/global-public-context'
|
||||
|
||||
const ProviderList = () => {
|
||||
const { t } = useTranslation()
|
||||
const { systemFeatures } = useGlobalPublicStore()
|
||||
|
||||
const [activeTab, setActiveTab] = useTabSearchParams({
|
||||
defaultTab: 'builtin',
|
||||
@ -100,7 +98,7 @@ const ProviderList = () => {
|
||||
'relative grid content-start grid-cols-1 gap-4 px-12 pt-2 pb-4 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 grow shrink-0',
|
||||
currentProvider && 'pr-6 sm:grid-cols-1 md:grid-cols-2 lg:grid-cols-3',
|
||||
)}>
|
||||
{activeTab === 'builtin' && !systemFeatures.branding.enabled && <ContributeCard />}
|
||||
{activeTab === 'builtin' && <ContributeCard />}
|
||||
{activeTab === 'api' && <CustomCreateCard onRefreshData={getProviderList} />}
|
||||
{filteredCollectionList.map(collection => (
|
||||
<ProviderCard
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user