Compare commits

...

101 Commits

Author SHA1 Message Date
GareArc
fc3d3e0565 fix: wrong web sso protocal source in json 2025-04-24 23:48:18 -04:00
GareArc
3761944a3f fix: remove debug logs 2025-04-23 23:09:45 -04:00
GareArc
09f8da1429 fix: allow empty list api 2025-04-22 22:20:29 -04:00
GareArc
fcc274d679 fix: add filter in installedapp list api 2025-04-22 02:54:30 -04:00
GareArc
bfa5828259 fix: temp fix for unauthorized user in explore page 2025-04-21 19:40:51 -04:00
GareArc
455d14296f fix: get app id from upstream decorator 2025-04-21 19:03:10 -04:00
GareArc
d1a25e54e5 fix: add logging 2025-04-21 18:48:24 -04:00
GareArc
9462ed7bbf fix: add auth constraint to explore apps 2025-04-21 18:47:24 -04:00
GareArc
c6e63ac816 Revert "fix: update webapp auth api path"
This reverts commit a27db51b83.
2025-04-21 02:07:43 -04:00
GareArc
a27db51b83 fix: update webapp auth api path 2025-04-21 02:06:07 -04:00
GareArc
e52a9fbfb7 fix: remove curr user in webapp permission api 2025-04-20 23:33:51 -04:00
GareArc
2af1dd6de3 feat: add webapp auth apis 2025-04-20 23:30:59 -04:00
NFish
509733fbf0 fix: update reset password token when email code verify success (#18367) 2025-04-18 17:15:02 +08:00
GareArc
7770a45253 fix: add password security update 2025-04-18 05:02:26 -04:00
GareArc
bafdbade52 fix: wrong json structure 2025-04-11 17:19:34 -04:00
GareArc
fa76590c24 chore: add log 2025-04-11 16:59:52 -04:00
GareArc
d5b75470e4 fix: bad request 2025-04-11 16:48:09 -04:00
GareArc
5f87bdbe3a fix: add batch get access mode api 2025-04-11 15:24:32 -04:00
GareArc
cb13b53ccd fix: update webapp sso features 2025-04-11 03:25:58 -04:00
GareArc
a1dc3cfdec fix: update code for access denied error 2025-04-11 02:45:46 -04:00
GareArc
7a4ec9cf23 fix: change error code for webapp auth 2025-04-11 02:41:02 -04:00
GareArc
4785c061a9 feat: add webapp clean up 2025-04-10 15:19:28 -04:00
GareArc
4105c8ff70 fix: bad api call 2025-04-10 06:27:00 -04:00
GareArc
b922c8c215 fix: make app private when created 2025-04-10 00:36:35 -04:00
GareArc
cbea30e65f fix: bad field name 2025-04-09 17:21:16 -04:00
GareArc
e9a207b38e fix: adjust enterprise api 2025-04-09 16:30:41 -04:00
GareArc
5e50570739 fix: update webapp jwt claim and add user accessibility support 2025-04-07 18:41:02 -04:00
GareArc
46d43e6758 feat: add web app auth 2025-04-07 17:03:26 -04:00
GareArc
1045f6db7a fix: wrong arg parsing 2025-03-26 01:37:45 -04:00
GareArc
50d36612f0 fix: bad import 2025-03-26 00:34:04 -04:00
GareArc
e38631db8a feat: add inner mail api 2025-03-25 21:47:30 -04:00
Garfield Dai
7f63cd52a2 update. 2025-03-24 23:08:54 +08:00
NFish
5b357fdbf0 Merge branch 'release/0.15.5' into e-0154 2025-03-24 16:42:11 +08:00
NFish
9283a5414f fix: update yarn.lock 2025-03-24 16:41:07 +08:00
NFish
8923e64b8d Merge branch 'release/0.15.5' into e-0154 2025-03-24 15:40:32 +08:00
-LAN-
2a2a0e9be9 fix: update DifySandbox image version to 0.2.11 in docker-compose files
Sgned-off-by: -LAN- <laipz8200@outlook.com>
2025-03-24 15:37:55 +08:00
Joel
061a765b7d fix: sanitizer svg to avoid xss (#16608) 2025-03-24 14:48:40 +08:00
-LAN-
acd7fead87 feat: remove Vanna provider and associated assets from the project
Signed-off-by: -LAN- <laipz8200@outlook.com>
2025-03-24 14:34:03 +08:00
KVOJJJin
64e9d96d84 chore: compatible with es5 (#14268) 2025-03-24 13:17:48 +08:00
NFish
d27de3818c Merge branch 'release/0.15.5' into e-0154 2025-03-24 11:46:30 +08:00
NFish
bbb080d5b2 fix: update chatbot help doc link on the create app form 2025-03-24 11:28:35 +08:00
NFish
8c025abb3b Merge branch 'release/0.15.5' into e-0154 2025-03-24 10:32:56 +08:00
NFish
c01d8a70f3 fix: upgrade nextjs to v14.2.25. a security patch for CVE-2025-29927. 2025-03-24 10:32:18 +08:00
NFish
98606ca558 fix: upgrade nextjs to v14.2.25 2025-03-24 10:12:21 +08:00
Garfield Dai
adf3e18ebd Merge tag '0.15.4' into e-0154 2025-03-21 18:29:43 +08:00
-LAN-
1ca15989e0 chore: update version to 0.15.4 in configuration and docker files
Signed-off-by: -LAN- <laipz8200@outlook.com>
2025-03-21 16:39:06 +08:00
-LAN-
8b5a3a9424 Merge branch 'release/0.15.4' of github.com:langgenius/dify into release/0.15.4 2025-03-21 16:31:06 +08:00
-LAN-
42ddcf1edd chore: remove 0.15.3 branch config in the build action
Signed-off-by: -LAN- <laipz8200@outlook.com>
2025-03-21 16:30:33 +08:00
Joel
21561df10f fix: xss in render svg (#16437) 2025-03-21 15:24:58 +08:00
Byron.wang
4327ec8c4c fix license expireAt field typo (#16428) 2025-03-21 13:43:43 +08:00
NFish
bbc5ec8301 fix: expired date calc error 2025-03-21 11:00:07 +08:00
NFish
4a51a72c1d Merge branch 'e-0154' into deploy/enterprise 2025-03-20 17:34:52 +08:00
NFish
4b6adffa8e fix: hide copyright on forgot-password/install/reset-password page 2025-03-20 17:34:19 +08:00
NFish
c7fd73d330 Merge branch 'e-0154' into deploy/enterprise 2025-03-20 10:13:09 +08:00
NFish
8a709e445a fix: remove Dify from Service API doc 2025-03-20 10:12:27 +08:00
NFish
f02b77b99f fix: Decouple login page logo component to avoid conflict with internal logo 2025-03-20 10:11:26 +08:00
GareArc
abc625bcce Merge branch 'e-0154' into deploy/enterprise 2025-03-18 22:35:39 -04:00
GareArc
b6bc1f8bc4 fix: adjust logic for branding toggle 2025-03-18 22:35:27 -04:00
NFish
b8f9037cd3 Merge branch 'e-0154' into deploy/enterprise 2025-03-18 16:13:14 +08:00
NFish
02606ba3c7 fix: cannot update webapp copyright info 2025-03-18 16:12:52 +08:00
GareArc
79311d3fb5 Merge branch 'e-0154' into deploy/enterprise 2025-03-18 03:53:18 -04:00
GareArc
31086a1fbf feat: add webapp copyright feature 2025-03-18 03:53:07 -04:00
NFish
6ae5d052e5 Merge branch 'e-0154' into deploy/enterprise 2025-03-18 14:55:36 +08:00
NFish
c794ecf101 fix: user can edit webapp copyright info only if webapp_copyright_enabled is true 2025-03-18 14:54:34 +08:00
GareArc
d887aae012 Merge branch 'e-0154' into deploy/enterprise 2025-03-18 01:55:38 -04:00
GareArc
1b1e96eff7 fix: typo 2025-03-18 01:55:27 -04:00
GareArc
eecd091063 Merge branch 'e-0154' into deploy/enterprise 2025-03-17 15:34:49 -04:00
GareArc
d38f2cb380 fix: change subject title 2025-03-17 15:34:28 -04:00
GareArc
56aaee5558 fix: wrong branding title 2025-03-17 15:01:31 -04:00
GareArc
d72b4752c9 fix: wrong title location 2025-03-17 15:00:04 -04:00
GareArc
ea769c6483 Merge branch 'e-0154' into deploy/enterprise 2025-03-17 14:24:00 -04:00
GareArc
ec194fa3d4 fix: invalid email template variables 2025-03-17 14:23:46 -04:00
NFish
b877039859 Merge branch 'e-0154' into deploy/enterprise 2025-03-17 10:37:20 +08:00
NFish
54634f26d2 fix: show copyright in webapp 2025-03-17 10:36:51 +08:00
NFish
3bef91a2cd fix: show loading icon when fetching system features 2025-03-15 12:01:30 +08:00
NFish
7da45ba589 fix: show loading icon when fetching system features 2025-03-15 12:00:22 +08:00
NFish
e0232c67cc fix: update document title and favicon in client side 2025-03-15 12:00:22 +08:00
GareArc
1dc4a229d4 Merge branch 'e-0154' into deploy/enterprise 2025-03-14 16:37:02 -04:00
GareArc
0e0bada1f3 fix: missing json keys 2025-03-14 16:36:49 -04:00
GareArc
5366a814f9 fix: update json keys 2025-03-14 16:35:05 -04:00
GareArc
f1240a22db fix: remove default value 2025-03-14 13:26:44 -04:00
NFish
66f35c2b7e Merge branch 'e-0154' into deploy/enterprise 2025-03-15 01:25:15 +08:00
NFish
766ee48531 fix: update document title and favicon in client side 2025-03-15 01:25:04 +08:00
NFish
083045f45c Merge branch 'e-0154' into deploy/enterprise 2025-03-14 20:49:17 +08:00
NFish
fe237802c9 fix: update Dify text 2025-03-14 19:10:03 +08:00
NFish
00b923651f fix: update document title with system features config 2025-03-14 19:10:03 +08:00
NFish
24fce3cc64 chore: use global zustand manage systemFeatures and share between all pages 2025-03-14 19:10:03 +08:00
GareArc
8ba969f67d fix: add ci workflow 2025-03-13 17:15:11 -04:00
GareArc
6844d59371 fix: add default title name 2025-03-13 17:07:45 -04:00
GareArc
fe5529db85 Trigger workflow 2025-03-13 17:04:13 -04:00
GareArc
d89034d913 feat: add application title 2025-03-13 15:49:04 -04:00
NFish
360fbeb108 fix: update email template, add application_title 2025-03-13 17:28:49 +08:00
GareArc
e7c2fa1cfa fix: remove system feature is_branding 2025-03-12 10:48:58 -04:00
Hash Brown
735f09d977 fix: build failed due to getPrevChatList no longer exists (#13383) 2025-03-12 10:22:33 +08:00
GareArc
f83a5e3e49 fix: wrong type 2025-03-11 07:46:48 -04:00
NFish
01a8d4efcc fix: remove dify from invite template 2025-03-11 19:25:30 +08:00
GareArc
fdb1e649d4 feat: add branding support 2025-03-11 07:14:52 -04:00
NFish
0856792a57 fix: add email templates that are no brands or logo 2025-03-11 16:03:15 +08:00
crazywoola
0e33a3aa5f chore: add ci 2025-02-19 14:34:36 +08:00
Hash Brown
d3895bcd6b revert 2025-02-19 14:32:28 +08:00
Hash Brown
eeb390650b fix: build failed 2025-02-19 14:32:28 +08:00
108 changed files with 1817 additions and 877 deletions

View File

@@ -5,6 +5,8 @@ on:
branches: branches:
- "main" - "main"
- "deploy/dev" - "deploy/dev"
- "deploy/enterprise"
- "e-0154"
release: release:
types: [published] types: [published]

29
.github/workflows/deploy-enterprise.yml vendored Normal file
View File

@@ -0,0 +1,29 @@
name: Deploy Enterprise
permissions:
contents: read
on:
workflow_run:
workflows: ["Build and Push API & Web"]
branches:
- "deploy/enterprise"
types:
- completed
jobs:
deploy:
runs-on: ubuntu-latest
if: |
github.event.workflow_run.conclusion == 'success' &&
github.event.workflow_run.head_branch == 'deploy/enterprise'
steps:
- name: Deploy to server
uses: appleboy/ssh-action@v0.1.8
with:
host: ${{ secrets.ENTERPRISE_SSH_HOST }}
username: ${{ secrets.ENTERPRISE_SSH_USER }}
password: ${{ secrets.ENTERPRISE_SSH_PASSWORD }}
script: |
${{ vars.ENTERPRISE_SSH_SCRIPT || secrets.ENTERPRISE_SSH_SCRIPT }}

View File

@@ -9,7 +9,7 @@ class PackagingInfo(BaseSettings):
CURRENT_VERSION: str = Field( CURRENT_VERSION: str = Field(
description="Dify version", description="Dify version",
default="0.15.3", default="0.15.4",
) )
COMMIT_SHA: str = Field( COMMIT_SHA: str = Field(

View File

@@ -2,30 +2,28 @@ import uuid
from typing import cast from typing import cast
from flask_login import current_user # type: ignore from flask_login import current_user # type: ignore
from flask_restful import Resource, inputs, marshal, marshal_with, reqparse # type: ignore from flask_restful import (Resource, inputs, marshal, # type: ignore
marshal_with, reqparse)
from sqlalchemy import select from sqlalchemy import select
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from werkzeug.exceptions import BadRequest, Forbidden, abort from werkzeug.exceptions import BadRequest, Forbidden, abort
from controllers.console import api from controllers.console import api
from controllers.console.app.wraps import get_app_model from controllers.console.app.wraps import get_app_model
from controllers.console.wraps import ( from controllers.console.wraps import (account_initialization_required,
account_initialization_required, cloud_edition_billing_resource_check,
cloud_edition_billing_resource_check, enterprise_license_required,
enterprise_license_required, setup_required)
setup_required,
)
from core.ops.ops_trace_manager import OpsTraceManager from core.ops.ops_trace_manager import OpsTraceManager
from extensions.ext_database import db from extensions.ext_database import db
from fields.app_fields import ( from fields.app_fields import (app_detail_fields, app_detail_fields_with_site,
app_detail_fields, app_pagination_fields)
app_detail_fields_with_site,
app_pagination_fields,
)
from libs.login import login_required from libs.login import login_required
from models import Account, App from models import Account, App
from services.app_dsl_service import AppDslService, ImportMode from services.app_dsl_service import AppDslService, ImportMode
from services.app_service import AppService from services.app_service import AppService
from services.enterprise.enterprise_service import EnterpriseService
from services.feature_service import FeatureService
ALLOW_CREATE_APP_MODES = ["chat", "agent-chat", "advanced-chat", "workflow", "completion"] ALLOW_CREATE_APP_MODES = ["chat", "agent-chat", "advanced-chat", "workflow", "completion"]
@@ -67,7 +65,17 @@ class AppListApi(Resource):
if not app_pagination: if not app_pagination:
return {"data": [], "total": 0, "page": 1, "limit": 20, "has_more": False} return {"data": [], "total": 0, "page": 1, "limit": 20, "has_more": False}
return marshal(app_pagination, app_pagination_fields) if FeatureService.get_system_features().webapp_auth.enabled:
app_ids = [str(app.id) for app in app_pagination.items]
res = EnterpriseService.WebAppAuth.batch_get_app_access_mode_by_id(app_ids=app_ids)
if len(res) != len(app_ids):
raise BadRequest("Invalid app id in webapp auth")
for app in app_pagination.items:
if str(app.id) in res:
app.access_mode = res[str(app.id)].access_mode
return marshal(app_pagination, app_pagination_fields), 200
@setup_required @setup_required
@login_required @login_required
@@ -111,6 +119,10 @@ class AppApi(Resource):
app_model = app_service.get_app(app_model) app_model = app_service.get_app(app_model)
if FeatureService.get_system_features().webapp_auth.enabled:
app_setting = EnterpriseService.WebAppAuth.get_app_access_mode_by_id(app_id=str(app_model.id))
app_model.access_mode = app_setting.access_mode
return app_model return app_model
@setup_required @setup_required

View File

@@ -6,9 +6,13 @@ from flask_restful import Resource, reqparse # type: ignore
from constants.languages import languages from constants.languages import languages
from controllers.console import api from controllers.console import api
from controllers.console.auth.error import EmailCodeError, InvalidEmailError, InvalidTokenError, PasswordMismatchError from controllers.console.auth.error import (EmailCodeError, InvalidEmailError,
from controllers.console.error import AccountInFreezeError, AccountNotFound, EmailSendIpLimitError InvalidTokenError,
from controllers.console.wraps import setup_required 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 events.tenant_event import tenant_was_created
from extensions.ext_database import db from extensions.ext_database import db
from libs.helper import email, extract_remote_ip from libs.helper import email, extract_remote_ip
@@ -22,6 +26,7 @@ from services.feature_service import FeatureService
class ForgotPasswordSendEmailApi(Resource): class ForgotPasswordSendEmailApi(Resource):
@setup_required @setup_required
@email_password_login_enabled
def post(self): def post(self):
parser = reqparse.RequestParser() parser = reqparse.RequestParser()
parser.add_argument("email", type=email, required=True, location="json") parser.add_argument("email", type=email, required=True, location="json")
@@ -53,6 +58,7 @@ class ForgotPasswordSendEmailApi(Resource):
class ForgotPasswordCheckApi(Resource): class ForgotPasswordCheckApi(Resource):
@setup_required @setup_required
@email_password_login_enabled
def post(self): def post(self):
parser = reqparse.RequestParser() parser = reqparse.RequestParser()
parser.add_argument("email", type=str, required=True, location="json") parser.add_argument("email", type=str, required=True, location="json")
@@ -72,11 +78,20 @@ class ForgotPasswordCheckApi(Resource):
if args["code"] != token_data.get("code"): if args["code"] != token_data.get("code"):
raise EmailCodeError() raise EmailCodeError()
return {"is_valid": True, "email": token_data.get("email")} # Verified, revoke the first token
AccountService.revoke_reset_password_token(args["token"])
# Refresh token data by generating a new token
_, new_token = AccountService.generate_reset_password_token(
user_email, code=args["code"], additional_data={"phase": "reset"}
)
return {"is_valid": True, "email": token_data.get("email"), "token": new_token}
class ForgotPasswordResetApi(Resource): class ForgotPasswordResetApi(Resource):
@setup_required @setup_required
@email_password_login_enabled
def post(self): def post(self):
parser = reqparse.RequestParser() parser = reqparse.RequestParser()
parser.add_argument("token", type=str, required=True, nullable=False, location="json") parser.add_argument("token", type=str, required=True, nullable=False, location="json")
@@ -95,6 +110,9 @@ class ForgotPasswordResetApi(Resource):
if reset_data is None: if reset_data is None:
raise InvalidTokenError() raise InvalidTokenError()
# Must use token in reset phase
if reset_data.get("phase", "") != "reset":
raise InvalidTokenError()
AccountService.revoke_reset_password_token(token) AccountService.revoke_reset_password_token(token)

View File

@@ -22,7 +22,7 @@ from controllers.console.error import (
EmailSendIpLimitError, EmailSendIpLimitError,
NotAllowedCreateWorkspace, NotAllowedCreateWorkspace,
) )
from controllers.console.wraps import setup_required from controllers.console.wraps import email_password_login_enabled, setup_required
from events.tenant_event import tenant_was_created from events.tenant_event import tenant_was_created
from libs.helper import email, extract_remote_ip from libs.helper import email, extract_remote_ip
from libs.password import valid_password from libs.password import valid_password
@@ -38,6 +38,7 @@ class LoginApi(Resource):
"""Resource for user login.""" """Resource for user login."""
@setup_required @setup_required
@email_password_login_enabled
def post(self): def post(self):
"""Authenticate user and login.""" """Authenticate user and login."""
parser = reqparse.RequestParser() parser = reqparse.RequestParser()
@@ -110,6 +111,7 @@ class LogoutApi(Resource):
class ResetPasswordSendEmailApi(Resource): class ResetPasswordSendEmailApi(Resource):
@setup_required @setup_required
@email_password_login_enabled
def post(self): def post(self):
parser = reqparse.RequestParser() parser = reqparse.RequestParser()
parser.add_argument("email", type=email, required=True, location="json") parser.add_argument("email", type=email, required=True, location="json")

View File

@@ -23,3 +23,9 @@ class AppSuggestedQuestionsAfterAnswerDisabledError(BaseHTTPException):
error_code = "app_suggested_questions_after_answer_disabled" error_code = "app_suggested_questions_after_answer_disabled"
description = "Function Suggested questions after answer disabled." description = "Function Suggested questions after answer disabled."
code = 403 code = 403
class AppAccessDeniedError(BaseHTTPException):
error_code = "access_denied"
description = "App access denied."
code = 403

View File

@@ -1,20 +1,26 @@
import logging
from datetime import UTC, datetime from datetime import UTC, datetime
from typing import Any from typing import Any
from flask import request from flask import request
from flask_login import current_user # type: ignore from flask_login import current_user # type: ignore
from flask_restful import Resource, inputs, marshal_with, reqparse # type: ignore from flask_restful import (Resource, inputs, marshal_with, # type: ignore
reqparse)
from sqlalchemy import and_ from sqlalchemy import and_
from werkzeug.exceptions import BadRequest, Forbidden, NotFound from werkzeug.exceptions import BadRequest, Forbidden, NotFound
from controllers.console import api from controllers.console import api
from controllers.console.explore.wraps import InstalledAppResource from controllers.console.explore.wraps import InstalledAppResource
from controllers.console.wraps import account_initialization_required, cloud_edition_billing_resource_check from controllers.console.wraps import (account_initialization_required,
cloud_edition_billing_resource_check)
from extensions.ext_database import db from extensions.ext_database import db
from fields.installed_app_fields import installed_app_list_fields from fields.installed_app_fields import installed_app_list_fields
from libs.login import login_required from libs.login import login_required
from models import App, InstalledApp, RecommendedApp from models import App, InstalledApp, RecommendedApp
from services.account_service import TenantService from services.account_service import TenantService
from services.app_service import AppService
from services.enterprise.enterprise_service import EnterpriseService
from services.feature_service import FeatureService
class InstalledAppsListApi(Resource): class InstalledAppsListApi(Resource):
@@ -48,6 +54,23 @@ class InstalledAppsListApi(Resource):
for installed_app in installed_apps for installed_app in installed_apps
if installed_app.app is not None if installed_app.app is not None
] ]
# filter out apps that user doesn't have access to
if FeatureService.get_system_features().webapp_auth.enabled:
user_id = current_user.id
res = []
for installed_app in installed_app_list:
app_code = AppService.get_app_code_by_id(str(installed_app["app"].id))
if EnterpriseService.WebAppAuth.is_user_allowed_to_access_webapp(
user_id=user_id,
app_code=app_code,
):
res.append(installed_app)
installed_app_list = res
logging.info(
f"installed_app_list: {installed_app_list}, user_id: {user_id}"
)
installed_app_list.sort( installed_app_list.sort(
key=lambda app: ( key=lambda app: (
-app["is_pinned"], -app["is_pinned"],

View File

@@ -4,10 +4,14 @@ from flask_login import current_user # type: ignore
from flask_restful import Resource # type: ignore from flask_restful import Resource # type: ignore
from werkzeug.exceptions import NotFound from werkzeug.exceptions import NotFound
from controllers.console.explore.error import AppAccessDeniedError
from controllers.console.wraps import account_initialization_required from controllers.console.wraps import account_initialization_required
from extensions.ext_database import db from extensions.ext_database import db
from libs.login import login_required from libs.login import login_required
from models import InstalledApp from models import InstalledApp
from services.app_service import AppService
from services.enterprise.enterprise_service import EnterpriseService
from services.feature_service import FeatureService
def installed_app_required(view=None): def installed_app_required(view=None):
@@ -48,6 +52,30 @@ def installed_app_required(view=None):
return decorator return decorator
def user_allowed_to_access_app(view=None):
def decorator(view):
@wraps(view)
def decorated(installed_app: InstalledApp, *args, **kwargs):
feature = FeatureService.get_system_features()
if feature.webapp_auth.enabled:
app_id = installed_app.app_id
app_code = AppService.get_app_code_by_id(app_id)
res = EnterpriseService.WebAppAuth.is_user_allowed_to_access_webapp(
user_id=str(current_user.id),
app_code=app_code,
)
if not res:
raise AppAccessDeniedError()
return view(installed_app, *args, **kwargs)
return decorated
if view:
return decorator(view)
return decorator
class InstalledAppResource(Resource): class InstalledAppResource(Resource):
# must be reversed if there are multiple decorators # must be reversed if there are multiple decorators
method_decorators = [installed_app_required, account_initialization_required, login_required]
method_decorators = [user_allowed_to_access_app, installed_app_required, account_initialization_required, login_required]

View File

@@ -11,7 +11,8 @@ from models.model import DifySetup
from services.feature_service import FeatureService, LicenseStatus from services.feature_service import FeatureService, LicenseStatus
from services.operation_service import OperationService from services.operation_service import OperationService
from .error import NotInitValidateError, NotSetupError, UnauthorizedAndForceLogout from .error import (NotInitValidateError, NotSetupError,
UnauthorizedAndForceLogout)
def account_initialization_required(view): def account_initialization_required(view):
@@ -39,6 +40,17 @@ def only_edition_cloud(view):
return decorated return decorated
def only_enterprise_edition(view):
@wraps(view)
def decorated(*args, **kwargs):
if not dify_config.ENTERPRISE_ENABLED:
abort(404)
return view(*args, **kwargs)
return decorated
def only_edition_self_hosted(view): def only_edition_self_hosted(view):
@wraps(view) @wraps(view)
def decorated(*args, **kwargs): def decorated(*args, **kwargs):
@@ -154,3 +166,16 @@ def enterprise_license_required(view):
return view(*args, **kwargs) return view(*args, **kwargs)
return decorated return decorated
def email_password_login_enabled(view):
@wraps(view)
def decorated(*args, **kwargs):
features = FeatureService.get_system_features()
if features.enable_email_password_login:
return view(*args, **kwargs)
# otherwise, return 403
abort(403)
return decorated

View File

@@ -5,4 +5,5 @@ from libs.external_api import ExternalApi
bp = Blueprint("inner_api", __name__, url_prefix="/inner/api") bp = Blueprint("inner_api", __name__, url_prefix="/inner/api")
api = ExternalApi(bp) api = ExternalApi(bp)
from . import mail
from .workspace import workspace from .workspace import workspace

View File

@@ -0,0 +1,27 @@
from flask_restful import (
Resource, # type: ignore
reqparse,
)
from controllers.console.wraps import setup_required
from controllers.inner_api import api
from controllers.inner_api.wraps import inner_api_only
from services.enterprise.mail_service import DifyMail, EnterpriseMailService
class EnterpriseMail(Resource):
@setup_required
@inner_api_only
def post(self):
parser = reqparse.RequestParser()
parser.add_argument("to", type=str, action="append", required=True)
parser.add_argument("subject", type=str, required=True)
parser.add_argument("body", type=str, required=True)
parser.add_argument("substitutions", type=dict, required=False)
args = parser.parse_args()
EnterpriseMailService.send_mail(DifyMail(**args))
return {"message": "success"}, 200
api.add_resource(EnterpriseMail, "/enterprise/mail")

View File

@@ -1,12 +1,16 @@
from flask_restful import marshal_with # type: ignore
from flask import request
from flask_restful import Resource, marshal_with, reqparse # type: ignore
from controllers.common import fields from controllers.common import fields
from controllers.common import helpers as controller_helpers from controllers.common import helpers as controller_helpers
from controllers.web import api from controllers.web import api
from controllers.web.error import AppUnavailableError from controllers.web.error import AppUnavailableError
from controllers.web.wraps import WebApiResource from controllers.web.wraps import WebApiResource
from libs.passport import PassportService
from models.model import App, AppMode from models.model import App, AppMode
from services.app_service import AppService from services.app_service import AppService
from services.enterprise.enterprise_service import EnterpriseService
class AppParameterApi(WebApiResource): class AppParameterApi(WebApiResource):
@@ -42,5 +46,51 @@ class AppMeta(WebApiResource):
return AppService().get_app_meta(app_model) return AppService().get_app_meta(app_model)
class AppAccessMode(Resource):
def get(self):
parser = reqparse.RequestParser()
parser.add_argument("appId", type=str, required=True, location="args")
args = parser.parse_args()
app_id = args["appId"]
res = EnterpriseService.WebAppAuth.get_app_access_mode_by_id(app_id)
return {"accessMode": res.access_mode}
class AppWebAuthPermission(Resource):
def get(self):
user_id = "visitor"
try:
auth_header = request.headers.get("Authorization")
if auth_header is None:
raise
if " " not in auth_header:
raise
auth_scheme, tk = auth_header.split(None, 1)
auth_scheme = auth_scheme.lower()
if auth_scheme != "bearer":
raise
decoded = PassportService().verify(tk)
user_id = decoded.get("user_id", "visitor")
except Exception as e:
pass
parser = reqparse.RequestParser()
parser.add_argument("appId", type=str, required=True, location="args")
args = parser.parse_args()
app_id = args["appId"]
app_code = AppService.get_app_code_by_id(app_id)
res = EnterpriseService.WebAppAuth.is_user_allowed_to_access_webapp(str(user_id), app_code)
return {"result": res}
api.add_resource(AppParameterApi, "/parameters") api.add_resource(AppParameterApi, "/parameters")
api.add_resource(AppMeta, "/meta") api.add_resource(AppMeta, "/meta")
# webapp auth apis
api.add_resource(AppAccessMode, "/webapp/access-mode")
api.add_resource(AppWebAuthPermission, "/webapp/permission")

View File

@@ -121,9 +121,15 @@ class UnsupportedFileTypeError(BaseHTTPException):
code = 415 code = 415
class WebSSOAuthRequiredError(BaseHTTPException): class WebAppAuthRequiredError(BaseHTTPException):
error_code = "web_sso_auth_required" error_code = "web_sso_auth_required"
description = "Web SSO authentication required." description = "Web app authentication required."
code = 401
class WebAppAuthAccessDeniedError(BaseHTTPException):
error_code = "web_app_access_denied"
description = "You do not have permission to access this web app."
code = 401 code = 401

View File

@@ -0,0 +1,121 @@
from flask import request
from flask_restful import Resource, reqparse
from jwt import InvalidTokenError # type: ignore
from web import api
from werkzeug.exceptions import BadRequest
import services
from controllers.console.auth.error import EmailCodeError, EmailOrPasswordMismatchError, InvalidEmailError
from controllers.console.error import AccountBannedError, AccountNotFound
from controllers.console.wraps import setup_required
from libs.helper import email
from libs.password import valid_password
from services.account_service import AccountService
from services.webapp_auth_service import WebAppAuthService
class LoginApi(Resource):
"""Resource for web app email/password login."""
def post(self):
"""Authenticate user and login."""
parser = reqparse.RequestParser()
parser.add_argument("email", type=email, required=True, location="json")
parser.add_argument("password", type=valid_password, required=True, location="json")
args = parser.parse_args()
app_code = request.headers.get("X-App-Code")
if app_code is None:
raise BadRequest("X-App-Code header is missing.")
try:
account = WebAppAuthService.authenticate(args["email"], args["password"])
except services.errors.account.AccountLoginError:
raise AccountBannedError()
except services.errors.account.AccountPasswordError:
raise EmailOrPasswordMismatchError()
except services.errors.account.AccountNotFoundError:
raise AccountNotFound()
WebAppAuthService._validate_user_accessibility(account=account, app_code=app_code)
end_user = WebAppAuthService.create_end_user(email=args["email"], app_code=app_code)
token = WebAppAuthService.login(account=account, app_code=app_code, end_user_id=end_user.id)
return {"result": "success", "token": token}
# class LogoutApi(Resource):
# @setup_required
# def get(self):
# account = cast(Account, flask_login.current_user)
# if isinstance(account, flask_login.AnonymousUserMixin):
# return {"result": "success"}
# flask_login.logout_user()
# return {"result": "success"}
class EmailCodeLoginSendEmailApi(Resource):
@setup_required
def post(self):
parser = reqparse.RequestParser()
parser.add_argument("email", type=email, required=True, location="json")
parser.add_argument("language", type=str, required=False, location="json")
args = parser.parse_args()
if args["language"] is not None and args["language"] == "zh-Hans":
language = "zh-Hans"
else:
language = "en-US"
account = WebAppAuthService.get_user_through_email(args["email"])
if account is None:
raise AccountNotFound()
else:
token = WebAppAuthService.send_email_code_login_email(account=account, language=language)
return {"result": "success", "data": token}
class EmailCodeLoginApi(Resource):
@setup_required
def post(self):
parser = reqparse.RequestParser()
parser.add_argument("email", type=str, required=True, location="json")
parser.add_argument("code", type=str, required=True, location="json")
parser.add_argument("token", type=str, required=True, location="json")
args = parser.parse_args()
user_email = args["email"]
app_code = request.headers.get("X-App-Code")
if app_code is None:
raise BadRequest("X-App-Code header is missing.")
token_data = WebAppAuthService.get_email_code_login_data(args["token"])
if token_data is None:
raise InvalidTokenError()
if token_data["email"] != args["email"]:
raise InvalidEmailError()
if token_data["code"] != args["code"]:
raise EmailCodeError()
WebAppAuthService.revoke_email_code_login_token(args["token"])
account = WebAppAuthService.get_user_through_email(user_email)
if not account:
raise AccountNotFound()
WebAppAuthService._validate_user_accessibility(account=account, app_code=app_code)
end_user = WebAppAuthService.create_end_user(email=user_email, app_code=app_code)
token = WebAppAuthService.login(account=account, app_code=app_code, end_user_id=end_user.id)
AccountService.reset_login_error_rate_limit(args["email"])
return {"result": "success", "token": token}
api.add_resource(LoginApi, "/login")
# api.add_resource(LogoutApi, "/logout")
api.add_resource(EmailCodeLoginSendEmailApi, "/email-code-login")
api.add_resource(EmailCodeLoginApi, "/email-code-login/validity")

View File

@@ -5,7 +5,7 @@ from flask_restful import Resource # type: ignore
from werkzeug.exceptions import NotFound, Unauthorized from werkzeug.exceptions import NotFound, Unauthorized
from controllers.web import api from controllers.web import api
from controllers.web.error import WebSSOAuthRequiredError from controllers.web.error import WebAppAuthRequiredError
from extensions.ext_database import db from extensions.ext_database import db
from libs.passport import PassportService from libs.passport import PassportService
from models.model import App, EndUser, Site from models.model import App, EndUser, Site
@@ -22,10 +22,10 @@ class PassportResource(Resource):
if app_code is None: if app_code is None:
raise Unauthorized("X-App-Code header is missing.") raise Unauthorized("X-App-Code header is missing.")
if system_features.sso_enforced_for_web: if system_features.webapp_auth.enabled:
app_web_sso_enabled = EnterpriseService.get_app_web_sso_enabled(app_code).get("enabled", False) app_settings = EnterpriseService.WebAppAuth.get_app_access_mode_by_code(app_code=app_code)
if app_web_sso_enabled: if not app_settings or not app_settings.access_mode == "public":
raise WebSSOAuthRequiredError() raise WebAppAuthRequiredError()
# get site from db and check if it is normal # get site from db and check if it is normal
site = db.session.query(Site).filter(Site.code == app_code, Site.status == "normal").first() site = db.session.query(Site).filter(Site.code == app_code, Site.status == "normal").first()

View File

@@ -4,7 +4,7 @@ from flask import request
from flask_restful import Resource # type: ignore from flask_restful import Resource # type: ignore
from werkzeug.exceptions import BadRequest, NotFound, Unauthorized from werkzeug.exceptions import BadRequest, NotFound, Unauthorized
from controllers.web.error import WebSSOAuthRequiredError from controllers.web.error import WebAppAuthAccessDeniedError, WebAppAuthRequiredError
from extensions.ext_database import db from extensions.ext_database import db
from libs.passport import PassportService from libs.passport import PassportService
from models.model import App, EndUser, Site from models.model import App, EndUser, Site
@@ -57,35 +57,53 @@ def decode_jwt_token():
if not end_user: if not end_user:
raise NotFound() raise NotFound()
_validate_web_sso_token(decoded, system_features, app_code) # for enterprise webapp auth
app_web_auth_enabled = False
if system_features.webapp_auth.enabled:
app_web_auth_enabled = (
EnterpriseService.WebAppAuth.get_app_access_mode_by_code(app_code=app_code).access_mode != "public"
)
_validate_webapp_token(decoded, app_web_auth_enabled, system_features.webapp_auth.enabled)
_validate_user_accessibility(decoded, app_code, app_web_auth_enabled, system_features.webapp_auth.enabled)
return app_model, end_user return app_model, end_user
except Unauthorized as e: except Unauthorized as e:
if system_features.sso_enforced_for_web: if system_features.webapp_auth.enabled:
app_web_sso_enabled = EnterpriseService.get_app_web_sso_enabled(app_code).get("enabled", False) app_web_auth_enabled = (
if app_web_sso_enabled: EnterpriseService.WebAppAuth.get_app_access_mode_by_code(app_code=app_code).access_mode != "public"
raise WebSSOAuthRequiredError() )
if app_web_auth_enabled:
raise WebAppAuthRequiredError()
raise Unauthorized(e.description) raise Unauthorized(e.description)
def _validate_web_sso_token(decoded, system_features, app_code): def _validate_webapp_token(decoded, app_web_auth_enabled: bool, system_webapp_auth_enabled: bool):
app_web_sso_enabled = False # Check if authentication is enforced for web app, and if the token source is not webapp,
# raise an error and redirect to login
# Check if SSO is enforced for web, and if the token source is not SSO, raise an error and redirect to SSO login if system_webapp_auth_enabled and app_web_auth_enabled:
if system_features.sso_enforced_for_web:
app_web_sso_enabled = EnterpriseService.get_app_web_sso_enabled(app_code).get("enabled", False)
if app_web_sso_enabled:
source = decoded.get("token_source")
if not source or source != "sso":
raise WebSSOAuthRequiredError()
# Check if SSO is not enforced for web, and if the token source is SSO,
# raise an error and redirect to normal passport login
if not system_features.sso_enforced_for_web or not app_web_sso_enabled:
source = decoded.get("token_source") source = decoded.get("token_source")
if source and source == "sso": if not source or source != "webapp":
raise Unauthorized("sso token expired.") raise WebAppAuthRequiredError()
# Check if authentication is not enforced for web, and if the token source is webapp,
# raise an error and redirect to normal passport login
if not system_webapp_auth_enabled or not app_web_auth_enabled:
source = decoded.get("token_source")
if source and source == "webapp":
raise Unauthorized("webapp token expired.")
def _validate_user_accessibility(decoded, app_code, app_web_auth_enabled: bool, system_webapp_auth_enabled: bool):
if system_webapp_auth_enabled and app_web_auth_enabled:
# Check if the user is allowed to access the web app
user_id = decoded.get("user_id")
if not user_id:
raise WebAppAuthRequiredError()
if not EnterpriseService.WebAppAuth.is_user_allowed_to_access_webapp(user_id, app_code=app_code):
raise WebAppAuthAccessDeniedError()
class WebApiResource(Resource): class WebApiResource(Resource):

View File

@@ -77,5 +77,4 @@
- onebot - onebot
- regex - regex
- trello - trello
- vanna
- fal - fal

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.5 KiB

View File

@@ -1,134 +0,0 @@
from typing import Any, Union
from vanna.remote import VannaDefault # type: ignore
from core.tools.entities.tool_entities import ToolInvokeMessage
from core.tools.errors import ToolProviderCredentialValidationError
from core.tools.tool.builtin_tool import BuiltinTool
class VannaTool(BuiltinTool):
def _invoke(
self, user_id: str, tool_parameters: dict[str, Any]
) -> Union[ToolInvokeMessage, list[ToolInvokeMessage]]:
"""
invoke tools
"""
# Ensure runtime and credentials
if not self.runtime or not self.runtime.credentials:
raise ToolProviderCredentialValidationError("Tool runtime or credentials are missing")
api_key = self.runtime.credentials.get("api_key", None)
if not api_key:
raise ToolProviderCredentialValidationError("Please input api key")
model = tool_parameters.get("model", "")
if not model:
return self.create_text_message("Please input RAG model")
prompt = tool_parameters.get("prompt", "")
if not prompt:
return self.create_text_message("Please input prompt")
url = tool_parameters.get("url", "")
if not url:
return self.create_text_message("Please input URL/Host/DSN")
db_name = tool_parameters.get("db_name", "")
username = tool_parameters.get("username", "")
password = tool_parameters.get("password", "")
port = tool_parameters.get("port", 0)
base_url = self.runtime.credentials.get("base_url", None)
vn = VannaDefault(model=model, api_key=api_key, config={"endpoint": base_url})
db_type = tool_parameters.get("db_type", "")
if db_type in {"Postgres", "MySQL", "Hive", "ClickHouse"}:
if not db_name:
return self.create_text_message("Please input database name")
if not username:
return self.create_text_message("Please input username")
if port < 1:
return self.create_text_message("Please input port")
schema_sql = "SELECT * FROM INFORMATION_SCHEMA.COLUMNS"
match db_type:
case "SQLite":
schema_sql = "SELECT type, sql FROM sqlite_master WHERE sql is not null"
vn.connect_to_sqlite(url)
case "Postgres":
vn.connect_to_postgres(host=url, dbname=db_name, user=username, password=password, port=port)
case "DuckDB":
vn.connect_to_duckdb(url=url)
case "SQLServer":
vn.connect_to_mssql(url)
case "MySQL":
vn.connect_to_mysql(host=url, dbname=db_name, user=username, password=password, port=port)
case "Oracle":
vn.connect_to_oracle(user=username, password=password, dsn=url)
case "Hive":
vn.connect_to_hive(host=url, dbname=db_name, user=username, password=password, port=port)
case "ClickHouse":
vn.connect_to_clickhouse(host=url, dbname=db_name, user=username, password=password, port=port)
enable_training = tool_parameters.get("enable_training", False)
reset_training_data = tool_parameters.get("reset_training_data", False)
if enable_training:
if reset_training_data:
existing_training_data = vn.get_training_data()
if len(existing_training_data) > 0:
for _, training_data in existing_training_data.iterrows():
vn.remove_training_data(training_data["id"])
ddl = tool_parameters.get("ddl", "")
question = tool_parameters.get("question", "")
sql = tool_parameters.get("sql", "")
memos = tool_parameters.get("memos", "")
training_metadata = tool_parameters.get("training_metadata", False)
if training_metadata:
if db_type == "SQLite":
df_ddl = vn.run_sql(schema_sql)
for ddl in df_ddl["sql"].to_list():
vn.train(ddl=ddl)
else:
df_information_schema = vn.run_sql(schema_sql)
plan = vn.get_training_plan_generic(df_information_schema)
vn.train(plan=plan)
if ddl:
vn.train(ddl=ddl)
if sql:
if question:
vn.train(question=question, sql=sql)
else:
vn.train(sql=sql)
if memos:
vn.train(documentation=memos)
#########################################################################################
# Due to CVE-2024-5565, we have to disable the chart generation feature
# The Vanna library uses a prompt function to present the user with visualized results,
# it is possible to alter the prompt using prompt injection and run arbitrary Python code
# instead of the intended visualization code.
# Specifically - allowing external input to the librarys “ask” method
# with "visualize" set to True (default behavior) leads to remote code execution.
# Affected versions: <= 0.5.5
#########################################################################################
allow_llm_to_see_data = tool_parameters.get("allow_llm_to_see_data", False)
res = vn.ask(
prompt, print_results=False, auto_train=True, visualize=False, allow_llm_to_see_data=allow_llm_to_see_data
)
result = []
if res is not None:
result.append(self.create_text_message(res[0]))
if len(res) > 1 and res[1] is not None:
result.append(self.create_text_message(res[1].to_markdown()))
if len(res) > 2 and res[2] is not None:
result.append(
self.create_blob_message(blob=res[2].to_image(format="svg"), meta={"mime_type": "image/svg+xml"})
)
return result

View File

@@ -1,213 +0,0 @@
identity:
name: vanna
author: QCTC
label:
en_US: Vanna.AI
zh_Hans: Vanna.AI
description:
human:
en_US: The fastest way to get actionable insights from your database just by asking questions.
zh_Hans: 一个基于大模型和RAG的Text2SQL工具。
llm: A tool for converting text to SQL.
parameters:
- name: prompt
type: string
required: true
label:
en_US: Prompt
zh_Hans: 提示词
pt_BR: Prompt
human_description:
en_US: used for generating SQL
zh_Hans: 用于生成SQL
llm_description: key words for generating SQL
form: llm
- name: model
type: string
required: true
label:
en_US: RAG Model
zh_Hans: RAG模型
human_description:
en_US: RAG Model for your database DDL
zh_Hans: 存储数据库训练数据的RAG模型
llm_description: RAG Model for generating SQL
form: llm
- name: db_type
type: select
required: true
options:
- value: SQLite
label:
en_US: SQLite
zh_Hans: SQLite
- value: Postgres
label:
en_US: Postgres
zh_Hans: Postgres
- value: DuckDB
label:
en_US: DuckDB
zh_Hans: DuckDB
- value: SQLServer
label:
en_US: Microsoft SQL Server
zh_Hans: 微软 SQL Server
- value: MySQL
label:
en_US: MySQL
zh_Hans: MySQL
- value: Oracle
label:
en_US: Oracle
zh_Hans: Oracle
- value: Hive
label:
en_US: Hive
zh_Hans: Hive
- value: ClickHouse
label:
en_US: ClickHouse
zh_Hans: ClickHouse
default: SQLite
label:
en_US: DB Type
zh_Hans: 数据库类型
human_description:
en_US: Database type.
zh_Hans: 选择要链接的数据库类型。
form: form
- name: url
type: string
required: true
label:
en_US: URL/Host/DSN
zh_Hans: URL/Host/DSN
human_description:
en_US: Please input depending on DB type, visit https://vanna.ai/docs/ for more specification
zh_Hans: 请根据数据库类型填入对应值详情参考https://vanna.ai/docs/
form: form
- name: db_name
type: string
required: false
label:
en_US: DB name
zh_Hans: 数据库名
human_description:
en_US: Database name
zh_Hans: 数据库名
form: form
- name: username
type: string
required: false
label:
en_US: Username
zh_Hans: 用户名
human_description:
en_US: Username
zh_Hans: 用户名
form: form
- name: password
type: secret-input
required: false
label:
en_US: Password
zh_Hans: 密码
human_description:
en_US: Password
zh_Hans: 密码
form: form
- name: port
type: number
required: false
label:
en_US: Port
zh_Hans: 端口
human_description:
en_US: Port
zh_Hans: 端口
form: form
- name: ddl
type: string
required: false
label:
en_US: Training DDL
zh_Hans: 训练DDL
human_description:
en_US: DDL statements for training data
zh_Hans: 用于训练RAG Model的建表语句
form: llm
- name: question
type: string
required: false
label:
en_US: Training Question
zh_Hans: 训练问题
human_description:
en_US: Question-SQL Pairs
zh_Hans: Question-SQL中的问题
form: llm
- name: sql
type: string
required: false
label:
en_US: Training SQL
zh_Hans: 训练SQL
human_description:
en_US: SQL queries to your training data
zh_Hans: 用于训练RAG Model的SQL语句
form: llm
- name: memos
type: string
required: false
label:
en_US: Training Memos
zh_Hans: 训练说明
human_description:
en_US: Sometimes you may want to add documentation about your business terminology or definitions
zh_Hans: 添加更多关于数据库的业务说明
form: llm
- name: enable_training
type: boolean
required: false
default: false
label:
en_US: Training Data
zh_Hans: 训练数据
human_description:
en_US: You only need to train once. Do not train again unless you want to add more training data
zh_Hans: 训练数据无更新时,训练一次即可
form: form
- name: reset_training_data
type: boolean
required: false
default: false
label:
en_US: Reset Training Data
zh_Hans: 重置训练数据
human_description:
en_US: Remove all training data in the current RAG Model
zh_Hans: 删除当前RAG Model中的所有训练数据
form: form
- name: training_metadata
type: boolean
required: false
default: false
label:
en_US: Training Metadata
zh_Hans: 训练元数据
human_description:
en_US: If enabled, it will attempt to train on the metadata of that database
zh_Hans: 是否自动从数据库获取元数据来训练
form: form
- name: allow_llm_to_see_data
type: boolean
required: false
default: false
label:
en_US: Whether to allow the LLM to see the data
zh_Hans: 是否允许LLM查看数据
human_description:
en_US: Whether to allow the LLM to see the data
zh_Hans: 是否允许LLM查看数据
form: form

View File

@@ -1,46 +0,0 @@
import re
from typing import Any
from urllib.parse import urlparse
from core.tools.errors import ToolProviderCredentialValidationError
from core.tools.provider.builtin.vanna.tools.vanna import VannaTool
from core.tools.provider.builtin_tool_provider import BuiltinToolProviderController
class VannaProvider(BuiltinToolProviderController):
def _get_protocol_and_main_domain(self, url):
parsed_url = urlparse(url)
protocol = parsed_url.scheme
hostname = parsed_url.hostname
port = f":{parsed_url.port}" if parsed_url.port else ""
# Check if the hostname is an IP address
is_ip = re.match(r"^\d{1,3}(\.\d{1,3}){3}$", hostname) is not None
# Return the full hostname (with port if present) for IP addresses, otherwise return the main domain
main_domain = f"{hostname}{port}" if is_ip else ".".join(hostname.split(".")[-2:]) + port
return f"{protocol}://{main_domain}"
def _validate_credentials(self, credentials: dict[str, Any]) -> None:
base_url = credentials.get("base_url")
if not base_url:
base_url = "https://ask.vanna.ai/rpc"
else:
base_url = base_url.removesuffix("/")
credentials["base_url"] = base_url
try:
VannaTool().fork_tool_runtime(
runtime={
"credentials": credentials,
}
).invoke(
user_id="",
tool_parameters={
"model": "chinook",
"db_type": "SQLite",
"url": f"{self._get_protocol_and_main_domain(credentials['base_url'])}/Chinook.sqlite",
"query": "What are the top 10 customers by sales?",
},
)
except Exception as e:
raise ToolProviderCredentialValidationError(str(e))

View File

@@ -1,35 +0,0 @@
identity:
author: QCTC
name: vanna
label:
en_US: Vanna.AI
zh_Hans: Vanna.AI
description:
en_US: The fastest way to get actionable insights from your database just by asking questions.
zh_Hans: 一个基于大模型和RAG的Text2SQL工具。
icon: icon.png
tags:
- utilities
- productivity
credentials_for_provider:
api_key:
type: secret-input
required: true
label:
en_US: API key
zh_Hans: API key
placeholder:
en_US: Please input your API key
zh_Hans: 请输入你的 API key
pt_BR: Please input your API key
help:
en_US: Get your API key from Vanna.AI
zh_Hans: 从 Vanna.AI 获取你的 API key
url: https://vanna.ai/account/profile
base_url:
type: text-input
required: false
label:
en_US: Vanna.AI Endpoint Base URL
placeholder:
en_US: https://ask.vanna.ai/rpc

View File

@@ -63,6 +63,7 @@ app_detail_fields = {
"created_at": TimestampField, "created_at": TimestampField,
"updated_by": fields.String, "updated_by": fields.String,
"updated_at": TimestampField, "updated_at": TimestampField,
"access_mode": fields.String,
} }
prompt_config_fields = { prompt_config_fields = {
@@ -98,6 +99,7 @@ app_partial_fields = {
"updated_by": fields.String, "updated_by": fields.String,
"updated_at": TimestampField, "updated_at": TimestampField,
"tags": fields.List(fields.Nested(tag_fields)), "tags": fields.List(fields.Nested(tag_fields)),
"access_mode": fields.String,
} }
@@ -170,6 +172,7 @@ app_detail_fields_with_site = {
"updated_by": fields.String, "updated_by": fields.String,
"updated_at": TimestampField, "updated_at": TimestampField,
"deleted_tools": fields.List(fields.String), "deleted_tools": fields.List(fields.String),
"access_mode": fields.String,
} }
app_site_fields = { app_site_fields = {

View File

@@ -406,10 +406,8 @@ class AccountService:
raise PasswordResetRateLimitExceededError() raise PasswordResetRateLimitExceededError()
code = "".join([str(random.randint(0, 9)) for _ in range(6)]) code, token = cls.generate_reset_password_token(account_email, account)
token = TokenManager.generate_token(
account=account, email=email, token_type="reset_password", additional_data={"code": code}
)
send_reset_password_mail_task.delay( send_reset_password_mail_task.delay(
language=language, language=language,
to=account_email, to=account_email,
@@ -418,6 +416,22 @@ class AccountService:
cls.reset_password_rate_limiter.increment_rate_limit(account_email) cls.reset_password_rate_limiter.increment_rate_limit(account_email)
return token return token
@classmethod
def generate_reset_password_token(
cls,
email: str,
account: Optional[Account] = None,
code: Optional[str] = None,
additional_data: dict[str, Any] = {},
):
if not code:
code = "".join([str(random.randint(0, 9)) for _ in range(6)])
additional_data["code"] = code
token = TokenManager.generate_token(
account=account, email=email, token_type="reset_password", additional_data=additional_data
)
return code, token
@classmethod @classmethod
def revoke_reset_password_token(cls, token: str): def revoke_reset_password_token(cls, token: str):
TokenManager.revoke_token(token, "reset_password") TokenManager.revoke_token(token, "reset_password")

View File

@@ -19,8 +19,10 @@ from core.tools.utils.configuration import ToolParameterConfigurationManager
from events.app_event import app_was_created from events.app_event import app_was_created
from extensions.ext_database import db from extensions.ext_database import db
from models.account import Account from models.account import Account
from models.model import App, AppMode, AppModelConfig from models.model import App, AppMode, AppModelConfig, Site
from models.tools import ApiToolProvider from models.tools import ApiToolProvider
from services.enterprise.enterprise_service import EnterpriseService
from services.feature_service import FeatureService
from services.tag_service import TagService from services.tag_service import TagService
from tasks.remove_app_and_related_data_task import remove_app_and_related_data_task from tasks.remove_app_and_related_data_task import remove_app_and_related_data_task
@@ -152,6 +154,10 @@ class AppService:
app_was_created.send(app, account=account) app_was_created.send(app, account=account)
if FeatureService.get_system_features().webapp_auth.enabled:
# update web app setting as private
EnterpriseService.WebAppAuth.update_app_access_mode(app.id, "private")
return app return app
def get_app(self, app: App) -> App: def get_app(self, app: App) -> App:
@@ -308,6 +314,10 @@ class AppService:
db.session.delete(app) db.session.delete(app)
db.session.commit() db.session.commit()
# clean up web app settings
if FeatureService.get_system_features().webapp_auth.enabled:
EnterpriseService.WebAppAuth.cleanup_webapp(app.id)
# Trigger asynchronous deletion of app and related data # Trigger asynchronous deletion of app and related data
remove_app_and_related_data_task.delay(tenant_id=app.tenant_id, app_id=app.id) remove_app_and_related_data_task.delay(tenant_id=app.tenant_id, app_id=app.id)
@@ -374,3 +384,15 @@ class AppService:
meta["tool_icons"][tool_name] = {"background": "#252525", "content": "\ud83d\ude01"} meta["tool_icons"][tool_name] = {"background": "#252525", "content": "\ud83d\ude01"}
return meta return meta
@staticmethod
def get_app_code_by_id(app_id: str) -> str:
"""
Get app code by app id
:param app_id: app id
:return: app code
"""
site = db.session.query(Site).filter(Site.app_id == app_id).first()
if not site:
raise ValueError(f"App with id {app_id} not found")
return str(site.code)

View File

@@ -1,11 +1,87 @@
from pydantic import BaseModel, Field
from services.enterprise.base import EnterpriseRequest from services.enterprise.base import EnterpriseRequest
class WebAppSettings(BaseModel):
access_mode: str = Field(
description="Access mode for the web app. Can be 'public' or 'private'",
default="private",
alias="accessMode",
)
class EnterpriseService: class EnterpriseService:
@classmethod @classmethod
def get_info(cls): def get_info(cls):
return EnterpriseRequest.send_request("GET", "/info") return EnterpriseRequest.send_request("GET", "/info")
@classmethod class WebAppAuth:
def get_app_web_sso_enabled(cls, app_code): @classmethod
return EnterpriseRequest.send_request("GET", f"/app-sso-setting?appCode={app_code}") def is_user_allowed_to_access_webapp(cls, user_id: str, app_code: str) -> bool:
params = {"userId": user_id, "appCode": app_code}
data = EnterpriseRequest.send_request("GET", "/webapp/permission", params=params)
return data.get("result", False)
@classmethod
def get_app_access_mode_by_id(cls, app_id: str) -> WebAppSettings:
if not app_id:
raise ValueError("app_id must be provided.")
params = {"appId": app_id}
data = EnterpriseRequest.send_request("GET", "/webapp/access-mode/id", params=params)
if not data:
raise ValueError("No data found.")
return WebAppSettings(**data)
@classmethod
def batch_get_app_access_mode_by_id(cls, app_ids: list[str]) -> dict[str, WebAppSettings]:
if not app_ids:
return {}
body = {"appIds": app_ids}
data: dict[str, str] = EnterpriseRequest.send_request("POST", "/webapp/access-mode/batch/id", json=body)
if not data:
raise ValueError("No data found.")
if not isinstance(data["accessModes"], dict):
raise ValueError("Invalid data format.")
ret = {}
for key, value in data["accessModes"].items():
curr = WebAppSettings()
curr.access_mode = value
ret[key] = curr
return ret
@classmethod
def get_app_access_mode_by_code(cls, app_code: str) -> WebAppSettings:
if not app_code:
raise ValueError("app_code must be provided.")
params = {"appCode": app_code}
data = EnterpriseRequest.send_request("GET", "/webapp/access-mode/code", params=params)
if not data:
raise ValueError("No data found.")
return WebAppSettings(**data)
@classmethod
def update_app_access_mode(cls, app_id: str, access_mode: str) -> bool:
if not app_id:
raise ValueError("app_id must be provided.")
if access_mode not in ["public", "private", "private_all"]:
raise ValueError("access_mode must be either 'public', 'private', or 'private_all'")
data = {"appId": app_id, "accessMode": access_mode}
response = EnterpriseRequest.send_request("POST", "/webapp/access-mode", json=data)
return response.get("result", False)
@classmethod
def cleanup_webapp(cls, app_id: str):
if not app_id:
raise ValueError("app_id must be provided.")
body = {"appId": app_id}
EnterpriseRequest.send_request("DELETE", "/webapp/clean", json=body)

View File

@@ -0,0 +1,18 @@
from pydantic import BaseModel
from tasks.mail_enterprise_task import send_enterprise_email_task
class DifyMail(BaseModel):
to: list[str]
subject: str
body: str
substitutions: dict[str, str] = {}
class EnterpriseMailService:
@classmethod
def send_mail(cls, mail: DifyMail):
send_enterprise_email_task.delay(
to=mail.to, subject=mail.subject, body=mail.body, substitutions=mail.substitutions
)

View File

@@ -36,6 +36,26 @@ class LicenseModel(BaseModel):
expired_at: str = "" expired_at: str = ""
class BrandingModel(BaseModel):
enabled: bool = False
application_title: str = ""
login_page_logo: str = ""
workspace_logo: str = ""
favicon: str = ""
class WebAppAuthSSOModel(BaseModel):
protocol: str = ""
class WebAppAuthModel(BaseModel):
enabled: bool = False
allow_sso: bool = False
sso_config: WebAppAuthSSOModel = WebAppAuthSSOModel()
allow_email_code_login: bool = False
allow_email_password_login: bool = False
class FeatureModel(BaseModel): class FeatureModel(BaseModel):
billing: BillingModel = BillingModel() billing: BillingModel = BillingModel()
members: LimitationModel = LimitationModel(size=0, limit=1) members: LimitationModel = LimitationModel(size=0, limit=1)
@@ -47,6 +67,7 @@ class FeatureModel(BaseModel):
can_replace_logo: bool = False can_replace_logo: bool = False
model_load_balancing_enabled: bool = False model_load_balancing_enabled: bool = False
dataset_operator_enabled: bool = False dataset_operator_enabled: bool = False
webapp_copyright_enabled: bool = False
# pydantic configs # pydantic configs
model_config = ConfigDict(protected_namespaces=()) model_config = ConfigDict(protected_namespaces=())
@@ -55,9 +76,6 @@ class FeatureModel(BaseModel):
class SystemFeatureModel(BaseModel): class SystemFeatureModel(BaseModel):
sso_enforced_for_signin: bool = False sso_enforced_for_signin: bool = False
sso_enforced_for_signin_protocol: str = "" sso_enforced_for_signin_protocol: str = ""
sso_enforced_for_web: bool = False
sso_enforced_for_web_protocol: str = ""
enable_web_sso_switch_component: bool = False
enable_email_code_login: bool = False enable_email_code_login: bool = False
enable_email_password_login: bool = True enable_email_password_login: bool = True
enable_social_oauth_login: bool = False enable_social_oauth_login: bool = False
@@ -65,6 +83,8 @@ class SystemFeatureModel(BaseModel):
is_allow_create_workspace: bool = False is_allow_create_workspace: bool = False
is_email_setup: bool = False is_email_setup: bool = False
license: LicenseModel = LicenseModel() license: LicenseModel = LicenseModel()
branding: BrandingModel = BrandingModel()
webapp_auth: WebAppAuthModel = WebAppAuthModel()
class FeatureService: class FeatureService:
@@ -77,6 +97,9 @@ class FeatureService:
if dify_config.BILLING_ENABLED and tenant_id: if dify_config.BILLING_ENABLED and tenant_id:
cls._fulfill_params_from_billing_api(features, tenant_id) cls._fulfill_params_from_billing_api(features, tenant_id)
if dify_config.ENTERPRISE_ENABLED:
features.webapp_copyright_enabled = True
return features return features
@classmethod @classmethod
@@ -86,8 +109,8 @@ class FeatureService:
cls._fulfill_system_params_from_env(system_features) cls._fulfill_system_params_from_env(system_features)
if dify_config.ENTERPRISE_ENABLED: if dify_config.ENTERPRISE_ENABLED:
system_features.enable_web_sso_switch_component = True system_features.branding.enabled = True
system_features.webapp_auth.enabled = True
cls._fulfill_params_from_enterprise(system_features) cls._fulfill_params_from_enterprise(system_features)
return system_features return system_features
@@ -115,6 +138,9 @@ class FeatureService:
features.billing.subscription.plan = billing_info["subscription"]["plan"] features.billing.subscription.plan = billing_info["subscription"]["plan"]
features.billing.subscription.interval = billing_info["subscription"]["interval"] features.billing.subscription.interval = billing_info["subscription"]["interval"]
if features.billing.subscription.plan != "sandbox":
features.webapp_copyright_enabled = True
if "members" in billing_info: if "members" in billing_info:
features.members.size = billing_info["members"]["size"] features.members.size = billing_info["members"]["size"]
features.members.limit = billing_info["members"]["limit"] features.members.limit = billing_info["members"]["limit"]
@@ -145,38 +171,45 @@ class FeatureService:
features.model_load_balancing_enabled = billing_info["model_load_balancing_enabled"] features.model_load_balancing_enabled = billing_info["model_load_balancing_enabled"]
@classmethod @classmethod
def _fulfill_params_from_enterprise(cls, features): def _fulfill_params_from_enterprise(cls, features: SystemFeatureModel):
enterprise_info = EnterpriseService.get_info() enterprise_info = EnterpriseService.get_info()
if "sso_enforced_for_signin" in enterprise_info: if "SSOEnforcedForSignin" in enterprise_info:
features.sso_enforced_for_signin = enterprise_info["sso_enforced_for_signin"] features.sso_enforced_for_signin = enterprise_info["SSOEnforcedForSignin"]
if "sso_enforced_for_signin_protocol" in enterprise_info: if "EnableEmailCodeLogin" in enterprise_info:
features.sso_enforced_for_signin_protocol = enterprise_info["sso_enforced_for_signin_protocol"] features.enable_email_code_login = enterprise_info["EnableEmailCodeLogin"]
if "sso_enforced_for_web" in enterprise_info: if "EnableEmailPasswordLogin" in enterprise_info:
features.sso_enforced_for_web = enterprise_info["sso_enforced_for_web"] features.enable_email_password_login = enterprise_info["EnableEmailPasswordLogin"]
if "sso_enforced_for_web_protocol" in enterprise_info: if "IsAllowRegister" in enterprise_info:
features.sso_enforced_for_web_protocol = enterprise_info["sso_enforced_for_web_protocol"] features.is_allow_register = enterprise_info["IsAllowRegister"]
if "enable_email_code_login" in enterprise_info: if "IsAllowCreateWorkspace" in enterprise_info:
features.enable_email_code_login = enterprise_info["enable_email_code_login"] features.is_allow_create_workspace = enterprise_info["IsAllowCreateWorkspace"]
if "enable_email_password_login" in enterprise_info: if "Branding" in enterprise_info:
features.enable_email_password_login = enterprise_info["enable_email_password_login"] features.branding.application_title = enterprise_info["Branding"].get("applicationTitle", "")
features.branding.login_page_logo = enterprise_info["Branding"].get("loginPageLogo", "")
features.branding.workspace_logo = enterprise_info["Branding"].get("workspaceLogo", "")
features.branding.favicon = enterprise_info["Branding"].get("favicon", "")
if "is_allow_register" in enterprise_info: if "WebAppAuth" in enterprise_info:
features.is_allow_register = enterprise_info["is_allow_register"] features.webapp_auth.allow_sso = enterprise_info["WebAppAuth"].get("allowSso", False)
features.webapp_auth.allow_email_code_login = enterprise_info["WebAppAuth"].get(
"allowEmailCodeLogin", False
)
features.webapp_auth.allow_email_password_login = enterprise_info["WebAppAuth"].get(
"allowEmailPasswordLogin", False
)
features.webapp_auth.sso_config.protocol = enterprise_info.get("SSOEnforcedForWebProtocol", "")
if "is_allow_create_workspace" in enterprise_info: if "License" in enterprise_info:
features.is_allow_create_workspace = enterprise_info["is_allow_create_workspace"] license_info = enterprise_info["License"]
if "license" in enterprise_info:
license_info = enterprise_info["license"]
if "status" in license_info: if "status" in license_info:
features.license.status = LicenseStatus(license_info.get("status", LicenseStatus.INACTIVE)) features.license.status = LicenseStatus(license_info.get("status", LicenseStatus.INACTIVE))
if "expired_at" in license_info: if "expiredAt" in license_info:
features.license.expired_at = license_info["expired_at"] features.license.expired_at = license_info["expiredAt"]

View File

@@ -0,0 +1,137 @@
import random
from datetime import UTC, datetime, timedelta
from typing import Any, Optional, cast
from werkzeug.exceptions import NotFound, Unauthorized
from configs import dify_config
from controllers.web.error import WebAppAuthAccessDeniedError
from extensions.ext_database import db
from libs.helper import TokenManager
from libs.passport import PassportService
from libs.password import compare_password
from models.account import Account, AccountStatus
from models.model import App, EndUser, Site
from services.enterprise.enterprise_service import EnterpriseService
from services.errors.account import AccountLoginError, AccountNotFoundError, AccountPasswordError
from services.feature_service import FeatureService
from tasks.mail_email_code_login import send_email_code_login_mail_task
class WebAppAuthService:
"""Service for web app authentication."""
@staticmethod
def authenticate(email: str, password: str) -> Account:
"""authenticate account with email and password"""
account = Account.query.filter_by(email=email).first()
if not account:
raise AccountNotFoundError()
if account.status == AccountStatus.BANNED.value:
raise AccountLoginError("Account is banned.")
if account.password is None or not compare_password(password, account.password, account.password_salt):
raise AccountPasswordError("Invalid email or password.")
return cast(Account, account)
@classmethod
def login(cls, account: Account, app_code: str, end_user_id: str) -> str:
site = db.session.query(Site).filter(Site.code == app_code).first()
if not site:
raise NotFound("Site not found.")
access_token = cls._get_account_jwt_token(account=account, site=site, end_user_id=end_user_id)
return access_token
@classmethod
def get_user_through_email(cls, email: str):
account = db.session.query(Account).filter(Account.email == email).first()
if not account:
return None
if account.status == AccountStatus.BANNED.value:
raise Unauthorized("Account is banned.")
return account
@classmethod
def send_email_code_login_email(
cls, account: Optional[Account] = None, email: Optional[str] = None, language: Optional[str] = "en-US"
):
email = account.email if account else email
if email is None:
raise ValueError("Email must be provided.")
code = "".join([str(random.randint(0, 9)) for _ in range(6)])
token = TokenManager.generate_token(
account=account, email=email, token_type="webapp_email_code_login", additional_data={"code": code}
)
send_email_code_login_mail_task.delay(
language=language,
to=account.email if account else email,
code=code,
)
return token
@classmethod
def get_email_code_login_data(cls, token: str) -> Optional[dict[str, Any]]:
return TokenManager.get_token_data(token, "webapp_email_code_login")
@classmethod
def revoke_email_code_login_token(cls, token: str):
TokenManager.revoke_token(token, "webapp_email_code_login")
@classmethod
def create_end_user(cls, app_code, email) -> EndUser:
site = db.session.query(Site).filter(Site.code == app_code).first()
app_model = db.session.query(App).filter(App.id == site.app_id).first()
end_user = EndUser(
tenant_id=app_model.tenant_id,
app_id=app_model.id,
type="browser",
is_anonymous=False,
session_id=email,
name="enterpriseuser",
external_user_id="enterpriseuser",
)
db.session.add(end_user)
db.session.commit()
return end_user
@classmethod
def _validate_user_accessibility(cls, account: Account, app_code: str):
"""Check if the user is allowed to access the app."""
system_features = FeatureService.get_system_features()
if system_features.webapp_auth.enabled:
app_settings = EnterpriseService.WebAppAuth.get_app_access_mode_by_code(app_code=app_code)
if (
app_settings.access_mode != "public"
and not EnterpriseService.WebAppAuth.is_user_allowed_to_access_webapp(account.id, app_code=app_code)
):
raise WebAppAuthAccessDeniedError()
@classmethod
def _get_account_jwt_token(cls, account: Account, site: Site, end_user_id: str) -> str:
exp_dt = datetime.now(UTC) + timedelta(hours=dify_config.WebAppSessionTimeoutInHours * 24)
exp = int(exp_dt.timestamp())
payload = {
"iss": site.id,
"sub": "Web API Passport",
"app_id": site.app_id,
"app_code": site.code,
"user_id": account.id,
"end_user_id": end_user_id,
"token_source": "webapp",
"exp": exp,
}
token: str = PassportService().issue(payload)
return token

View File

@@ -6,6 +6,7 @@ from celery import shared_task # type: ignore
from flask import render_template from flask import render_template
from extensions.ext_mail import mail from extensions.ext_mail import mail
from services.feature_service import FeatureService
@shared_task(queue="mail") @shared_task(queue="mail")
@@ -25,10 +26,24 @@ def send_email_code_login_mail_task(language: str, to: str, code: str):
# send email code login mail using different languages # send email code login mail using different languages
try: try:
if language == "zh-Hans": if language == "zh-Hans":
html_content = render_template("email_code_login_mail_template_zh-CN.html", to=to, code=code) template = "email_code_login_mail_template_zh-CN.html"
system_features = FeatureService.get_system_features()
if system_features.branding.enabled:
application_title = system_features.branding.application_title
template = "without-brand/email_code_login_mail_template_zh-CN.html"
html_content = render_template(template, to=to, code=code, application_title=application_title)
else:
html_content = render_template(template, to=to, code=code)
mail.send(to=to, subject="邮箱验证码", html=html_content) mail.send(to=to, subject="邮箱验证码", html=html_content)
else: else:
html_content = render_template("email_code_login_mail_template_en-US.html", to=to, code=code) template = "email_code_login_mail_template_en-US.html"
system_features = FeatureService.get_system_features()
if system_features.branding.enabled:
application_title = system_features.branding.application_title
template = "without-brand/email_code_login_mail_template_en-US.html"
html_content = render_template(template, to=to, code=code, application_title=application_title)
else:
html_content = render_template(template, to=to, code=code)
mail.send(to=to, subject="Email Code", html=html_content) mail.send(to=to, subject="Email Code", html=html_content)
end_at = time.perf_counter() end_at = time.perf_counter()

View File

@@ -0,0 +1,33 @@
import logging
import time
import click
from celery import shared_task # type: ignore
from flask import render_template_string
from extensions.ext_mail import mail
@shared_task(queue="mail")
def send_enterprise_email_task(to, subject, body, substitutions):
if not mail.is_inited():
return
logging.info(click.style("Start enterprise mail to {} with subject {}".format(to, subject), fg="green"))
start_at = time.perf_counter()
try:
html_content = render_template_string(body, **substitutions)
if isinstance(to, list):
for t in to:
mail.send(to=t, subject=subject, html=html_content)
else:
mail.send(to=to, subject=subject, html=html_content)
end_at = time.perf_counter()
logging.info(
click.style("Send enterprise mail to {} succeeded: latency: {}".format(to, end_at - start_at), fg="green")
)
except Exception:
logging.exception("Send enterprise mail to {} failed".format(to))

View File

@@ -7,6 +7,7 @@ from flask import render_template
from configs import dify_config from configs import dify_config
from extensions.ext_mail import mail from extensions.ext_mail import mail
from services.feature_service import FeatureService
@shared_task(queue="mail") @shared_task(queue="mail")
@@ -33,23 +34,45 @@ def send_invite_member_mail_task(language: str, to: str, token: str, inviter_nam
try: try:
url = f"{dify_config.CONSOLE_WEB_URL}/activate?token={token}" url = f"{dify_config.CONSOLE_WEB_URL}/activate?token={token}"
if language == "zh-Hans": if language == "zh-Hans":
html_content = render_template( template = "invite_member_mail_template_zh-CN.html"
"invite_member_mail_template_zh-CN.html", system_features = FeatureService.get_system_features()
to=to, if system_features.branding.enabled:
inviter_name=inviter_name, application_title = system_features.branding.application_title
workspace_name=workspace_name, template = "without-brand/invite_member_mail_template_zh-CN.html"
url=url, html_content = render_template(
) template,
mail.send(to=to, subject="立即加入 Dify 工作空间", html=html_content) to=to,
inviter_name=inviter_name,
workspace_name=workspace_name,
url=url,
application_title=application_title,
)
mail.send(to=to, subject=f"立即加入 {application_title} 工作空间", html=html_content)
else:
html_content = render_template(
template, to=to, inviter_name=inviter_name, workspace_name=workspace_name, url=url
)
mail.send(to=to, subject="立即加入 Dify 工作空间", html=html_content)
else: else:
html_content = render_template( template = "invite_member_mail_template_en-US.html"
"invite_member_mail_template_en-US.html", system_features = FeatureService.get_system_features()
to=to, if system_features.branding.enabled:
inviter_name=inviter_name, application_title = system_features.branding.application_title
workspace_name=workspace_name, template = "without-brand/invite_member_mail_template_en-US.html"
url=url, html_content = render_template(
) template,
mail.send(to=to, subject="Join Dify Workspace Now", html=html_content) to=to,
inviter_name=inviter_name,
workspace_name=workspace_name,
url=url,
application_title=application_title,
)
mail.send(to=to, subject=f"Join {application_title} Workspace Now", html=html_content)
else:
html_content = render_template(
template, to=to, inviter_name=inviter_name, workspace_name=workspace_name, url=url
)
mail.send(to=to, subject="Join Dify Workspace Now", html=html_content)
end_at = time.perf_counter() end_at = time.perf_counter()
logging.info( logging.info(

View File

@@ -6,6 +6,7 @@ from celery import shared_task # type: ignore
from flask import render_template from flask import render_template
from extensions.ext_mail import mail from extensions.ext_mail import mail
from services.feature_service import FeatureService
@shared_task(queue="mail") @shared_task(queue="mail")
@@ -25,11 +26,27 @@ def send_reset_password_mail_task(language: str, to: str, code: str):
# send reset password mail using different languages # send reset password mail using different languages
try: try:
if language == "zh-Hans": if language == "zh-Hans":
html_content = render_template("reset_password_mail_template_zh-CN.html", to=to, code=code) template = "reset_password_mail_template_zh-CN.html"
mail.send(to=to, subject="设置您的 Dify 密码", html=html_content) system_features = FeatureService.get_system_features()
if system_features.branding.enabled:
application_title = system_features.branding.application_title
template = "without-brand/reset_password_mail_template_zh-CN.html"
html_content = render_template(template, to=to, code=code, application_title=application_title)
mail.send(to=to, subject=f"设置您的 {application_title} 密码", html=html_content)
else:
html_content = render_template(template, to=to, code=code)
mail.send(to=to, subject="设置您的 Dify 密码", html=html_content)
else: else:
html_content = render_template("reset_password_mail_template_en-US.html", to=to, code=code) template = "reset_password_mail_template_en-US.html"
mail.send(to=to, subject="Set Your Dify Password", html=html_content) system_features = FeatureService.get_system_features()
if system_features.branding.enabled:
application_title = system_features.branding.application_title
template = "without-brand/reset_password_mail_template_en-US.html"
html_content = render_template(template, to=to, code=code, application_title=application_title)
mail.send(to=to, subject=f"Set Your {application_title} Password", html=html_content)
else:
html_content = render_template(template, to=to, code=code)
mail.send(to=to, subject="Set Your Dify Password", html=html_content)
end_at = time.perf_counter() end_at = time.perf_counter()
logging.info( logging.info(

View File

@@ -0,0 +1,70 @@
<!DOCTYPE html>
<html>
<head>
<style>
body {
font-family: 'Arial', sans-serif;
line-height: 16pt;
color: #101828;
background-color: #e9ebf0;
margin: 0;
padding: 0;
}
.container {
width: 600px;
height: 360px;
margin: 40px auto;
padding: 36px 48px;
background-color: #fcfcfd;
border-radius: 16px;
border: 1px solid #ffffff;
box-shadow: 0 2px 4px -2px rgba(9, 9, 11, 0.08);
}
.header {
margin-bottom: 24px;
}
.header img {
max-width: 100px;
height: auto;
}
.title {
font-weight: 600;
font-size: 24px;
line-height: 28.8px;
}
.description {
font-size: 13px;
line-height: 16px;
color: #676f83;
margin-top: 12px;
}
.code-content {
padding: 16px 32px;
text-align: center;
border-radius: 16px;
background-color: #f2f4f7;
margin: 16px auto;
}
.code {
line-height: 36px;
font-weight: 700;
font-size: 30px;
}
.tips {
line-height: 16px;
color: #676f83;
font-size: 13px;
}
</style>
</head>
<body>
<div class="container">
<p class="title">Your login code for {{application_title}}</p>
<p class="description">Copy and paste this code, this code will only be valid for the next 5 minutes.</p>
<div class="code-content">
<span class="code">{{code}}</span>
</div>
<p class="tips">If you didn't request a login, don't worry. You can safely ignore this email.</p>
</div>
</body>
</html>

View File

@@ -0,0 +1,70 @@
<!DOCTYPE html>
<html>
<head>
<style>
body {
font-family: 'Arial', sans-serif;
line-height: 16pt;
color: #101828;
background-color: #e9ebf0;
margin: 0;
padding: 0;
}
.container {
width: 600px;
height: 360px;
margin: 40px auto;
padding: 36px 48px;
background-color: #fcfcfd;
border-radius: 16px;
border: 1px solid #ffffff;
box-shadow: 0 2px 4px -2px rgba(9, 9, 11, 0.08);
}
.header {
margin-bottom: 24px;
}
.header img {
max-width: 100px;
height: auto;
}
.title {
font-weight: 600;
font-size: 24px;
line-height: 28.8px;
}
.description {
font-size: 13px;
line-height: 16px;
color: #676f83;
margin-top: 12px;
}
.code-content {
padding: 16px 32px;
text-align: center;
border-radius: 16px;
background-color: #f2f4f7;
margin: 16px auto;
}
.code {
line-height: 36px;
font-weight: 700;
font-size: 30px;
}
.tips {
line-height: 16px;
color: #676f83;
font-size: 13px;
}
</style>
</head>
<body>
<div class="container">
<p class="title">{{application_title}} 的登录验证码</p>
<p class="description">复制并粘贴此验证码,注意验证码仅在接下来的 5 分钟内有效。</p>
<div class="code-content">
<span class="code">{{code}}</span>
</div>
<p class="tips">如果您没有请求登录,请不要担心。您可以安全地忽略此电子邮件。</p>
</div>
</body>
</html>

View File

@@ -0,0 +1,69 @@
<!DOCTYPE html>
<html>
<head>
<style>
body {
font-family: 'Arial', sans-serif;
line-height: 16pt;
color: #374151;
background-color: #E5E7EB;
margin: 0;
padding: 0;
}
.container {
width: 100%;
max-width: 560px;
margin: 40px auto;
padding: 20px;
background-color: #F3F4F6;
border-radius: 8px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
}
.header {
text-align: center;
margin-bottom: 20px;
}
.header img {
max-width: 100px;
height: auto;
}
.button {
display: inline-block;
padding: 12px 24px;
background-color: #2970FF;
color: white;
text-decoration: none;
border-radius: 4px;
text-align: center;
transition: background-color 0.3s ease;
}
.button:hover {
background-color: #265DD4;
}
.footer {
font-size: 0.9em;
color: #777777;
margin-top: 30px;
}
.content {
margin-top: 20px;
}
</style>
</head>
<body>
<div class="container">
<div class="content">
<p>Dear {{ to }},</p>
<p>{{ inviter_name }} is pleased to invite you to join our workspace on {{application_title}}, a platform specifically designed for LLM application development. On {{application_title}}, you can explore, create, and collaborate to build and operate AI applications.</p>
<p>Click the button below to log in to {{application_title}} and join the workspace.</p>
<p style="text-align: center;"><a style="color: #fff; text-decoration: none" class="button" href="{{ url }}">Login Here</a></p>
</div>
<div class="footer">
<p>Best regards,</p>
<p>{{application_title}} Team</p>
<p>Please do not reply directly to this email; it is automatically sent by the system.</p>
</div>
</div>
</body>
</html>

View File

@@ -0,0 +1,69 @@
<!DOCTYPE html>
<html>
<head>
<style>
body {
font-family: 'Arial', sans-serif;
line-height: 16pt;
color: #374151;
background-color: #E5E7EB;
margin: 0;
padding: 0;
}
.container {
width: 100%;
max-width: 560px;
margin: 40px auto;
padding: 20px;
background-color: #F3F4F6;
border-radius: 8px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
}
.header {
text-align: center;
margin-bottom: 20px;
}
.header img {
max-width: 100px;
height: auto;
}
.button {
display: inline-block;
padding: 12px 24px;
background-color: #2970FF;
color: white;
text-decoration: none;
border-radius: 4px;
text-align: center;
transition: background-color 0.3s ease;
}
.button:hover {
background-color: #265DD4;
}
.footer {
font-size: 0.9em;
color: #777777;
margin-top: 30px;
}
.content {
margin-top: 20px;
}
</style>
</head>
<body>
<div class="container">
<div class="content">
<p>尊敬的 {{ to }}</p>
<p>{{ inviter_name }} 现邀请您加入我们在 {{application_title}} 的工作区,这是一个专为 LLM 应用开发而设计的平台。在 {{application_title}} 上,您可以探索、创造和合作,构建和运营 AI 应用。</p>
<p>点击下方按钮即可登录 {{application_title}} 并且加入空间。</p>
<p style="text-align: center;"><a style="color: #fff; text-decoration: none" class="button" href="{{ url }}">在此登录</a></p>
</div>
<div class="footer">
<p>此致,</p>
<p>{{application_title}} 团队</p>
<p>请不要直接回复此电子邮件;由系统自动发送。</p>
</div>
</div>
</body>
</html>

View File

@@ -0,0 +1,70 @@
<!DOCTYPE html>
<html>
<head>
<style>
body {
font-family: 'Arial', sans-serif;
line-height: 16pt;
color: #101828;
background-color: #e9ebf0;
margin: 0;
padding: 0;
}
.container {
width: 600px;
height: 360px;
margin: 40px auto;
padding: 36px 48px;
background-color: #fcfcfd;
border-radius: 16px;
border: 1px solid #ffffff;
box-shadow: 0 2px 4px -2px rgba(9, 9, 11, 0.08);
}
.header {
margin-bottom: 24px;
}
.header img {
max-width: 100px;
height: auto;
}
.title {
font-weight: 600;
font-size: 24px;
line-height: 28.8px;
}
.description {
font-size: 13px;
line-height: 16px;
color: #676f83;
margin-top: 12px;
}
.code-content {
padding: 16px 32px;
text-align: center;
border-radius: 16px;
background-color: #f2f4f7;
margin: 16px auto;
}
.code {
line-height: 36px;
font-weight: 700;
font-size: 30px;
}
.tips {
line-height: 16px;
color: #676f83;
font-size: 13px;
}
</style>
</head>
<body>
<div class="container">
<p class="title">Set your {{application_title}} password</p>
<p class="description">Copy and paste this code, this code will only be valid for the next 5 minutes.</p>
<div class="code-content">
<span class="code">{{code}}</span>
</div>
<p class="tips">If you didn't request, don't worry. You can safely ignore this email.</p>
</div>
</body>
</html>

View File

@@ -0,0 +1,70 @@
<!DOCTYPE html>
<html>
<head>
<style>
body {
font-family: 'Arial', sans-serif;
line-height: 16pt;
color: #101828;
background-color: #e9ebf0;
margin: 0;
padding: 0;
}
.container {
width: 600px;
height: 360px;
margin: 40px auto;
padding: 36px 48px;
background-color: #fcfcfd;
border-radius: 16px;
border: 1px solid #ffffff;
box-shadow: 0 2px 4px -2px rgba(9, 9, 11, 0.08);
}
.header {
margin-bottom: 24px;
}
.header img {
max-width: 100px;
height: auto;
}
.title {
font-weight: 600;
font-size: 24px;
line-height: 28.8px;
}
.description {
font-size: 13px;
line-height: 16px;
color: #676f83;
margin-top: 12px;
}
.code-content {
padding: 16px 32px;
text-align: center;
border-radius: 16px;
background-color: #f2f4f7;
margin: 16px auto;
}
.code {
line-height: 36px;
font-weight: 700;
font-size: 30px;
}
.tips {
line-height: 16px;
color: #676f83;
font-size: 13px;
}
</style>
</head>
<body>
<div class="container">
<p class="title">设置您的 {{application_title}} 账户密码</p>
<p class="description">复制并粘贴此验证码,注意验证码仅在接下来的 5 分钟内有效。</p>
<div class="code-content">
<span class="code">{{code}}</span>
</div>
<p class="tips">如果您没有请求,请不要担心。您可以安全地忽略此电子邮件。</p>
</div>
</body>
</html>

View File

@@ -2,7 +2,7 @@ x-shared-env: &shared-api-worker-env
services: services:
# API service # API service
api: api:
image: langgenius/dify-api:0.15.3 image: langgenius/dify-api:0.15.4
restart: always restart: always
environment: environment:
# Use the shared environment variables. # Use the shared environment variables.
@@ -25,7 +25,7 @@ services:
# worker service # worker service
# The Celery worker for processing the queue. # The Celery worker for processing the queue.
worker: worker:
image: langgenius/dify-api:0.15.3 image: langgenius/dify-api:0.15.4
restart: always restart: always
environment: environment:
# Use the shared environment variables. # Use the shared environment variables.
@@ -47,7 +47,7 @@ services:
# Frontend web application. # Frontend web application.
web: web:
image: langgenius/dify-web:0.15.3 image: langgenius/dify-web:0.15.4
restart: always restart: always
environment: environment:
CONSOLE_API_URL: ${CONSOLE_API_URL:-} CONSOLE_API_URL: ${CONSOLE_API_URL:-}
@@ -98,7 +98,7 @@ services:
# The DifySandbox # The DifySandbox
sandbox: sandbox:
image: langgenius/dify-sandbox:0.2.10 image: langgenius/dify-sandbox:0.2.11
restart: always restart: always
environment: environment:
# The DifySandbox configurations # The DifySandbox configurations

View File

@@ -43,7 +43,7 @@ services:
# The DifySandbox # The DifySandbox
sandbox: sandbox:
image: langgenius/dify-sandbox:0.2.10 image: langgenius/dify-sandbox:0.2.11
restart: always restart: always
environment: environment:
# The DifySandbox configurations # The DifySandbox configurations

View File

@@ -393,7 +393,7 @@ x-shared-env: &shared-api-worker-env
services: services:
# API service # API service
api: api:
image: langgenius/dify-api:0.15.3 image: langgenius/dify-api:0.15.4
restart: always restart: always
environment: environment:
# Use the shared environment variables. # Use the shared environment variables.
@@ -416,7 +416,7 @@ services:
# worker service # worker service
# The Celery worker for processing the queue. # The Celery worker for processing the queue.
worker: worker:
image: langgenius/dify-api:0.15.3 image: langgenius/dify-api:0.15.4
restart: always restart: always
environment: environment:
# Use the shared environment variables. # Use the shared environment variables.
@@ -438,7 +438,7 @@ services:
# Frontend web application. # Frontend web application.
web: web:
image: langgenius/dify-web:0.15.3 image: langgenius/dify-web:0.15.4
restart: always restart: always
environment: environment:
CONSOLE_API_URL: ${CONSOLE_API_URL:-} CONSOLE_API_URL: ${CONSOLE_API_URL:-}
@@ -489,7 +489,7 @@ services:
# The DifySandbox # The DifySandbox
sandbox: sandbox:
image: langgenius/dify-sandbox:0.2.10 image: langgenius/dify-sandbox:0.2.11
restart: always restart: always
environment: environment:
# The DifySandbox configurations # The DifySandbox configurations

View File

@@ -15,17 +15,17 @@ import {
} from '@remixicon/react' } from '@remixicon/react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { useShallow } from 'zustand/react/shallow' import { useShallow } from 'zustand/react/shallow'
import { useContextSelector } from 'use-context-selector'
import s from './style.module.css' import s from './style.module.css'
import cn from '@/utils/classnames' import cn from '@/utils/classnames'
import { useStore } from '@/app/components/app/store' import { useStore } from '@/app/components/app/store'
import AppSideBar from '@/app/components/app-sidebar' import AppSideBar from '@/app/components/app-sidebar'
import type { NavIcon } from '@/app/components/app-sidebar/navLink' import type { NavIcon } from '@/app/components/app-sidebar/navLink'
import { fetchAppDetail, fetchAppSSO } from '@/service/apps' import { fetchAppDetail, fetchAppSSO } from '@/service/apps'
import AppContext, { useAppContext } from '@/context/app-context' import { useAppContext } from '@/context/app-context'
import Loading from '@/app/components/base/loading' import Loading from '@/app/components/base/loading'
import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints' import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
import type { App } from '@/types/app' import type { App } from '@/types/app'
import { useGlobalPublicStore } from '@/context/global-public-context'
export type IAppDetailLayoutProps = { export type IAppDetailLayoutProps = {
children: React.ReactNode children: React.ReactNode
@@ -56,7 +56,7 @@ const AppDetailLayout: FC<IAppDetailLayoutProps> = (props) => {
icon: NavIcon icon: NavIcon
selectedIcon: NavIcon selectedIcon: NavIcon
}>>([]) }>>([])
const systemFeatures = useContextSelector(AppContext, state => state.systemFeatures) const { systemFeatures } = useGlobalPublicStore()
const getNavigations = useCallback((appId: string, isCurrentWorkspaceEditor: boolean, mode: string) => { const getNavigations = useCallback((appId: string, isCurrentWorkspaceEditor: boolean, mode: string) => {
const navs = [ const navs = [
@@ -98,7 +98,11 @@ const AppDetailLayout: FC<IAppDetailLayoutProps> = (props) => {
useEffect(() => { useEffect(() => {
if (appDetail) { if (appDetail) {
document.title = `${(appDetail.name || 'App')} - Dify` if (systemFeatures.branding.enabled)
document.title = `${(appDetail.name || 'App')} - ${systemFeatures.branding.application_title}`
else
document.title = `${(appDetail.name || 'App')} - Dify`
const localeMode = localStorage.getItem('app-detail-collapse-or-expand') || 'expand' const localeMode = localStorage.getItem('app-detail-collapse-or-expand') || 'expand'
const mode = isMobile ? 'collapse' : 'expand' const mode = isMobile ? 'collapse' : 'expand'
setAppSiderbarExpand(isMobile ? mode : localeMode) setAppSiderbarExpand(isMobile ? mode : localeMode)
@@ -106,7 +110,7 @@ const AppDetailLayout: FC<IAppDetailLayoutProps> = (props) => {
// if ((appDetail.mode === 'advanced-chat' || appDetail.mode === 'workflow') && (pathname).endsWith('workflow')) // if ((appDetail.mode === 'advanced-chat' || appDetail.mode === 'workflow') && (pathname).endsWith('workflow'))
// setAppSiderbarExpand('collapse') // setAppSiderbarExpand('collapse')
} }
}, [appDetail, isMobile]) }, [appDetail, isMobile, pathname, setAppSiderbarExpand, systemFeatures])
useEffect(() => { useEffect(() => {
setAppDetail() setAppDetail()

View File

@@ -2,7 +2,7 @@
import type { FC } from 'react' import type { FC } from 'react'
import React from 'react' import React from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { useContext, useContextSelector } from 'use-context-selector' import { useContext } from 'use-context-selector'
import AppCard from '@/app/components/app/overview/appCard' import AppCard from '@/app/components/app/overview/appCard'
import Loading from '@/app/components/base/loading' import Loading from '@/app/components/base/loading'
import { ToastContext } from '@/app/components/base/toast' import { ToastContext } from '@/app/components/base/toast'
@@ -20,7 +20,7 @@ import { asyncRunSafe } from '@/utils'
import { NEED_REFRESH_APP_LIST_KEY } from '@/config' import { NEED_REFRESH_APP_LIST_KEY } from '@/config'
import type { IAppCardProps } from '@/app/components/app/overview/appCard' import type { IAppCardProps } from '@/app/components/app/overview/appCard'
import { useStore as useAppStore } from '@/app/components/app/store' import { useStore as useAppStore } from '@/app/components/app/store'
import AppContext from '@/context/app-context' import { useGlobalPublicStore } from '@/context/global-public-context'
export type ICardViewProps = { export type ICardViewProps = {
appId: string appId: string
@@ -31,7 +31,7 @@ const CardView: FC<ICardViewProps> = ({ appId }) => {
const { notify } = useContext(ToastContext) const { notify } = useContext(ToastContext)
const appDetail = useAppStore(state => state.appDetail) const appDetail = useAppStore(state => state.appDetail)
const setAppDetail = useAppStore(state => state.setAppDetail) const setAppDetail = useAppStore(state => state.setAppDetail)
const systemFeatures = useContextSelector(AppContext, state => state.systemFeatures) const { systemFeatures } = useGlobalPublicStore()
const updateAppDetail = async () => { const updateAppDetail = async () => {
try { try {

View File

@@ -2,7 +2,9 @@
import type { FC } from 'react' import type { FC } from 'react'
import React, { useEffect } from 'react' import React, { useEffect } from 'react'
import { useRouter } from 'next/navigation' import { useRouter } from 'next/navigation'
import { useTranslation } from 'react-i18next'
import { useAppContext } from '@/context/app-context' import { useAppContext } from '@/context/app-context'
import useDocumentTitle from '@/hooks/use-document-title'
export type IAppDetail = { export type IAppDetail = {
children: React.ReactNode children: React.ReactNode
@@ -11,11 +13,13 @@ export type IAppDetail = {
const AppDetail: FC<IAppDetail> = ({ children }) => { const AppDetail: FC<IAppDetail> = ({ children }) => {
const router = useRouter() const router = useRouter()
const { isCurrentWorkspaceDatasetOperator } = useAppContext() const { isCurrentWorkspaceDatasetOperator } = useAppContext()
const { t } = useTranslation()
useDocumentTitle(t('common.menus.appDetail'))
useEffect(() => { useEffect(() => {
if (isCurrentWorkspaceDatasetOperator) if (isCurrentWorkspaceDatasetOperator)
return router.replace('/datasets') return router.replace('/datasets')
}, [isCurrentWorkspaceDatasetOperator]) }, [isCurrentWorkspaceDatasetOperator, router])
return ( return (
<> <>

View File

@@ -85,7 +85,6 @@ const Apps = () => {
] ]
useEffect(() => { useEffect(() => {
document.title = `${t('common.menus.apps')} - Dify`
if (localStorage.getItem(NEED_REFRESH_APP_LIST_KEY) === '1') { if (localStorage.getItem(NEED_REFRESH_APP_LIST_KEY) === '1') {
localStorage.removeItem(NEED_REFRESH_APP_LIST_KEY) localStorage.removeItem(NEED_REFRESH_APP_LIST_KEY)
mutate() mutate()

View File

@@ -1,21 +1,20 @@
'use client' 'use client'
import { useContextSelector } from 'use-context-selector'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { RiDiscordFill, RiGithubFill } from '@remixicon/react' import { RiDiscordFill, RiGithubFill } from '@remixicon/react'
import Link from 'next/link' import Link from 'next/link'
import style from '../list.module.css' import style from '../list.module.css'
import Apps from './Apps' import Apps from './Apps'
import AppContext from '@/context/app-context' import { useGlobalPublicStore } from '@/context/global-public-context'
import { LicenseStatus } from '@/types/feature' import useDocumentTitle from '@/hooks/use-document-title'
const AppList = () => { const AppList = () => {
const { t } = useTranslation() const { t } = useTranslation()
const systemFeatures = useContextSelector(AppContext, v => v.systemFeatures) const { systemFeatures } = useGlobalPublicStore()
useDocumentTitle(t('common.menus.apps'))
return ( return (
<div className='relative flex flex-col overflow-y-auto bg-background-body shrink-0 h-0 grow'> <div className='relative flex flex-col overflow-y-auto bg-background-body shrink-0 h-0 grow'>
<Apps /> <Apps />
{systemFeatures.license.status === LicenseStatus.NONE && <footer className='px-12 py-6 grow-0 shrink-0'> {!systemFeatures.branding.enabled && <footer className='px-12 py-6 grow-0 shrink-0'>
<h3 className='text-xl font-semibold leading-tight text-gradient'>{t('app.join')}</h3> <h3 className='text-xl font-semibold leading-tight text-gradient'>{t('app.join')}</h3>
<p className='mt-1 system-sm-regular text-text-tertiary'>{t('app.communityIntro')}</p> <p className='mt-1 system-sm-regular text-text-tertiary'>{t('app.communityIntro')}</p>
<div className='flex items-center gap-2 mt-3'> <div className='flex items-center gap-2 mt-3'>

View File

@@ -31,6 +31,7 @@ import { getLocaleOnClient } from '@/i18n'
import { useAppContext } from '@/context/app-context' import { useAppContext } from '@/context/app-context'
import Tooltip from '@/app/components/base/tooltip' import Tooltip from '@/app/components/base/tooltip'
import LinkedAppsPanel from '@/app/components/base/linked-apps-panel' import LinkedAppsPanel from '@/app/components/base/linked-apps-panel'
import useDocumentTitle from '@/hooks/use-document-title'
export type IAppDetailLayoutProps = { export type IAppDetailLayoutProps = {
children: React.ReactNode children: React.ReactNode
@@ -186,11 +187,7 @@ const DatasetDetailLayout: FC<IAppDetailLayoutProps> = (props) => {
} }
return baseNavigation return baseNavigation
}, [datasetRes?.provider, datasetId, t]) }, [datasetRes?.provider, datasetId, t])
useDocumentTitle(`${datasetRes?.name || 'Dataset'}`)
useEffect(() => {
if (datasetRes)
document.title = `${datasetRes.name || 'Dataset'} - Dify`
}, [datasetRes])
const setAppSiderbarExpand = useStore(state => state.setAppSiderbarExpand) const setAppSiderbarExpand = useStore(state => state.setAppSiderbarExpand)

View File

@@ -29,9 +29,11 @@ import { useTabSearchParams } from '@/hooks/use-tab-searchparams'
import { useStore as useTagStore } from '@/app/components/base/tag-management/store' import { useStore as useTagStore } from '@/app/components/base/tag-management/store'
import { useAppContext } from '@/context/app-context' import { useAppContext } from '@/context/app-context'
import { useExternalApiPanel } from '@/context/external-api-panel-context' import { useExternalApiPanel } from '@/context/external-api-panel-context'
import { useGlobalPublicStore } from '@/context/global-public-context'
const Container = () => { const Container = () => {
const { t } = useTranslation() const { t } = useTranslation()
const { systemFeatures } = useGlobalPublicStore()
const router = useRouter() const router = useRouter()
const { currentWorkspace, isCurrentWorkspaceOwner } = useAppContext() const { currentWorkspace, isCurrentWorkspaceOwner } = useAppContext()
const showTagManagementModal = useTagStore(s => s.showTagManagementModal) const showTagManagementModal = useTagStore(s => s.showTagManagementModal)
@@ -123,7 +125,7 @@ const Container = () => {
{activeTab === 'dataset' && ( {activeTab === 'dataset' && (
<> <>
<Datasets containerRef={containerRef} tags={tagIDs} keywords={searchKeywords} includeAll={includeAll} /> <Datasets containerRef={containerRef} tags={tagIDs} keywords={searchKeywords} includeAll={includeAll} />
<DatasetFooter /> {!systemFeatures.branding.enabled && <DatasetFooter />}
{showTagManagementModal && ( {showTagManagementModal && (
<TagManagementModal type='knowledge' show={showTagManagementModal} /> <TagManagementModal type='knowledge' show={showTagManagementModal} />
)} )}

View File

@@ -3,7 +3,6 @@
import { useEffect, useRef } from 'react' import { useEffect, useRef } from 'react'
import useSWRInfinite from 'swr/infinite' import useSWRInfinite from 'swr/infinite'
import { debounce } from 'lodash-es' import { debounce } from 'lodash-es'
import { useTranslation } from 'react-i18next'
import NewDatasetCard from './NewDatasetCard' import NewDatasetCard from './NewDatasetCard'
import DatasetCard from './DatasetCard' import DatasetCard from './DatasetCard'
import type { DataSetListResponse, FetchDatasetsParams } from '@/models/datasets' import type { DataSetListResponse, FetchDatasetsParams } from '@/models/datasets'
@@ -57,11 +56,8 @@ const Datasets = ({
const loadingStateRef = useRef(false) const loadingStateRef = useRef(false)
const anchorRef = useRef<HTMLAnchorElement>(null) const anchorRef = useRef<HTMLAnchorElement>(null)
const { t } = useTranslation()
useEffect(() => { useEffect(() => {
loadingStateRef.current = isLoading loadingStateRef.current = isLoading
document.title = `${t('dataset.knowledge')} - Dify`
}, [isLoading]) }, [isLoading])
useEffect(() => { useEffect(() => {
@@ -80,7 +76,7 @@ const Datasets = ({
return ( return (
<nav className='grid content-start grid-cols-1 gap-4 px-12 pt-2 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 grow shrink-0'> <nav className='grid content-start grid-cols-1 gap-4 px-12 pt-2 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 grow shrink-0'>
{ isCurrentWorkspaceEditor && <NewDatasetCard ref={anchorRef} /> } {isCurrentWorkspaceEditor && <NewDatasetCard ref={anchorRef} />}
{data?.map(({ data: datasets }) => datasets.map(dataset => ( {data?.map(({ data: datasets }) => datasets.map(dataset => (
<DatasetCard key={dataset.id} dataset={dataset} onSuccess={mutate} />), <DatasetCard key={dataset.id} dataset={dataset} onSuccess={mutate} />),
))} ))}

View File

@@ -1,11 +1,12 @@
'use client'
import { useTranslation } from 'react-i18next'
import Container from './Container' import Container from './Container'
import useDocumentTitle from '@/hooks/use-document-title'
const AppList = async () => { const AppList = () => {
const { t } = useTranslation()
useDocumentTitle(t('common.menus.datasets'))
return <Container /> return <Container />
} }
export const metadata = {
title: 'Datasets - Dify',
}
export default AppList export default AppList

View File

@@ -6,7 +6,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty, PropertyInstructi
<div> <div>
### Authentication ### Authentication
Service API of Dify authenticates using an `API-Key`. Service API authenticates using an `API-Key`.
It is suggested that developers store the `API-Key` in the backend instead of sharing or storing it in the client side to avoid the leakage of the `API-Key`, which may lead to property loss. It is suggested that developers store the `API-Key` in the backend instead of sharing or storing it in the client side to avoid the leakage of the `API-Key`, which may lead to property loss.

View File

@@ -6,7 +6,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty, PropertyInstructi
<div> <div>
### 鉴权 ### 鉴权
Dify Service API 使用 `API-Key` 进行鉴权。 Service API 使用 `API-Key` 进行鉴权。
建议开发者把 `API-Key` 放在后端存储,而非分享或者放在客户端存储,以免 `API-Key` 泄露,导致财产损失。 建议开发者把 `API-Key` 放在后端存储,而非分享或者放在客户端存储,以免 `API-Key` 泄露,导致财产损失。

View File

@@ -1,11 +1,13 @@
import type { FC } from 'react' 'use client'
import type { FC, PropsWithChildren } from 'react'
import React from 'react' import React from 'react'
import { useTranslation } from 'react-i18next'
import ExploreClient from '@/app/components/explore' import ExploreClient from '@/app/components/explore'
export type IAppDetail = { import useDocumentTitle from '@/hooks/use-document-title'
children: React.ReactNode
}
const AppDetail: FC<IAppDetail> = ({ children }) => { const ExploreLayout: FC<PropsWithChildren> = ({ children }) => {
const { t } = useTranslation()
useDocumentTitle(t('common.menus.explore'))
return ( return (
<ExploreClient> <ExploreClient>
{children} {children}
@@ -13,4 +15,4 @@ const AppDetail: FC<IAppDetail> = ({ children }) => {
) )
} }
export default React.memo(AppDetail) export default React.memo(ExploreLayout)

View File

@@ -30,9 +30,4 @@ const Layout = ({ children }: { children: ReactNode }) => {
</> </>
) )
} }
export const metadata = {
title: 'Dify',
}
export default Layout export default Layout

View File

@@ -1,22 +1,16 @@
'use client' 'use client'
import type { FC } from 'react' import type { FC } from 'react'
import { useRouter } from 'next/navigation' import { useRouter } from 'next/navigation'
import { useTranslation } from 'react-i18next'
import React, { useEffect } from 'react' import React, { useEffect } from 'react'
import { useTranslation } from 'react-i18next'
import ToolProviderList from '@/app/components/tools/provider-list' import ToolProviderList from '@/app/components/tools/provider-list'
import { useAppContext } from '@/context/app-context' import { useAppContext } from '@/context/app-context'
import useDocumentTitle from '@/hooks/use-document-title'
const Layout: FC = () => { const ToolsList: FC = () => {
const { t } = useTranslation()
const router = useRouter() const router = useRouter()
const { isCurrentWorkspaceDatasetOperator } = useAppContext() const { isCurrentWorkspaceDatasetOperator } = useAppContext()
const { t } = useTranslation()
useEffect(() => { useDocumentTitle(t('common.menus.tools'))
if (typeof window !== 'undefined')
document.title = `${t('tools.title')} - Dify`
if (isCurrentWorkspaceDatasetOperator)
return router.replace('/datasets')
}, [isCurrentWorkspaceDatasetOperator, router, t])
useEffect(() => { useEffect(() => {
if (isCurrentWorkspaceDatasetOperator) if (isCurrentWorkspaceDatasetOperator)
@@ -25,4 +19,4 @@ const Layout: FC = () => {
return <ToolProviderList /> return <ToolProviderList />
} }
export default React.memo(Layout) export default React.memo(ToolsList)

View File

@@ -16,6 +16,7 @@ import { ToastContext } from '@/app/components/base/toast'
import AppIcon from '@/app/components/base/app-icon' import AppIcon from '@/app/components/base/app-icon'
import { IS_CE_EDITION } from '@/config' import { IS_CE_EDITION } from '@/config'
import Input from '@/app/components/base/input' import Input from '@/app/components/base/input'
import { useGlobalPublicStore } from '@/context/global-public-context'
const titleClassName = ` const titleClassName = `
system-sm-semibold text-text-secondary system-sm-semibold text-text-secondary
@@ -28,7 +29,7 @@ const validPassword = /^(?=.*[a-zA-Z])(?=.*\d).{8,}$/
export default function AccountPage() { export default function AccountPage() {
const { t } = useTranslation() const { t } = useTranslation()
const { systemFeatures } = useAppContext() const { systemFeatures } = useGlobalPublicStore()
const { mutateUserProfile, userProfile, apps } = useAppContext() const { mutateUserProfile, userProfile, apps } = useAppContext()
const { notify } = useContext(ToastContext) const { notify } = useContext(ToastContext)
const [editNameModalVisible, setEditNameModalVisible] = useState(false) const [editNameModalVisible, setEditNameModalVisible] = useState(false)
@@ -133,7 +134,7 @@ export default function AccountPage() {
<h4 className='title-2xl-semi-bold text-text-primary'>{t('common.account.myAccount')}</h4> <h4 className='title-2xl-semi-bold text-text-primary'>{t('common.account.myAccount')}</h4>
</div> </div>
<div className='mb-8 p-6 rounded-xl flex items-center bg-gradient-to-r from-background-gradient-bg-fill-chat-bg-2 to-background-gradient-bg-fill-chat-bg-1'> <div className='mb-8 p-6 rounded-xl flex items-center bg-gradient-to-r from-background-gradient-bg-fill-chat-bg-2 to-background-gradient-bg-fill-chat-bg-1'>
<AvatarWithEdit avatar={userProfile.avatar_url} name={userProfile.name} onSave={ mutateUserProfile } size={64} /> <AvatarWithEdit avatar={userProfile.avatar_url} name={userProfile.name} onSave={mutateUserProfile} size={64} />
<div className='ml-4'> <div className='ml-4'>
<p className='system-xl-semibold text-text-primary'>{userProfile.name}</p> <p className='system-xl-semibold text-text-primary'>{userProfile.name}</p>
<p className='system-xs-regular text-text-tertiary'>{userProfile.email}</p> <p className='system-xs-regular text-text-tertiary'>{userProfile.email}</p>

View File

@@ -5,9 +5,11 @@ import { useRouter } from 'next/navigation'
import Button from '../components/base/button' import Button from '../components/base/button'
import Avatar from './avatar' import Avatar from './avatar'
import LogoSite from '@/app/components/base/logo/logo-site' import LogoSite from '@/app/components/base/logo/logo-site'
import { useGlobalPublicStore } from '@/context/global-public-context'
const Header = () => { const Header = () => {
const { t } = useTranslation() const { t } = useTranslation()
const { systemFeatures } = useGlobalPublicStore()
const router = useRouter() const router = useRouter()
const back = () => { const back = () => {
@@ -25,7 +27,7 @@ const Header = () => {
<div className='flex items-center flex-shrink-0 gap-3'> <div className='flex items-center flex-shrink-0 gap-3'>
<Button className='gap-2 py-2 px-3 system-sm-medium' onClick={back}> <Button className='gap-2 py-2 px-3 system-sm-medium' onClick={back}>
<RiRobot2Line className='w-4 h-4' /> <RiRobot2Line className='w-4 h-4' />
<p>{t('common.account.studio')}</p> <p>{!systemFeatures.branding.enabled && 'Dify '}{t('common.account.studio')}</p>
<RiArrowRightUpLine className='w-4 h-4' /> <RiArrowRightUpLine className='w-4 h-4' />
</Button> </Button>
<div className='w-[1px] h-4 bg-divider-regular' /> <div className='w-[1px] h-4 bg-divider-regular' />

View File

@@ -32,9 +32,4 @@ const Layout = ({ children }: { children: ReactNode }) => {
</> </>
) )
} }
export const metadata = {
title: 'Dify',
}
export default Layout export default Layout

View File

@@ -1,6 +1,11 @@
'use client'
import { useTranslation } from 'react-i18next'
import AccountPage from './account-page' import AccountPage from './account-page'
import useDocumentTitle from '@/hooks/use-document-title'
export default function Account() { export default function Account() {
const { t } = useTranslation()
useDocumentTitle(t('common.menus.account'))
return <div className='max-w-[640px] w-full mx-auto pt-12 px-6'> return <div className='max-w-[640px] w-full mx-auto pt-12 px-6'>
<AccountPage /> <AccountPage />
</div> </div>

View File

@@ -7,8 +7,10 @@ import Button from '@/app/components/base/button'
import { invitationCheck } from '@/service/common' import { invitationCheck } from '@/service/common'
import Loading from '@/app/components/base/loading' import Loading from '@/app/components/base/loading'
import useDocumentTitle from '@/hooks/use-document-title'
const ActivateForm = () => { const ActivateForm = () => {
useDocumentTitle('')
const router = useRouter() const router = useRouter()
const { t } = useTranslation() const { t } = useTranslation()
const searchParams = useSearchParams() const searchParams = useSearchParams()

View File

@@ -1,10 +1,13 @@
'use client'
import React from 'react' import React from 'react'
import Header from '../signin/_header' import Header from '../signin/_header'
import style from '../signin/page.module.css' import style from '../signin/page.module.css'
import ActivateForm from './activateForm' import ActivateForm from './activateForm'
import cn from '@/utils/classnames' import cn from '@/utils/classnames'
import { useGlobalPublicStore } from '@/context/global-public-context'
const Activate = () => { const Activate = () => {
const { systemFeatures } = useGlobalPublicStore()
return ( return (
<div className={cn( <div className={cn(
style.background, style.background,
@@ -21,9 +24,9 @@ const Activate = () => {
}> }>
<Header /> <Header />
<ActivateForm /> <ActivateForm />
<div className='px-8 py-6 text-sm font-normal text-gray-500'> {!systemFeatures.branding.enabled && <div className='px-8 py-6 text-sm font-normal text-gray-500'>
© {new Date().getFullYear()} LangGenius, Inc. All rights reserved. © {new Date().getFullYear()} LangGenius, Inc. All rights reserved.
</div> </div>}
</div> </div>
</div> </div>
) )

View File

@@ -312,7 +312,7 @@ function AppPreview({ mode }: { mode: AppMode }) {
'chat': { 'chat': {
title: t('app.types.chatbot'), title: t('app.types.chatbot'),
description: t('app.newApp.chatbotUserDescription'), description: t('app.newApp.chatbotUserDescription'),
link: 'https://docs.dify.ai/guides/application-orchestrate/conversation-application?fallback=true', link: 'https://docs.dify.ai/guides/application-orchestrate#application_type',
}, },
'advanced-chat': { 'advanced-chat': {
title: t('app.types.advanced'), title: t('app.types.advanced'),

View File

@@ -4,7 +4,7 @@ import React, { useCallback, useEffect, useState } from 'react'
import { RiArrowRightSLine, RiCloseLine } from '@remixicon/react' import { RiArrowRightSLine, RiCloseLine } from '@remixicon/react'
import Link from 'next/link' import Link from 'next/link'
import { Trans, useTranslation } from 'react-i18next' import { Trans, useTranslation } from 'react-i18next'
import { useContext, useContextSelector } from 'use-context-selector' import { useContext } from 'use-context-selector'
import { SparklesSoft } from '@/app/components/base/icons/src/public/common' import { SparklesSoft } from '@/app/components/base/icons/src/public/common'
import Modal from '@/app/components/base/modal' import Modal from '@/app/components/base/modal'
import ActionButton from '@/app/components/base/action-button' import ActionButton from '@/app/components/base/action-button'
@@ -21,13 +21,14 @@ import type { AppIconType, AppSSO, Language } from '@/types/app'
import { useToastContext } from '@/app/components/base/toast' import { useToastContext } from '@/app/components/base/toast'
import { LanguagesSupported, languages } from '@/i18n/language' import { LanguagesSupported, languages } from '@/i18n/language'
import Tooltip from '@/app/components/base/tooltip' import Tooltip from '@/app/components/base/tooltip'
import AppContext, { useAppContext } from '@/context/app-context' import { useAppContext } from '@/context/app-context'
import { useProviderContext } from '@/context/provider-context' import { useProviderContext } from '@/context/provider-context'
import { useModalContext } from '@/context/modal-context' import { useModalContext } from '@/context/modal-context'
import type { AppIconSelection } from '@/app/components/base/app-icon-picker' import type { AppIconSelection } from '@/app/components/base/app-icon-picker'
import AppIconPicker from '@/app/components/base/app-icon-picker' import AppIconPicker from '@/app/components/base/app-icon-picker'
import I18n from '@/context/i18n' import I18n from '@/context/i18n'
import cn from '@/utils/classnames' import cn from '@/utils/classnames'
import { useGlobalPublicStore } from '@/context/global-public-context'
export type ISettingsModalProps = { export type ISettingsModalProps = {
isChat: boolean isChat: boolean
@@ -65,7 +66,7 @@ const SettingsModal: FC<ISettingsModalProps> = ({
onClose, onClose,
onSave, onSave,
}) => { }) => {
const systemFeatures = useContextSelector(AppContext, state => state.systemFeatures) const { systemFeatures } = useGlobalPublicStore()
const { isCurrentWorkspaceEditor } = useAppContext() const { isCurrentWorkspaceEditor } = useAppContext()
const { notify } = useToastContext() const { notify } = useToastContext()
const [isShowMore, setIsShowMore] = useState(false) const [isShowMore, setIsShowMore] = useState(false)
@@ -110,7 +111,7 @@ const SettingsModal: FC<ISettingsModalProps> = ({
: { type: 'emoji', icon, background: icon_background! }, : { type: 'emoji', icon, background: icon_background! },
) )
const { enableBilling, plan } = useProviderContext() const { enableBilling, plan, webappCopyrightEnabled } = useProviderContext()
const { setShowPricingModal, setShowAccountSettingModal } = useModalContext() const { setShowPricingModal, setShowAccountSettingModal } = useModalContext()
const isFreePlan = plan.type === 'sandbox' const isFreePlan = plan.type === 'sandbox'
const handlePlanClick = useCallback(() => { const handlePlanClick = useCallback(() => {
@@ -177,7 +178,7 @@ const SettingsModal: FC<ISettingsModalProps> = ({
chat_color_theme: inputInfo.chatColorTheme, chat_color_theme: inputInfo.chatColorTheme,
chat_color_theme_inverted: inputInfo.chatColorThemeInverted, chat_color_theme_inverted: inputInfo.chatColorThemeInverted,
prompt_public: false, prompt_public: false,
copyright: isFreePlan copyright: !webappCopyrightEnabled
? '' ? ''
: inputInfo.copyrightSwitchValue : inputInfo.copyrightSwitchValue
? inputInfo.copyright ? inputInfo.copyright
@@ -354,7 +355,7 @@ const SettingsModal: FC<ISettingsModalProps> = ({
<div className={cn('py-1 text-text-secondary system-sm-semibold')}>{t(`${prefixSettings}.more.entry`)}</div> <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> <p className={cn('pb-0.5 text-text-tertiary body-xs-regular')}>{t(`${prefixSettings}.more.copyRightPlaceholder`)} & {t(`${prefixSettings}.more.privacyPolicyPlaceholder`)}</p>
</div> </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> </div>
)} )}
{/* more settings */} {/* more settings */}
@@ -380,14 +381,14 @@ const SettingsModal: FC<ISettingsModalProps> = ({
)} )}
</div> </div>
<Tooltip <Tooltip
disabled={!isFreePlan} disabled={webappCopyrightEnabled}
popupContent={ popupContent={
<div className='w-[260px]'>{t(`${prefixSettings}.more.copyrightTooltip`)}</div> <div className='w-[180px]'>{t(`${prefixSettings}.more.copyrightTooltip`)}</div>
} }
asChild={false} asChild={false}
> >
<Switch <Switch
disabled={isFreePlan} disabled={!webappCopyrightEnabled}
defaultValue={inputInfo.copyrightSwitchValue} defaultValue={inputInfo.copyrightSwitchValue}
onChange={v => setInputInfo({ ...inputInfo, copyrightSwitchValue: v })} onChange={v => setInputInfo({ ...inputInfo, copyrightSwitchValue: v })}
/> />
@@ -439,20 +440,22 @@ const SettingsModal: FC<ISettingsModalProps> = ({
<Button variant='primary' onClick={onClickSave} loading={saveLoading}>{t('common.operation.save')}</Button> <Button variant='primary' onClick={onClickSave} loading={saveLoading}>{t('common.operation.save')}</Button>
</div> </div>
</Modal > </Modal >
{showAppIconPicker && ( {
<AppIconPicker showAppIconPicker && (
onSelect={(payload) => { <AppIconPicker
setAppIcon(payload) onSelect={(payload) => {
setShowAppIconPicker(false) setAppIcon(payload)
}} setShowAppIconPicker(false)
onClose={() => { }}
setAppIcon(icon_type === 'image' onClose={() => {
? { type: 'image', url: icon_url!, fileId: icon } setAppIcon(icon_type === 'image'
: { type: 'emoji', icon, background: icon_background! }) ? { type: 'image', url: icon_url!, fileId: icon }
setShowAppIconPicker(false) : { type: 'emoji', icon, background: icon_background! })
}} setShowAppIconPicker(false)
/> }}
)} />
)
}
</> </>
) )

View File

@@ -11,10 +11,12 @@ import { useLocalStorageState } from 'ahooks'
import produce from 'immer' import produce from 'immer'
import type { import type {
ChatConfig, ChatConfig,
ChatItem,
Feedback, Feedback,
} from '../types' } from '../types'
import { CONVERSATION_ID_INFO } from '../constants' import { CONVERSATION_ID_INFO } from '../constants'
import { getPrevChatList, getProcessedInputsFromUrlParams } from '../utils' import { buildChatItemTree, getProcessedInputsFromUrlParams } from '../utils'
import { getProcessedFilesFromResponse } from '../../file-uploader/utils'
import { import {
fetchAppInfo, fetchAppInfo,
fetchAppMeta, fetchAppMeta,
@@ -32,6 +34,33 @@ import { useToastContext } from '@/app/components/base/toast'
import { changeLanguage } from '@/i18n/i18next-config' import { changeLanguage } from '@/i18n/i18next-config'
import { InputVarType } from '@/app/components/workflow/types' import { InputVarType } from '@/app/components/workflow/types'
import { TransferMethod } from '@/types/app' import { TransferMethod } from '@/types/app'
import { addFileInfos, sortAgentSorts } from '@/app/components/tools/utils'
function getFormattedChatList(messages: any[]) {
const newChatList: ChatItem[] = []
messages.forEach((item) => {
const questionFiles = item.message_files?.filter((file: any) => file.belongs_to === 'user') || []
newChatList.push({
id: `question-${item.id}`,
content: item.query,
isAnswer: false,
message_files: getProcessedFilesFromResponse(questionFiles.map((item: any) => ({ ...item, related_id: item.id }))),
parentMessageId: item.parent_message_id || undefined,
})
const answerFiles = item.message_files?.filter((file: any) => file.belongs_to === 'assistant') || []
newChatList.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 }))),
parentMessageId: `question-${item.id}`,
})
})
return newChatList
}
export const useEmbeddedChatbot = () => { export const useEmbeddedChatbot = () => {
const isInstalledApp = false const isInstalledApp = false
@@ -77,7 +106,7 @@ export const useEmbeddedChatbot = () => {
const appPrevChatList = useMemo( const appPrevChatList = useMemo(
() => (currentConversationId && appChatListData?.data.length) () => (currentConversationId && appChatListData?.data.length)
? getPrevChatList(appChatListData.data) ? buildChatItemTree(getFormattedChatList(appChatListData.data))
: [], : [],
[appChatListData, currentConversationId], [appChatListData, currentConversationId],
) )

View File

@@ -100,7 +100,7 @@ function getThreadMessages(tree: ChatItemInTree[], targetMessageId?: string): Ch
let targetNode: ChatItemInTree | undefined let targetNode: ChatItemInTree | undefined
// find path to the target message // find path to the target message
const stack = tree.toReversed().map(rootNode => ({ const stack = tree.slice().reverse().map(rootNode => ({
node: rootNode, node: rootNode,
path: [rootNode], path: [rootNode],
})) }))

View File

@@ -2,6 +2,7 @@
import type { FC } from 'react' import type { FC } from 'react'
import classNames from '@/utils/classnames' import classNames from '@/utils/classnames'
import { useSelector } from '@/context/app-context' import { useSelector } from '@/context/app-context'
import { useGlobalPublicStore } from '@/context/global-public-context'
type LogoSiteProps = { type LogoSiteProps = {
className?: string className?: string
@@ -10,13 +11,17 @@ type LogoSiteProps = {
const LogoSite: FC<LogoSiteProps> = ({ const LogoSite: FC<LogoSiteProps> = ({
className, className,
}) => { }) => {
const { systemFeatures } = useGlobalPublicStore()
const { theme } = useSelector((s) => { const { theme } = useSelector((s) => {
return { return {
theme: s.theme, theme: s.theme,
} }
}) })
const src = theme === 'light' ? '/logo/logo-site.png' : `/logo/logo-site-${theme}.png` let src = theme === 'light' ? '/logo/logo-site.png' : `/logo/logo-site-${theme}.png`
if (systemFeatures.branding.enabled)
src = systemFeatures.branding.workspace_logo
return ( return (
<img <img
src={src} src={src}

View File

@@ -10,6 +10,7 @@ import SyntaxHighlighter from 'react-syntax-highlighter'
import { atelierHeathLight } from 'react-syntax-highlighter/dist/esm/styles/hljs' import { atelierHeathLight } from 'react-syntax-highlighter/dist/esm/styles/hljs'
import { Component, memo, useMemo, useRef, useState } from 'react' import { Component, memo, useMemo, useRef, useState } from 'react'
import type { CodeComponent } from 'react-markdown/lib/ast-to-react' import type { CodeComponent } from 'react-markdown/lib/ast-to-react'
import SVGRenderer from './svg-gallery'
import cn from '@/utils/classnames' import cn from '@/utils/classnames'
import CopyBtn from '@/app/components/base/copy-btn' import CopyBtn from '@/app/components/base/copy-btn'
import SVGBtn from '@/app/components/base/svg' import SVGBtn from '@/app/components/base/svg'
@@ -18,7 +19,7 @@ import ImageGallery from '@/app/components/base/image-gallery'
import { useChatContext } from '@/app/components/base/chat/chat/context' import { useChatContext } from '@/app/components/base/chat/chat/context'
import VideoGallery from '@/app/components/base/video-gallery' import VideoGallery from '@/app/components/base/video-gallery'
import AudioGallery from '@/app/components/base/audio-gallery' import AudioGallery from '@/app/components/base/audio-gallery'
import SVGRenderer from '@/app/components/base/svg-gallery' // import SVGRenderer from '@/app/components/base/svg-gallery'
import MarkdownButton from '@/app/components/base/markdown-blocks/button' import MarkdownButton from '@/app/components/base/markdown-blocks/button'
import MarkdownForm from '@/app/components/base/markdown-blocks/form' import MarkdownForm from '@/app/components/base/markdown-blocks/form'

View File

@@ -1,5 +1,6 @@
import { useEffect, useRef, useState } from 'react' import { useEffect, useRef, useState } from 'react'
import { SVG } from '@svgdotjs/svg.js' import { SVG } from '@svgdotjs/svg.js'
import DOMPurify from 'dompurify'
import ImagePreview from '@/app/components/base/image-uploader/image-preview' import ImagePreview from '@/app/components/base/image-uploader/image-preview'
export const SVGRenderer = ({ content }: { content: string }) => { export const SVGRenderer = ({ content }: { content: string }) => {
@@ -44,7 +45,7 @@ export const SVGRenderer = ({ content }: { content: string }) => {
svgRef.current.style.width = `${Math.min(originalWidth, 298)}px` svgRef.current.style.width = `${Math.min(originalWidth, 298)}px`
const rootElement = draw.svg(content) const rootElement = draw.svg(DOMPurify.sanitize(content))
rootElement.click(() => { rootElement.click(() => {
setImagePreview(svgToDataURL(svgElement as Element)) setImagePreview(svgToDataURL(svgElement as Element))

View File

@@ -67,6 +67,7 @@ export type CurrentPlanInfoBackend = {
can_replace_logo: boolean can_replace_logo: boolean
model_load_balancing_enabled: boolean model_load_balancing_enabled: boolean
dataset_operator_enabled: boolean dataset_operator_enabled: boolean
webapp_copyright_enabled: boolean
} }
export type SubscriptionItem = { export type SubscriptionItem = {

View File

@@ -15,7 +15,7 @@ import { Row, Col, Properties, Property, Heading, SubProperty } from '../md.tsx'
### 鉴权 ### 鉴权
Dify Service API 使用 `API-Key` 进行鉴权。 Service API 使用 `API-Key` 进行鉴权。
<i>**强烈建议开发者把 `API-Key` 放在后端存储,而非分享或者放在客户端存储,以免 `API-Key` 泄露,导致财产损失。**</i> <i>**强烈建议开发者把 `API-Key` 放在后端存储,而非分享或者放在客户端存储,以免 `API-Key` 泄露,导致财产损失。**</i>
所有 API 请求都应在 **`Authorization`** HTTP Header 中包含您的 `API-Key`,如下所示: 所有 API 请求都应在 **`Authorization`** HTTP Header 中包含您的 `API-Key`,如下所示:

View File

@@ -14,7 +14,7 @@ Workflow 应用无会话支持,适合用于翻译/文章写作/总结 AI 等
### Authentication ### Authentication
Dify Service API 使用 `API-Key` 进行鉴权。 Service API 使用 `API-Key` 进行鉴权。
<i>**强烈建议开发者把 `API-Key` 放在后端存储,而非分享或者放在客户端存储,以免 `API-Key` 泄露,导致财产损失。**</i> <i>**强烈建议开发者把 `API-Key` 放在后端存储,而非分享或者放在客户端存储,以免 `API-Key` 泄露,导致财产损失。**</i>
所有 API 请求都应在 **`Authorization`** HTTP Header 中包含您的 `API-Key`,如下所示: 所有 API 请求都应在 **`Authorization`** HTTP Header 中包含您的 `API-Key`,如下所示:

View File

@@ -2,7 +2,6 @@
import type { FC } from 'react' import type { FC } from 'react'
import React, { useEffect, useState } from 'react' import React, { useEffect, useState } from 'react'
import { useRouter } from 'next/navigation' import { useRouter } from 'next/navigation'
import { useTranslation } from 'react-i18next'
import ExploreContext from '@/context/explore-context' import ExploreContext from '@/context/explore-context'
import Sidebar from '@/app/components/explore/sidebar' import Sidebar from '@/app/components/explore/sidebar'
import { useAppContext } from '@/context/app-context' import { useAppContext } from '@/context/app-context'
@@ -16,7 +15,6 @@ export type IExploreProps = {
const Explore: FC<IExploreProps> = ({ const Explore: FC<IExploreProps> = ({
children, children,
}) => { }) => {
const { t } = useTranslation()
const router = useRouter() const router = useRouter()
const [controlUpdateInstalledApps, setControlUpdateInstalledApps] = useState(0) const [controlUpdateInstalledApps, setControlUpdateInstalledApps] = useState(0)
const { userProfile, isCurrentWorkspaceDatasetOperator } = useAppContext() const { userProfile, isCurrentWorkspaceDatasetOperator } = useAppContext()
@@ -24,7 +22,6 @@ const Explore: FC<IExploreProps> = ({
const [installedApps, setInstalledApps] = useState<InstalledApp[]>([]) const [installedApps, setInstalledApps] = useState<InstalledApp[]>([])
useEffect(() => { useEffect(() => {
document.title = `${t('explore.title')} - Dify`;
(async () => { (async () => {
const { accounts } = await fetchMembers({ url: '/workspaces/current/members', params: {} }) const { accounts } = await fetchMembers({ url: '/workspaces/current/members', params: {} })
if (!accounts) if (!accounts)

View File

@@ -20,6 +20,7 @@ import { useModalContext } from '@/context/modal-context'
import { LanguagesSupported } from '@/i18n/language' import { LanguagesSupported } from '@/i18n/language'
import { useProviderContext } from '@/context/provider-context' import { useProviderContext } from '@/context/provider-context'
import { Plan } from '@/app/components/billing/type' import { Plan } from '@/app/components/billing/type'
import { useGlobalPublicStore } from '@/context/global-public-context'
export type IAppSelector = { export type IAppSelector = {
isMobile: boolean isMobile: boolean
@@ -32,6 +33,7 @@ export default function AppSelector({ isMobile }: IAppSelector) {
` `
const router = useRouter() const router = useRouter()
const [aboutVisible, setAboutVisible] = useState(false) const [aboutVisible, setAboutVisible] = useState(false)
const { systemFeatures } = useGlobalPublicStore()
const { locale } = useContext(I18n) const { locale } = useContext(I18n)
const { t } = useTranslation() const { t } = useTranslation()
@@ -122,78 +124,80 @@ export default function AppSelector({ isMobile }: IAppSelector) {
<div>{t('common.userProfile.settings')}</div> <div>{t('common.userProfile.settings')}</div>
</div>} </div>}
</Menu.Item> </Menu.Item>
{canEmailSupport && <Menu.Item> {!systemFeatures.branding.enabled && <>
{({ active }) => <a {canEmailSupport && <Menu.Item>
className={classNames(itemClassName, 'group justify-between', {({ active }) => <a
active && 'bg-state-base-hover', className={classNames(itemClassName, 'group justify-between',
)}
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', active && 'bg-state-base-hover',
)} onClick={() => setAboutVisible(true)}> )}
<div>{t('common.userProfile.about')}</div> href={mailToSupport(userProfile.email, plan.type, langeniusVersionInfo.current_version)}
<div className='flex items-center'> target='_blank' rel='noopener noreferrer'>
<div className='mr-2 system-xs-regular text-text-tertiary'>{langeniusVersionInfo.current_version}</div> <div>{t('common.userProfile.emailSupport')}</div>
<Indicator color={langeniusVersionInfo.current_version === langeniusVersionInfo.latest_version ? 'green' : 'orange'} /> <ArrowUpRight className='hidden w-[14px] h-[14px] text-text-tertiary group-hover:flex' />
</div> </a>}
</div>} </Menu.Item>}
</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>
)
}
</>}
</div> </div>
<Menu.Item> <Menu.Item>
{({ active }) => <div className='p-1' onClick={() => handleLogout()}> {({ active }) => <div className='p-1' onClick={() => handleLogout()}>

View File

@@ -21,6 +21,7 @@ import { Plan } from '@/app/components/billing/type'
import UpgradeBtn from '@/app/components/billing/upgrade-btn' import UpgradeBtn from '@/app/components/billing/upgrade-btn'
import { NUM_INFINITE } from '@/app/components/billing/config' import { NUM_INFINITE } from '@/app/components/billing/config'
import { LanguagesSupported } from '@/i18n/language' import { LanguagesSupported } from '@/i18n/language'
import { useGlobalPublicStore } from '@/context/global-public-context'
dayjs.extend(relativeTime) dayjs.extend(relativeTime)
const MembersPage = () => { const MembersPage = () => {
@@ -34,7 +35,8 @@ const MembersPage = () => {
} }
const { locale } = useContext(I18n) const { locale } = useContext(I18n)
const { userProfile, currentWorkspace, isCurrentWorkspaceOwner, isCurrentWorkspaceManager, systemFeatures } = useAppContext() const { userProfile, currentWorkspace, isCurrentWorkspaceOwner, isCurrentWorkspaceManager } = useAppContext()
const { systemFeatures } = useGlobalPublicStore()
const { data, mutate } = useSWR({ url: '/workspaces/current/members' }, fetchMembers) const { data, mutate } = useSWR({ url: '/workspaces/current/members' }, fetchMembers)
const [inviteModalVisible, setInviteModalVisible] = useState(false) const [inviteModalVisible, setInviteModalVisible] = useState(false)
const [invitationResults, setInvitationResults] = useState<InvitationResult[]>([]) const [invitationResults, setInvitationResults] = useState<InvitationResult[]>([])

View File

@@ -4,7 +4,6 @@ import Link from 'next/link'
import { useBoolean } from 'ahooks' import { useBoolean } from 'ahooks'
import { useSelectedLayoutSegment } from 'next/navigation' import { useSelectedLayoutSegment } from 'next/navigation'
import { Bars3Icon } from '@heroicons/react/20/solid' import { Bars3Icon } from '@heroicons/react/20/solid'
import { useContextSelector } from 'use-context-selector'
import HeaderBillingBtn from '../billing/header-billing-btn' import HeaderBillingBtn from '../billing/header-billing-btn'
import AccountDropdown from './account-dropdown' import AccountDropdown from './account-dropdown'
import AppNav from './app-nav' import AppNav from './app-nav'
@@ -15,12 +14,13 @@ import ToolsNav from './tools-nav'
import GithubStar from './github-star' import GithubStar from './github-star'
import LicenseNav from './license-env' import LicenseNav from './license-env'
import { WorkspaceProvider } from '@/context/workspace-context' import { WorkspaceProvider } from '@/context/workspace-context'
import AppContext, { useAppContext } from '@/context/app-context' import { useAppContext } from '@/context/app-context'
import LogoSite from '@/app/components/base/logo/logo-site' import LogoSite from '@/app/components/base/logo/logo-site'
import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints' import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
import { useProviderContext } from '@/context/provider-context' import { useProviderContext } from '@/context/provider-context'
import { useModalContext } from '@/context/modal-context' import { useModalContext } from '@/context/modal-context'
import { LicenseStatus } from '@/types/feature' import { LicenseStatus } from '@/types/feature'
import { useGlobalPublicStore } from '@/context/global-public-context'
const navClassName = ` const navClassName = `
flex items-center relative mr-0 sm:mr-3 px-3 h-8 rounded-xl flex items-center relative mr-0 sm:mr-3 px-3 h-8 rounded-xl
@@ -30,7 +30,7 @@ const navClassName = `
const Header = () => { const Header = () => {
const { isCurrentWorkspaceEditor, isCurrentWorkspaceDatasetOperator } = useAppContext() const { isCurrentWorkspaceEditor, isCurrentWorkspaceDatasetOperator } = useAppContext()
const systemFeatures = useContextSelector(AppContext, v => v.systemFeatures) const { systemFeatures } = useGlobalPublicStore()
const selectedSegment = useSelectedLayoutSegment() const selectedSegment = useSelectedLayoutSegment()
const media = useBreakpoints() const media = useBreakpoints()
const isMobile = media === MediaType.mobile const isMobile = media === MediaType.mobile

View File

@@ -5,14 +5,15 @@ import { LicenseStatus } from '@/types/feature'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { useContextSelector } from 'use-context-selector' import { useContextSelector } from 'use-context-selector'
import dayjs from 'dayjs' import dayjs from 'dayjs'
import { useGlobalPublicStore } from '@/context/global-public-context'
const LicenseNav = () => { const LicenseNav = () => {
const { t } = useTranslation() const { t } = useTranslation()
const systemFeatures = useContextSelector(AppContext, s => s.systemFeatures) const { systemFeatures } = useGlobalPublicStore()
if (systemFeatures.license?.status === LicenseStatus.EXPIRING) { if (systemFeatures.license?.status === LicenseStatus.EXPIRING) {
const expiredAt = systemFeatures.license?.expired_at const expiredAt = systemFeatures.license?.expired_at
const count = dayjs(expiredAt).diff(dayjs(), 'days') const count = dayjs(expiredAt).diff(dayjs(), 'day')
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'> 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', { count })}</span>}
{count > 1 && <span>{t('common.license.expiring_plural', { count })}</span>} {count > 1 && <span>{t('common.license.expiring_plural', { count })}</span>}

View File

@@ -17,9 +17,11 @@ import ProviderCard from '@/app/components/tools/provider/card'
import ProviderDetail from '@/app/components/tools/provider/detail' import ProviderDetail from '@/app/components/tools/provider/detail'
import Empty from '@/app/components/tools/add-tool-modal/empty' import Empty from '@/app/components/tools/add-tool-modal/empty'
import { fetchCollectionList } from '@/service/tools' import { fetchCollectionList } from '@/service/tools'
import { useGlobalPublicStore } from '@/context/global-public-context'
const ProviderList = () => { const ProviderList = () => {
const { t } = useTranslation() const { t } = useTranslation()
const { systemFeatures } = useGlobalPublicStore()
const [activeTab, setActiveTab] = useTabSearchParams({ const [activeTab, setActiveTab] = useTabSearchParams({
defaultTab: 'builtin', defaultTab: 'builtin',
@@ -98,7 +100,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', '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', currentProvider && 'pr-6 sm:grid-cols-1 md:grid-cols-2 lg:grid-cols-3',
)}> )}>
{activeTab === 'builtin' && <ContributeCard />} {activeTab === 'builtin' && !systemFeatures.branding.enabled && <ContributeCard />}
{activeTab === 'api' && <CustomCreateCard onRefreshData={getProviderList} />} {activeTab === 'api' && <CustomCreateCard onRefreshData={getProviderList} />}
{filteredCollectionList.map(collection => ( {filteredCollectionList.map(collection => (
<ProviderCard <ProviderCard

View File

@@ -21,8 +21,8 @@ const Contribute: FC = () => {
> >
<div className='flex pt-[14px] px-[14px] pb-3 h-[66px] items-center gap-3 grow-0 shrink-0'> <div className='flex pt-[14px] px-[14px] pb-3 h-[66px] items-center gap-3 grow-0 shrink-0'>
<div className='relative shrink-0 flex items-center'> <div className='relative shrink-0 flex items-center'>
<div className='z-10 flex p-3 rounded-[10px] bg-white border-[0.5px] border-primary-100 shadow-md'><RiHammerFill className='w-4 h-4 text-primary-600'/></div> <div className='z-10 flex p-3 rounded-[10px] bg-white border-[0.5px] border-primary-100 shadow-md'><RiHammerFill className='w-4 h-4 text-primary-600' /></div>
<div className='-translate-x-2 flex p-3 rounded-[10px] bg-[#FEF6FB] border-[0.5px] border-[#FCE7F6] shadow-md'><Heart02 className='w-4 h-4 text-[#EE46BC]'/></div> <div className='-translate-x-2 flex p-3 rounded-[10px] bg-[#FEF6FB] border-[0.5px] border-[#FCE7F6] shadow-md'><Heart02 className='w-4 h-4 text-[#EE46BC]' /></div>
</div> </div>
</div> </div>
<div className='mb-3 px-[14px] text-[15px] leading-5 font-semibold'> <div className='mb-3 px-[14px] text-[15px] leading-5 font-semibold'>

View File

@@ -6,10 +6,14 @@ import Header from '../signin/_header'
import style from '../signin/page.module.css' import style from '../signin/page.module.css'
import ForgotPasswordForm from './ForgotPasswordForm' import ForgotPasswordForm from './ForgotPasswordForm'
import ChangePasswordForm from '@/app/forgot-password/ChangePasswordForm' import ChangePasswordForm from '@/app/forgot-password/ChangePasswordForm'
import useDocumentTitle from '@/hooks/use-document-title'
import { useGlobalPublicStore } from '@/context/global-public-context'
const ForgotPassword = () => { const ForgotPassword = () => {
useDocumentTitle('')
const searchParams = useSearchParams() const searchParams = useSearchParams()
const token = searchParams.get('token') const token = searchParams.get('token')
const { systemFeatures } = useGlobalPublicStore()
return ( return (
<div className={classNames( <div className={classNames(
@@ -27,9 +31,9 @@ const ForgotPassword = () => {
}> }>
<Header /> <Header />
{token ? <ChangePasswordForm /> : <ForgotPasswordForm />} {token ? <ChangePasswordForm /> : <ForgotPasswordForm />}
<div className='px-8 py-6 text-sm font-normal text-gray-500'> {!systemFeatures.branding.enabled && <div className='px-8 py-6 text-sm font-normal text-gray-500'>
© {new Date().getFullYear()} LangGenius, Inc. All rights reserved. © {new Date().getFullYear()} LangGenius, Inc. All rights reserved.
</div> </div>}
</div> </div>
</div> </div>
) )

View File

@@ -7,8 +7,10 @@ import Loading from '../components/base/loading'
import Button from '@/app/components/base/button' import Button from '@/app/components/base/button'
import { fetchInitValidateStatus, initValidate } from '@/service/common' import { fetchInitValidateStatus, initValidate } from '@/service/common'
import type { InitValidateStatusResponse } from '@/models/common' import type { InitValidateStatusResponse } from '@/models/common'
import useDocumentTitle from '@/hooks/use-document-title'
const InitPasswordPopup = () => { const InitPasswordPopup = () => {
useDocumentTitle('')
const [password, setPassword] = useState('') const [password, setPassword] = useState('')
const [loading, setLoading] = useState(true) const [loading, setLoading] = useState(true)
const [validated, setValidated] = useState(false) const [validated, setValidated] = useState(false)

View File

@@ -15,6 +15,7 @@ import Button from '@/app/components/base/button'
import { fetchInitValidateStatus, fetchSetupStatus, setup } from '@/service/common' import { fetchInitValidateStatus, fetchSetupStatus, setup } from '@/service/common'
import type { InitValidateStatusResponse, SetupStatusResponse } from '@/models/common' import type { InitValidateStatusResponse, SetupStatusResponse } from '@/models/common'
import useDocumentTitle from '@/hooks/use-document-title'
const validPassword = /^(?=.*[a-zA-Z])(?=.*\d).{8,}$/ const validPassword = /^(?=.*[a-zA-Z])(?=.*\d).{8,}$/
@@ -32,6 +33,7 @@ const accountFormSchema = z.object({
type AccountFormValues = z.infer<typeof accountFormSchema> type AccountFormValues = z.infer<typeof accountFormSchema>
const InstallForm = () => { const InstallForm = () => {
useDocumentTitle('')
const { t } = useTranslation() const { t } = useTranslation()
const router = useRouter() const router = useRouter()
const [showPassword, setShowPassword] = React.useState(false) const [showPassword, setShowPassword] = React.useState(false)

View File

@@ -1,10 +1,13 @@
'use client'
import React from 'react' import React from 'react'
import Header from '../signin/_header' import Header from '../signin/_header'
import style from '../signin/page.module.css' import style from '../signin/page.module.css'
import InstallForm from './installForm' import InstallForm from './installForm'
import classNames from '@/utils/classnames' import classNames from '@/utils/classnames'
import { useGlobalPublicStore } from '@/context/global-public-context'
const Install = () => { const Install = () => {
const { systemFeatures } = useGlobalPublicStore()
return ( return (
<div className={classNames( <div className={classNames(
style.background, style.background,
@@ -21,9 +24,9 @@ const Install = () => {
}> }>
<Header /> <Header />
<InstallForm /> <InstallForm />
<div className='px-8 py-6 text-sm font-normal text-gray-500'> {!systemFeatures.branding.enabled && <div className='px-8 py-6 text-sm font-normal text-gray-500'>
© {new Date().getFullYear()} LangGenius, Inc. All rights reserved. © {new Date().getFullYear()} LangGenius, Inc. All rights reserved.
</div> </div>}
</div> </div>
</div> </div>
) )

View File

@@ -1,4 +1,4 @@
import type { Viewport } from 'next' import type { Metadata, Viewport } from 'next'
import I18nServer from './components/i18n-server' import I18nServer from './components/i18n-server'
import BrowserInitor from './components/browser-initor' import BrowserInitor from './components/browser-initor'
import SentryInitor from './components/sentry-initor' import SentryInitor from './components/sentry-initor'
@@ -6,10 +6,7 @@ import { getLocaleOnServer } from '@/i18n/server'
import { TanstackQueryIniter } from '@/context/query-client' import { TanstackQueryIniter } from '@/context/query-client'
import './styles/globals.css' import './styles/globals.css'
import './styles/markdown.scss' import './styles/markdown.scss'
import GlobalPublicStoreProvider from '@/context/global-public-context'
export const metadata = {
title: 'Dify',
}
export const viewport: Viewport = { export const viewport: Viewport = {
width: 'device-width', width: 'device-width',
@@ -18,6 +15,10 @@ export const viewport: Viewport = {
viewportFit: 'cover', viewportFit: 'cover',
userScalable: false, userScalable: false,
} }
export const metadata: Metadata = {
title: ' ',
icons: 'data:',
}
const LocaleLayout = ({ const LocaleLayout = ({
children, children,
@@ -50,7 +51,11 @@ const LocaleLayout = ({
<BrowserInitor> <BrowserInitor>
<SentryInitor> <SentryInitor>
<TanstackQueryIniter> <TanstackQueryIniter>
<I18nServer>{children}</I18nServer> <I18nServer>
<GlobalPublicStoreProvider>
{children}
</GlobalPublicStoreProvider>
</I18nServer>
</TanstackQueryIniter> </TanstackQueryIniter>
</SentryInitor> </SentryInitor>
</BrowserInitor> </BrowserInitor>

View File

@@ -39,7 +39,11 @@ export default function CheckCode() {
} }
setIsLoading(true) setIsLoading(true)
const ret = await verifyResetPasswordCode({ email, code, token }) const ret = await verifyResetPasswordCode({ email, code, token })
ret.is_valid && router.push(`/reset-password/set-password?${searchParams.toString()}`) if (ret.is_valid) {
const params = new URLSearchParams(searchParams)
params.set('token', encodeURIComponent(ret.token))
router.push(`/reset-password/set-password?${params.toString()}`)
}
} }
catch (error) { console.error(error) } catch (error) { console.error(error) }
finally { finally {

View File

@@ -1,9 +1,12 @@
'use client'
import Header from '../signin/_header' import Header from '../signin/_header'
import style from '../signin/page.module.css' import style from '../signin/page.module.css'
import cn from '@/utils/classnames' import cn from '@/utils/classnames'
import { useGlobalPublicStore } from '@/context/global-public-context'
export default async function SignInLayout({ children }: any) { export default function SignInLayout({ children }: any) {
const { systemFeatures } = useGlobalPublicStore()
return <> return <>
<div className={cn( <div className={cn(
style.background, style.background,
@@ -30,9 +33,9 @@ export default async function SignInLayout({ children }: any) {
{children} {children}
</div> </div>
</div> </div>
<div className='px-8 py-6 system-xs-regular text-text-tertiary'> {!systemFeatures.branding.enabled && <div className='px-8 py-6 system-xs-regular text-text-tertiary'>
© {new Date().getFullYear()} LangGenius, Inc. All rights reserved. © {new Date().getFullYear()} LangGenius, Inc. All rights reserved.
</div> </div>}
</div> </div>
</div> </div>
</> </>

View File

@@ -12,9 +12,11 @@ import Input from '@/app/components/base/input'
import Toast from '@/app/components/base/toast' import Toast from '@/app/components/base/toast'
import { sendResetPasswordCode } from '@/service/common' import { sendResetPasswordCode } from '@/service/common'
import I18NContext from '@/context/i18n' import I18NContext from '@/context/i18n'
import useDocumentTitle from '@/hooks/use-document-title'
export default function CheckCode() { export default function CheckCode() {
const { t } = useTranslation() const { t } = useTranslation()
useDocumentTitle('')
const searchParams = useSearchParams() const searchParams = useSearchParams()
const router = useRouter() const router = useRouter()
const [email, setEmail] = useState('') const [email, setEmail] = useState('')

View File

@@ -0,0 +1,34 @@
'use client'
import type { FC } from 'react'
import classNames from '@/utils/classnames'
import { useSelector } from '@/context/app-context'
import { useGlobalPublicStore } from '@/context/global-public-context'
type LoginLogoProps = {
className?: string
}
const LoginLogo: FC<LoginLogoProps> = ({
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.login_page_logo
return (
<img
src={src}
className={classNames('block w-auto h-10', className)}
alt='logo'
/>
)
}
export default LoginLogo

View File

@@ -1,17 +1,17 @@
'use client' 'use client'
import React from 'react' import React from 'react'
import { useContext } from 'use-context-selector' import { useContext } from 'use-context-selector'
import LoginLogo from './LoginLogo'
import Select from '@/app/components/base/select/locale' import Select from '@/app/components/base/select/locale'
import { languages } from '@/i18n/language' import { languages } from '@/i18n/language'
import { type Locale } from '@/i18n' import { type Locale } from '@/i18n'
import I18n from '@/context/i18n' import I18n from '@/context/i18n'
import LogoSite from '@/app/components/base/logo/logo-site'
const Header = () => { const Header = () => {
const { locale, setLocaleOnClient } = useContext(I18n) const { locale, setLocaleOnClient } = useContext(I18n)
return <div className='flex items-center justify-between p-6 w-full'> return <div className='flex items-center justify-between p-6 w-full'>
<LogoSite /> <LoginLogo />
<Select <Select
value={locale} value={locale}
items={languages.filter(item => item.supported)} items={languages.filter(item => item.supported)}

View File

@@ -1,9 +1,14 @@
'use client'
import Header from './_header' import Header from './_header'
import style from './page.module.css' import style from './page.module.css'
import cn from '@/utils/classnames' import cn from '@/utils/classnames'
import { useGlobalPublicStore } from '@/context/global-public-context'
import useDocumentTitle from '@/hooks/use-document-title'
export default async function SignInLayout({ children }: any) { export default function SignInLayout({ children }: any) {
const { systemFeatures } = useGlobalPublicStore()
useDocumentTitle('')
return <> return <>
<div className={cn( <div className={cn(
style.background, style.background,
@@ -30,9 +35,9 @@ export default async function SignInLayout({ children }: any) {
{children} {children}
</div> </div>
</div> </div>
<div className='px-8 py-6 system-xs-regular text-text-tertiary'> {systemFeatures.branding.enabled === false && <div className='px-8 py-6 system-xs-regular text-text-tertiary'>
© {new Date().getFullYear()} LangGenius, Inc. All rights reserved. © {new Date().getFullYear()} LangGenius, Inc. All rights reserved.
</div> </div>}
</div> </div>
</div> </div>
</> </>

View File

@@ -9,10 +9,11 @@ import MailAndPasswordAuth from './components/mail-and-password-auth'
import SocialAuth from './components/social-auth' import SocialAuth from './components/social-auth'
import SSOAuth from './components/sso-auth' import SSOAuth from './components/sso-auth'
import cn from '@/utils/classnames' import cn from '@/utils/classnames'
import { getSystemFeatures, invitationCheck } from '@/service/common' import { invitationCheck } from '@/service/common'
import { LicenseStatus, defaultSystemFeatures } from '@/types/feature' import { LicenseStatus } from '@/types/feature'
import Toast from '@/app/components/base/toast' import Toast from '@/app/components/base/toast'
import { IS_CE_EDITION } from '@/config' import { IS_CE_EDITION } from '@/config'
import { useGlobalPublicStore } from '@/context/global-public-context'
const NormalForm = () => { const NormalForm = () => {
const { t } = useTranslation() const { t } = useTranslation()
@@ -23,7 +24,7 @@ const NormalForm = () => {
const message = decodeURIComponent(searchParams.get('message') || '') const message = decodeURIComponent(searchParams.get('message') || '')
const invite_token = decodeURIComponent(searchParams.get('invite_token') || '') const invite_token = decodeURIComponent(searchParams.get('invite_token') || '')
const [isLoading, setIsLoading] = useState(true) const [isLoading, setIsLoading] = useState(true)
const [systemFeatures, setSystemFeatures] = useState(defaultSystemFeatures) const { systemFeatures } = useGlobalPublicStore()
const [authType, updateAuthType] = useState<'code' | 'password'>('password') const [authType, updateAuthType] = useState<'code' | 'password'>('password')
const [showORLine, setShowORLine] = useState(false) const [showORLine, setShowORLine] = useState(false)
const [allMethodsAreDisabled, setAllMethodsAreDisabled] = useState(false) const [allMethodsAreDisabled, setAllMethodsAreDisabled] = useState(false)
@@ -46,12 +47,9 @@ const NormalForm = () => {
message, message,
}) })
} }
const features = await getSystemFeatures() setAllMethodsAreDisabled(!systemFeatures.enable_social_oauth_login && !systemFeatures.enable_email_code_login && !systemFeatures.enable_email_password_login && !systemFeatures.sso_enforced_for_signin)
const allFeatures = { ...defaultSystemFeatures, ...features } setShowORLine((systemFeatures.enable_social_oauth_login || systemFeatures.sso_enforced_for_signin) && (systemFeatures.enable_email_code_login || systemFeatures.enable_email_password_login))
setSystemFeatures(allFeatures) updateAuthType(systemFeatures.enable_email_password_login ? 'password' : 'code')
setAllMethodsAreDisabled(!allFeatures.enable_social_oauth_login && !allFeatures.enable_email_code_login && !allFeatures.enable_email_password_login && !allFeatures.sso_enforced_for_signin)
setShowORLine((allFeatures.enable_social_oauth_login || allFeatures.sso_enforced_for_signin) && (allFeatures.enable_email_code_login || allFeatures.enable_email_password_login))
updateAuthType(allFeatures.enable_email_password_login ? 'password' : 'code')
if (isInviteLink) { if (isInviteLink) {
const checkRes = await invitationCheck({ const checkRes = await invitationCheck({
url: '/activate/check', url: '/activate/check',
@@ -65,10 +63,9 @@ const NormalForm = () => {
catch (error) { catch (error) {
console.error(error) console.error(error)
setAllMethodsAreDisabled(true) setAllMethodsAreDisabled(true)
setSystemFeatures(defaultSystemFeatures)
} }
finally { setIsLoading(false) } finally { setIsLoading(false) }
}, [consoleToken, refreshToken, message, router, invite_token, isInviteLink]) }, [consoleToken, refreshToken, message, router, invite_token, isInviteLink, systemFeatures])
useEffect(() => { useEffect(() => {
init() init()
}, [init]) }, [init])
@@ -83,7 +80,7 @@ const NormalForm = () => {
<Loading type='area' /> <Loading type='area' />
</div> </div>
} }
if (systemFeatures.license?.status === LicenseStatus.LOST) { if (systemFeatures.license.status === LicenseStatus.LOST) {
return <div className='w-full mx-auto mt-8'> return <div className='w-full mx-auto mt-8'>
<div className='bg-white'> <div className='bg-white'>
<div className="p-4 rounded-lg bg-gradient-to-r from-workflow-workflow-progress-bg-1 to-workflow-workflow-progress-bg-2"> <div className="p-4 rounded-lg bg-gradient-to-r from-workflow-workflow-progress-bg-1 to-workflow-workflow-progress-bg-2">
@@ -97,7 +94,7 @@ const NormalForm = () => {
</div> </div>
</div> </div>
} }
if (systemFeatures.license?.status === LicenseStatus.EXPIRED) { if (systemFeatures.license.status === LicenseStatus.EXPIRED) {
return <div className='w-full mx-auto mt-8'> return <div className='w-full mx-auto mt-8'>
<div className='bg-white'> <div className='bg-white'>
<div className="p-4 rounded-lg bg-gradient-to-r from-workflow-workflow-progress-bg-1 to-workflow-workflow-progress-bg-2"> <div className="p-4 rounded-lg bg-gradient-to-r from-workflow-workflow-progress-bg-1 to-workflow-workflow-progress-bg-2">
@@ -111,7 +108,7 @@ const NormalForm = () => {
</div> </div>
</div> </div>
} }
if (systemFeatures.license?.status === LicenseStatus.INACTIVE) { if (systemFeatures.license.status === LicenseStatus.INACTIVE) {
return <div className='w-full mx-auto mt-8'> return <div className='w-full mx-auto mt-8'>
<div className='bg-white'> <div className='bg-white'>
<div className="p-4 rounded-lg bg-gradient-to-r from-workflow-workflow-progress-bg-1 to-workflow-workflow-progress-bg-2"> <div className="p-4 rounded-lg bg-gradient-to-r from-workflow-workflow-progress-bg-1 to-workflow-workflow-progress-bg-2">
@@ -132,11 +129,11 @@ const NormalForm = () => {
{isInviteLink {isInviteLink
? <div className="w-full mx-auto"> ? <div className="w-full mx-auto">
<h2 className="title-4xl-semi-bold text-text-primary">{t('login.join')}{workspaceName}</h2> <h2 className="title-4xl-semi-bold text-text-primary">{t('login.join')}{workspaceName}</h2>
<p className='mt-2 body-md-regular text-text-tertiary'>{t('login.joinTipStart')}{workspaceName}{t('login.joinTipEnd')}</p> {!systemFeatures.branding.enabled && <p className='mt-2 body-md-regular text-text-tertiary'>{t('login.joinTipStart')}{workspaceName}{t('login.joinTipEnd')}</p>}
</div> </div>
: <div className="w-full mx-auto"> : <div className="w-full mx-auto">
<h2 className="title-4xl-semi-bold text-text-primary">{t('login.pageTitle')}</h2> <h2 className="title-4xl-semi-bold text-text-primary">{t('login.pageTitle')}</h2>
<p className='mt-2 body-md-regular text-text-tertiary'>{t('login.welcome')}</p> {!systemFeatures.branding.enabled && <p className='mt-2 body-md-regular text-text-tertiary'>{t('login.welcome')}</p>}
</div>} </div>}
<div className="bg-white"> <div className="bg-white">
<div className="flex flex-col gap-3 mt-6"> <div className="flex flex-col gap-3 mt-6">
@@ -184,29 +181,31 @@ const NormalForm = () => {
</div> </div>
</div> </div>
</>} </>}
<div className="w-full block mt-2 system-xs-regular text-text-tertiary"> {!systemFeatures.branding.enabled && <>
{t('login.tosDesc')} <div className="w-full block mt-2 system-xs-regular text-text-tertiary">
&nbsp; {t('login.tosDesc')}
<Link &nbsp;
className='system-xs-medium text-text-secondary hover:underline' <Link
target='_blank' rel='noopener noreferrer' className='system-xs-medium text-text-secondary hover:underline'
href='https://dify.ai/terms' target='_blank' rel='noopener noreferrer'
>{t('login.tos')}</Link> href='https://dify.ai/terms'
&nbsp;&&nbsp; >{t('login.tos')}</Link>
<Link &nbsp;&&nbsp;
className='system-xs-medium text-text-secondary hover:underline' <Link
target='_blank' rel='noopener noreferrer' className='system-xs-medium text-text-secondary hover:underline'
href='https://dify.ai/privacy' target='_blank' rel='noopener noreferrer'
>{t('login.pp')}</Link> href='https://dify.ai/privacy'
</div> >{t('login.pp')}</Link>
{IS_CE_EDITION && <div className="w-hull block mt-2 system-xs-regular text-text-tertiary"> </div>
{t('login.goToInit')} {IS_CE_EDITION && <div className="w-hull block mt-2 system-xs-regular text-text-tertiary">
&nbsp; {t('login.goToInit')}
<Link &nbsp;
className='system-xs-medium text-text-secondary hover:underline' <Link
href='/install' className='system-xs-medium text-text-secondary hover:underline'
>{t('login.setAdminAccount')}</Link> href='/install'
</div>} >{t('login.setAdminAccount')}</Link>
</div>}
</>}
</div> </div>
</div> </div>

View File

@@ -6,19 +6,16 @@ import { createContext, useContext, useContextSelector } from 'use-context-selec
import type { FC, ReactNode } from 'react' import type { FC, ReactNode } from 'react'
import { fetchAppList } from '@/service/apps' import { fetchAppList } from '@/service/apps'
import Loading from '@/app/components/base/loading' import Loading from '@/app/components/base/loading'
import { fetchCurrentWorkspace, fetchLanggeniusVersion, fetchUserProfile, getSystemFeatures } from '@/service/common' import { fetchCurrentWorkspace, fetchLanggeniusVersion, fetchUserProfile } from '@/service/common'
import type { App } from '@/types/app' import type { App } from '@/types/app'
import { Theme } from '@/types/app' import { Theme } from '@/types/app'
import type { ICurrentWorkspace, LangGeniusVersionResponse, UserProfileResponse } from '@/models/common' import type { ICurrentWorkspace, LangGeniusVersionResponse, UserProfileResponse } from '@/models/common'
import MaintenanceNotice from '@/app/components/header/maintenance-notice' import MaintenanceNotice from '@/app/components/header/maintenance-notice'
import type { SystemFeatures } from '@/types/feature'
import { defaultSystemFeatures } from '@/types/feature'
export type AppContextValue = { export type AppContextValue = {
theme: Theme theme: Theme
setTheme: (theme: Theme) => void setTheme: (theme: Theme) => void
apps: App[] apps: App[]
systemFeatures: SystemFeatures
mutateApps: VoidFunction mutateApps: VoidFunction
userProfile: UserProfileResponse userProfile: UserProfileResponse
mutateUserProfile: VoidFunction mutateUserProfile: VoidFunction
@@ -57,7 +54,6 @@ const initialWorkspaceInfo: ICurrentWorkspace = {
const AppContext = createContext<AppContextValue>({ const AppContext = createContext<AppContextValue>({
theme: Theme.light, theme: Theme.light,
systemFeatures: defaultSystemFeatures,
setTheme: () => { }, setTheme: () => { },
apps: [], apps: [],
mutateApps: () => { }, mutateApps: () => { },
@@ -96,10 +92,6 @@ export const AppContextProvider: FC<AppContextProviderProps> = ({ children }) =>
const { data: userProfileResponse, mutate: mutateUserProfile } = useSWR({ url: '/account/profile', params: {} }, fetchUserProfile) const { data: userProfileResponse, mutate: mutateUserProfile } = useSWR({ url: '/account/profile', params: {} }, fetchUserProfile)
const { data: currentWorkspaceResponse, mutate: mutateCurrentWorkspace, isLoading: isLoadingCurrentWorkspace } = useSWR({ url: '/workspaces/current', params: {} }, fetchCurrentWorkspace) const { data: currentWorkspaceResponse, mutate: mutateCurrentWorkspace, isLoading: isLoadingCurrentWorkspace } = useSWR({ url: '/workspaces/current', params: {} }, fetchCurrentWorkspace)
const { data: systemFeatures } = useSWR({ url: '/console/system-features' }, getSystemFeatures, {
fallbackData: defaultSystemFeatures,
})
const [userProfile, setUserProfile] = useState<UserProfileResponse>() const [userProfile, setUserProfile] = useState<UserProfileResponse>()
const [langeniusVersionInfo, setLangeniusVersionInfo] = useState<LangGeniusVersionResponse>(initialLangeniusVersionInfo) const [langeniusVersionInfo, setLangeniusVersionInfo] = useState<LangGeniusVersionResponse>(initialLangeniusVersionInfo)
const [currentWorkspace, setCurrentWorkspace] = useState<ICurrentWorkspace>(initialWorkspaceInfo) const [currentWorkspace, setCurrentWorkspace] = useState<ICurrentWorkspace>(initialWorkspaceInfo)
@@ -146,7 +138,6 @@ export const AppContextProvider: FC<AppContextProviderProps> = ({ children }) =>
theme, theme,
setTheme: handleSetTheme, setTheme: handleSetTheme,
apps: appList.data, apps: appList.data,
systemFeatures: { ...defaultSystemFeatures, ...systemFeatures },
mutateApps, mutateApps,
userProfile, userProfile,
mutateUserProfile, mutateUserProfile,

View File

@@ -0,0 +1,37 @@
'use client'
import { create } from 'zustand'
import { useQuery } from '@tanstack/react-query'
import type { FC, PropsWithChildren } from 'react'
import { useEffect } from 'react'
import type { SystemFeatures } from '@/types/feature'
import { defaultSystemFeatures } from '@/types/feature'
import { getSystemFeatures } from '@/service/common'
import Loading from '@/app/components/base/loading'
type GlobalPublicStore = {
systemFeatures: SystemFeatures
setSystemFeatures: (systemFeatures: SystemFeatures) => void
}
export const useGlobalPublicStore = create<GlobalPublicStore>(set => ({
systemFeatures: defaultSystemFeatures,
setSystemFeatures: (systemFeatures: SystemFeatures) => set(() => ({ systemFeatures })),
}))
const GlobalPublicStoreProvider: FC<PropsWithChildren> = ({
children,
}) => {
const { isPending, data } = useQuery({
queryKey: ['systemFeatures'],
queryFn: getSystemFeatures,
})
const { setSystemFeatures } = useGlobalPublicStore()
useEffect(() => {
if (data)
setSystemFeatures({ ...defaultSystemFeatures, ...data })
}, [data, setSystemFeatures])
if (isPending)
return <div className='w-screen h-screen flex items-center justify-center'><Loading /></div>
return <>{children}</>
}
export default GlobalPublicStoreProvider

View File

@@ -35,6 +35,7 @@ type ProviderContextState = {
enableReplaceWebAppLogo: boolean enableReplaceWebAppLogo: boolean
modelLoadBalancingEnabled: boolean modelLoadBalancingEnabled: boolean
datasetOperatorEnabled: boolean datasetOperatorEnabled: boolean
webappCopyrightEnabled: boolean
} }
const ProviderContext = createContext<ProviderContextState>({ const ProviderContext = createContext<ProviderContextState>({
modelProviders: [], modelProviders: [],
@@ -64,6 +65,7 @@ const ProviderContext = createContext<ProviderContextState>({
enableReplaceWebAppLogo: false, enableReplaceWebAppLogo: false,
modelLoadBalancingEnabled: false, modelLoadBalancingEnabled: false,
datasetOperatorEnabled: false, datasetOperatorEnabled: false,
webappCopyrightEnabled: false,
}) })
export const useProviderContext = () => useContext(ProviderContext) export const useProviderContext = () => useContext(ProviderContext)
@@ -91,6 +93,7 @@ export const ProviderContextProvider = ({
const [enableReplaceWebAppLogo, setEnableReplaceWebAppLogo] = useState(false) const [enableReplaceWebAppLogo, setEnableReplaceWebAppLogo] = useState(false)
const [modelLoadBalancingEnabled, setModelLoadBalancingEnabled] = useState(false) const [modelLoadBalancingEnabled, setModelLoadBalancingEnabled] = useState(false)
const [datasetOperatorEnabled, setDatasetOperatorEnabled] = useState(false) const [datasetOperatorEnabled, setDatasetOperatorEnabled] = useState(false)
const [webappCopyrightEnabled, setWebappCopyrightEnabled] = useState(false)
const fetchPlan = async () => { const fetchPlan = async () => {
const data = await fetchCurrentPlanInfo() const data = await fetchCurrentPlanInfo()
@@ -105,6 +108,8 @@ export const ProviderContextProvider = ({
setModelLoadBalancingEnabled(true) setModelLoadBalancingEnabled(true)
if (data.dataset_operator_enabled) if (data.dataset_operator_enabled)
setDatasetOperatorEnabled(true) setDatasetOperatorEnabled(true)
if (data.webapp_copyright_enabled)
setWebappCopyrightEnabled(true)
} }
useEffect(() => { useEffect(() => {
fetchPlan() fetchPlan()
@@ -123,6 +128,7 @@ export const ProviderContextProvider = ({
enableReplaceWebAppLogo, enableReplaceWebAppLogo,
modelLoadBalancingEnabled, modelLoadBalancingEnabled,
datasetOperatorEnabled, datasetOperatorEnabled,
webappCopyrightEnabled,
}}> }}>
{children} {children}
</ProviderContext.Provider> </ProviderContext.Provider>

View File

@@ -0,0 +1,18 @@
'use client'
import { useLayoutEffect } from 'react'
import { useGlobalPublicStore } from '@/context/global-public-context'
export default function useDocumentTitle(title: string) {
const { systemFeatures } = useGlobalPublicStore()
useLayoutEffect(() => {
const prefix = title ? `${title} - ` : ''
if (systemFeatures.branding.enabled) {
document.title = `${prefix}${systemFeatures.branding.application_title}`
const faviconEle = document.querySelector('link[rel*=\'icon\']') as HTMLLinkElement
faviconEle.href = systemFeatures.branding.favicon
}
else {
document.title = `${prefix}Dify`
}
}, [systemFeatures, title])
}

View File

@@ -131,6 +131,8 @@ const translation = {
status: 'beta', status: 'beta',
explore: 'Explore', explore: 'Explore',
apps: 'Studio', apps: 'Studio',
appDetail: 'App Detail',
account: 'Account',
plugins: 'Plugins', plugins: 'Plugins',
pluginsTips: 'Integrate third-party plugins or create ChatGPT-compatible AI-Plugins.', pluginsTips: 'Integrate third-party plugins or create ChatGPT-compatible AI-Plugins.',
datasets: 'Knowledge', datasets: 'Knowledge',
@@ -167,7 +169,7 @@ const translation = {
account: { account: {
account: 'Account', account: 'Account',
myAccount: 'My Account', myAccount: 'My Account',
studio: 'Dify Studio', studio: 'Studio',
avatar: 'Avatar', avatar: 'Avatar',
name: 'Name', name: 'Name',
email: 'Email', email: 'Email',
@@ -179,8 +181,8 @@ const translation = {
newPassword: 'New password', newPassword: 'New password',
confirmPassword: 'Confirm password', confirmPassword: 'Confirm password',
notEqual: 'Two passwords are different.', notEqual: 'Two passwords are different.',
langGeniusAccount: 'Dify account', langGeniusAccount: 'Account\'s data',
langGeniusAccountTip: 'Your Dify account and associated user data.', langGeniusAccountTip: 'The user data of your account.',
editName: 'Edit Name', editName: 'Edit Name',
showAppLength: 'Show {{length}} apps', showAppLength: 'Show {{length}} apps',
delete: 'Delete Account', delete: 'Delete Account',

View File

@@ -16,7 +16,7 @@ const translation = {
}, },
}, },
apps: { apps: {
title: 'Explore Apps by Dify', title: 'Explore Apps',
description: 'Use these template apps instantly or customize your own apps based on the templates.', description: 'Use these template apps instantly or customize your own apps based on the templates.',
allCategories: 'Recommended', allCategories: 'Recommended',
}, },

View File

@@ -131,6 +131,8 @@ const translation = {
status: 'ベータ版', status: 'ベータ版',
explore: '探索', explore: '探索',
apps: 'スタジオ', apps: 'スタジオ',
appDetail: 'アプリの詳細',
account: 'アカウント',
plugins: 'プラグイン', plugins: 'プラグイン',
pluginsTips: 'サードパーティのプラグインを統合するか、ChatGPT互換のAIプラグインを作成します。', pluginsTips: 'サードパーティのプラグインを統合するか、ChatGPT互換のAIプラグインを作成します。',
datasets: 'ナレッジ', datasets: 'ナレッジ',
@@ -176,8 +178,8 @@ const translation = {
newPassword: '新しいパスワード', newPassword: '新しいパスワード',
confirmPassword: 'パスワードを確認', confirmPassword: 'パスワードを確認',
notEqual: '2つのパスワードが異なります。', notEqual: '2つのパスワードが異なります。',
langGeniusAccount: 'Difyアカウント', langGeniusAccount: 'アカウント関連データ',
langGeniusAccountTip: 'Difyアカウント関連するユーザーデータ。', langGeniusAccountTip: 'アカウント関連するユーザーデータ。',
editName: '名前を編集', editName: '名前を編集',
showAppLength: '{{length}}アプリを表示', showAppLength: '{{length}}アプリを表示',
delete: 'アカウントを削除', delete: 'アカウントを削除',
@@ -185,7 +187,7 @@ const translation = {
deleteConfirmTip: '確認のため、登録したメールから次の内容をに送信してください ', deleteConfirmTip: '確認のため、登録したメールから次の内容をに送信してください ',
account: 'アカウント', account: 'アカウント',
myAccount: 'マイアカウント', myAccount: 'マイアカウント',
studio: 'Difyスタジオ', studio: 'スタジオ',
deletePrivacyLinkTip: 'お客様のデータの取り扱い方法の詳細については、当社の', deletePrivacyLinkTip: 'お客様のデータの取り扱い方法の詳細については、当社の',
deletePrivacyLink: 'プライバシーポリシー。', deletePrivacyLink: 'プライバシーポリシー。',
deleteSuccessTip: 'アカウントの削除が完了するまでに時間が必要です。すべて完了しましたら、メールでお知らせします。', deleteSuccessTip: 'アカウントの削除が完了するまでに時間が必要です。すべて完了しましたら、メールでお知らせします。',

View File

@@ -16,7 +16,7 @@ const translation = {
}, },
}, },
apps: { apps: {
title: 'Difyによるアプリ探索', title: 'アプリ探索',
description: 'これらのテンプレートアプリを即座に使用するか、テンプレートに基づいて独自のアプリをカスタマイズしてください。', description: 'これらのテンプレートアプリを即座に使用するか、テンプレートに基づいて独自のアプリをカスタマイズしてください。',
allCategories: '推奨', allCategories: '推奨',
}, },

Some files were not shown because too many files have changed in this diff Show More