Compare commits

..
46 changed files with 186 additions and 602 deletions
-3
View File
@@ -62,9 +62,6 @@ class ChatMessageListApi(Resource):
@account_initialization_required
@marshal_with(message_infinite_scroll_pagination_fields)
def get(self, app_model):
if not isinstance(current_user, Account) or not current_user.has_edit_permission:
raise Forbidden()
parser = reqparse.RequestParser()
parser.add_argument("conversation_id", required=True, type=uuid_value, location="args")
parser.add_argument("first_id", type=uuid_value, location="args")
+6 -6
View File
@@ -513,7 +513,7 @@ class DraftWorkflowRunApi(Resource):
raise InvokeRateLimitHttpError(ex.description)
@console_ns.route("/apps/<uuid:app_id>/workflow-runs/tasks/<string:task_id>/stop")
@console_ns.route("/apps/<uuid:app_id>/workflows/tasks/<string:task_id>/stop")
class WorkflowTaskStopApi(Resource):
@api.doc("stop_workflow_task")
@api.doc(description="Stop running workflow task")
@@ -682,7 +682,7 @@ class PublishedWorkflowApi(Resource):
}
@console_ns.route("/apps/<uuid:app_id>/workflows/default-workflow-block-configs")
@console_ns.route("/apps/<uuid:app_id>/workflows/default-block-configs")
class DefaultBlockConfigsApi(Resource):
@api.doc("get_default_block_configs")
@api.doc(description="Get default block configurations for workflow")
@@ -708,7 +708,7 @@ class DefaultBlockConfigsApi(Resource):
return workflow_service.get_default_block_configs()
@console_ns.route("/apps/<uuid:app_id>/workflows/default-workflow-block-configs/<string:block_type>")
@console_ns.route("/apps/<uuid:app_id>/workflows/default-block-configs/<string:block_type>")
class DefaultBlockConfigApi(Resource):
@api.doc("get_default_block_config")
@api.doc(description="Get default block configuration by type")
@@ -791,7 +791,7 @@ class ConvertToWorkflowApi(Resource):
}
@console_ns.route("/apps/<uuid:app_id>/workflows/draft/config")
@console_ns.route("/apps/<uuid:app_id>/workflows/config")
class WorkflowConfigApi(Resource):
"""Resource for workflow configuration."""
@@ -809,7 +809,7 @@ class WorkflowConfigApi(Resource):
}
@console_ns.route("/apps/<uuid:app_id>/workflows")
@console_ns.route("/apps/<uuid:app_id>/workflows/published")
class PublishedAllWorkflowApi(Resource):
@api.doc("get_all_published_workflows")
@api.doc(description="Get all published workflows for an application")
@@ -865,7 +865,7 @@ class PublishedAllWorkflowApi(Resource):
}
@console_ns.route("/apps/<uuid:app_id>/workflows/<string:workflow_id>")
@console_ns.route("/apps/<uuid:app_id>/workflows/<uuid:workflow_id>")
class WorkflowByIdApi(Resource):
@api.doc("update_workflow_by_id")
@api.doc(description="Update workflow by ID")
@@ -499,7 +499,6 @@ class DocumentIndexingEstimateApi(DocumentResource):
return response, 200
@console_ns.route("/datasets/<uuid:dataset_id>/batch/<string:batch>/indexing-estimate")
class DocumentBatchIndexingEstimateApi(DocumentResource):
@setup_required
@login_required
@@ -592,7 +591,6 @@ class DocumentBatchIndexingEstimateApi(DocumentResource):
raise IndexingEstimateError(str(e))
@console_ns.route("/datasets/<uuid:dataset_id>/batch/<string:batch>/indexing-status")
class DocumentBatchIndexingStatusApi(DocumentResource):
@setup_required
@login_required
@@ -912,7 +910,6 @@ class DocumentMetadataApi(DocumentResource):
return {"result": "success", "message": "Document metadata updated."}, 200
@console_ns.route("/datasets/<uuid:dataset_id>/documents/status/<string:action>/batch")
class DocumentStatusApi(DocumentResource):
@setup_required
@login_required
@@ -949,7 +946,6 @@ class DocumentStatusApi(DocumentResource):
return {"result": "success"}, 200
@console_ns.route("/datasets/<uuid:dataset_id>/documents/<uuid:document_id>/processing/pause")
class DocumentPauseApi(DocumentResource):
@setup_required
@login_required
@@ -983,7 +979,6 @@ class DocumentPauseApi(DocumentResource):
return {"result": "success"}, 204
@console_ns.route("/datasets/<uuid:dataset_id>/documents/<uuid:document_id>/processing/resume")
class DocumentRecoverApi(DocumentResource):
@setup_required
@login_required
@@ -1014,7 +1009,6 @@ class DocumentRecoverApi(DocumentResource):
return {"result": "success"}, 204
@console_ns.route("/datasets/<uuid:dataset_id>/retry")
class DocumentRetryApi(DocumentResource):
@setup_required
@login_required
@@ -1058,7 +1052,6 @@ class DocumentRetryApi(DocumentResource):
return {"result": "success"}, 204
@console_ns.route("/datasets/<uuid:dataset_id>/documents/<uuid:document_id>/rename")
class DocumentRenameApi(DocumentResource):
@setup_required
@login_required
@@ -1082,7 +1075,6 @@ class DocumentRenameApi(DocumentResource):
return document
@console_ns.route("/datasets/<uuid:dataset_id>/documents/<uuid:document_id>/website-sync")
class WebsiteDocumentSyncApi(DocumentResource):
@setup_required
@login_required
@@ -15,6 +15,7 @@ from libs.datetime_utils import naive_utc_now
from libs.login import current_user, login_required
from models import Account, App, InstalledApp, RecommendedApp
from services.account_service import TenantService
from services.app_service import AppService
from services.enterprise.enterprise_service import EnterpriseService
from services.feature_service import FeatureService
@@ -67,26 +68,31 @@ class InstalledAppsListApi(Resource):
# Pre-filter out apps without setting or with sso_verified
filtered_installed_apps = []
app_id_to_app_code = {}
for installed_app in installed_app_list:
app_id = installed_app["app"].id
webapp_setting = webapp_settings.get(app_id)
if not webapp_setting or webapp_setting.access_mode == "sso_verified":
continue
app_code = AppService.get_app_code_by_id(str(app_id))
app_id_to_app_code[app_id] = app_code
filtered_installed_apps.append(installed_app)
app_codes = list(app_id_to_app_code.values())
# Batch permission check
app_ids = [installed_app["app"].id for installed_app in filtered_installed_apps]
permissions = EnterpriseService.WebAppAuth.batch_is_user_allowed_to_access_webapps(
user_id=user_id,
app_ids=app_ids,
app_codes=app_codes,
)
# Keep only allowed apps
res = []
for installed_app in filtered_installed_apps:
app_id = installed_app["app"].id
if permissions.get(app_id):
app_code = app_id_to_app_code[app_id]
if permissions.get(app_code):
res.append(installed_app)
installed_app_list = res
+3 -1
View File
@@ -11,6 +11,7 @@ from controllers.console.wraps import account_initialization_required
from extensions.ext_database import db
from libs.login import login_required
from models import InstalledApp
from services.app_service import AppService
from services.enterprise.enterprise_service import EnterpriseService
from services.feature_service import FeatureService
@@ -56,9 +57,10 @@ def user_allowed_to_access_app(view: Callable[Concatenate[InstalledApp, P], R] |
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_id=app_id,
app_code=app_code,
)
if not res:
raise AppAccessDeniedError()
@@ -30,6 +30,7 @@ from extensions.ext_database import db
from fields.document_fields import document_fields, document_status_fields
from libs.login import current_user
from models.dataset import Dataset, Document, DocumentSegment
from models.model import EndUser
from services.dataset_service import DatasetService, DocumentService
from services.entities.knowledge_entities.knowledge_entities import KnowledgeConfig
from services.file_service import FileService
@@ -298,6 +299,9 @@ class DocumentAddByFileApi(DatasetApiResource):
if not file.filename:
raise FilenameNotExistsError
if not isinstance(current_user, EndUser):
raise ValueError("Invalid user account")
upload_file = FileService.upload_file(
filename=file.filename,
content=file.read(),
@@ -387,6 +391,8 @@ class DocumentUpdateByFileApi(DatasetApiResource):
raise FilenameNotExistsError
try:
if not isinstance(current_user, EndUser):
raise ValueError("Invalid user account")
upload_file = FileService.upload_file(
filename=file.filename,
content=file.read(),
+2 -1
View File
@@ -160,8 +160,9 @@ class AppWebAuthPermission(Resource):
args = parser.parse_args()
app_id = args["appId"]
app_code = AppService.get_app_code_by_id(app_id)
res = True
if WebAppAuthService.is_app_require_permission_check(app_id=app_id):
res = EnterpriseService.WebAppAuth.is_user_allowed_to_access_webapp(str(user_id), app_id)
res = EnterpriseService.WebAppAuth.is_user_allowed_to_access_webapp(str(user_id), app_code)
return {"result": res}
+4 -10
View File
@@ -12,7 +12,6 @@ from controllers.web.error import WebAppAuthRequiredError
from extensions.ext_database import db
from libs.passport import PassportService
from models.model import App, EndUser, Site
from services.app_service import AppService
from services.enterprise.enterprise_service import EnterpriseService
from services.feature_service import FeatureService
from services.webapp_auth_service import WebAppAuthService, WebAppAuthType
@@ -39,7 +38,7 @@ class PassportResource(Resource):
if app_code is None:
raise Unauthorized("X-App-Code header is missing.")
app_id = AppService.get_app_id_by_code(app_code)
# exchange token for enterprise logined web user
enterprise_user_decoded = decode_enterprise_webapp_user_id(web_app_access_token)
if enterprise_user_decoded:
@@ -49,7 +48,7 @@ class PassportResource(Resource):
)
if system_features.webapp_auth.enabled:
app_settings = EnterpriseService.WebAppAuth.get_app_access_mode_by_id(app_id=app_id)
app_settings = EnterpriseService.WebAppAuth.get_app_access_mode_by_code(app_code=app_code)
if not app_settings or not app_settings.access_mode == "public":
raise WebAppAuthRequiredError()
@@ -127,8 +126,6 @@ def exchange_token_for_existing_web_user(app_code: str, enterprise_user_decoded:
end_user_id = enterprise_user_decoded.get("end_user_id")
session_id = enterprise_user_decoded.get("session_id")
user_auth_type = enterprise_user_decoded.get("auth_type")
exchanged_token_expires_unix = enterprise_user_decoded.get("exp")
if not user_auth_type:
raise Unauthorized("Missing auth_type in the token.")
@@ -172,11 +169,8 @@ def exchange_token_for_existing_web_user(app_code: str, enterprise_user_decoded:
)
db.session.add(end_user)
db.session.commit()
exp = int((datetime.now(UTC) + timedelta(minutes=dify_config.ACCESS_TOKEN_EXPIRE_MINUTES)).timestamp())
if exchanged_token_expires_unix:
exp = int(exchanged_token_expires_unix)
exp_dt = datetime.now(UTC) + timedelta(minutes=dify_config.ACCESS_TOKEN_EXPIRE_MINUTES)
exp = int(exp_dt.timestamp())
payload = {
"iss": site.id,
"sub": "Web API Passport",
+6 -37
View File
@@ -13,7 +13,6 @@ from controllers.web.error import WebAppAuthAccessDeniedError, WebAppAuthRequire
from extensions.ext_database import db
from libs.passport import PassportService
from models.model import App, EndUser, Site
from services.app_service import AppService
from services.enterprise.enterprise_service import EnterpriseService, WebAppSettings
from services.feature_service import FeatureService
from services.webapp_auth_service import WebAppAuthService
@@ -38,11 +37,7 @@ def validate_jwt_token(view: Callable[Concatenate[App, EndUser, P], R] | None =
def decode_jwt_token():
system_features = FeatureService.get_system_features()
app_code = request.headers.get("X-App-Code")
if not app_code:
app_code = None
else:
app_code = str(app_code)
app_code = str(request.headers.get("X-App-Code"))
try:
auth_header = request.headers.get("Authorization")
if auth_header is None:
@@ -56,30 +51,15 @@ def decode_jwt_token():
if auth_scheme != "bearer":
raise Unauthorized("Invalid Authorization header format. Expected 'Bearer <api-key>' format.")
# Check for invalid token values
if tk in ["undefined", "null", "None", ""]:
raise Unauthorized("Invalid token provided.")
decoded = PassportService().verify(tk)
# Preserve app_code from header if JWT token doesn't contain one
jwt_app_code = decoded.get("app_code")
if jwt_app_code:
app_code = jwt_app_code
app_code = decoded.get("app_code")
app_id = decoded.get("app_id")
# Validate required fields from JWT token
if not app_id:
raise Unauthorized("Invalid token: missing app_id.")
if not app_code:
raise Unauthorized("Invalid token: missing app_code.")
with Session(db.engine, expire_on_commit=False) as session:
app_model = session.scalar(select(App).where(App.id == app_id))
site = session.scalar(select(Site).where(Site.code == app_code))
if not app_model:
raise NotFound()
if not site:
if not app_code or not site:
raise BadRequest("Site URL is no longer valid.")
if app_model.enable_site is False:
raise BadRequest("Site is disabled.")
@@ -92,12 +72,7 @@ def decode_jwt_token():
app_web_auth_enabled = False
webapp_settings = None
if system_features.webapp_auth.enabled:
if not app_code:
raise BadRequest("App code is required for webapp authentication.")
if app_code in ["undefined", "null", "None", ""]:
raise BadRequest("Invalid app code provided.")
app_id = AppService.get_app_id_by_code(app_code)
webapp_settings = EnterpriseService.WebAppAuth.get_app_access_mode_by_id(app_id)
webapp_settings = EnterpriseService.WebAppAuth.get_app_access_mode_by_code(app_code=app_code)
if not webapp_settings:
raise NotFound("Web app settings not found.")
app_web_auth_enabled = webapp_settings.access_mode != "public"
@@ -112,11 +87,8 @@ def decode_jwt_token():
if system_features.webapp_auth.enabled:
if not app_code:
raise Unauthorized("Please re-login to access the web app.")
if app_code in ["undefined", "null", "None", ""]:
raise Unauthorized("Invalid app code provided.")
app_id = AppService.get_app_id_by_code(app_code)
app_web_auth_enabled = (
EnterpriseService.WebAppAuth.get_app_access_mode_by_id(app_id=app_id).access_mode != "public"
EnterpriseService.WebAppAuth.get_app_access_mode_by_code(app_code=str(app_code)).access_mode != "public"
)
if app_web_auth_enabled:
raise WebAppAuthRequiredError()
@@ -157,10 +129,7 @@ def _validate_user_accessibility(
raise WebAppAuthRequiredError("Web app settings not found.")
if WebAppAuthService.is_app_require_permission_check(access_mode=webapp_settings.access_mode):
if not app_code or app_code in ["undefined", "null", "None", ""]:
raise WebAppAuthAccessDeniedError("Invalid app code for permission check.")
app_id = AppService.get_app_id_by_code(app_code)
if not EnterpriseService.WebAppAuth.is_user_allowed_to_access_webapp(user_id, app_id):
if not EnterpriseService.WebAppAuth.is_user_allowed_to_access_webapp(user_id, app_code=app_code):
raise WebAppAuthAccessDeniedError()
auth_type = decoded.get("auth_type")
@@ -1140,15 +1140,6 @@ class ProviderConfiguration(BaseModel):
raise ValueError("Can't add same credential")
provider_model_record.credential_id = credential_record.id
provider_model_record.updated_at = naive_utc_now()
# clear cache
provider_model_credentials_cache = ProviderCredentialsCache(
tenant_id=self.tenant_id,
identity_id=provider_model_record.id,
cache_type=ProviderCredentialsCacheType.MODEL,
)
provider_model_credentials_cache.delete()
session.add(provider_model_record)
session.commit()
@@ -1182,14 +1173,6 @@ class ProviderConfiguration(BaseModel):
session.add(provider_model_record)
session.commit()
# clear cache
provider_model_credentials_cache = ProviderCredentialsCache(
tenant_id=self.tenant_id,
identity_id=provider_model_record.id,
cache_type=ProviderCredentialsCacheType.MODEL,
)
provider_model_credentials_cache.delete()
def delete_custom_model(self, model_type: ModelType, model: str):
"""
Delete custom model.
+2 -8
View File
@@ -394,14 +394,8 @@ class ApiTool(Tool):
parsed_response = self.validate_and_parse_response(response)
# assemble invoke message based on response type
if parsed_response.is_json:
if isinstance(parsed_response.content, dict):
yield self.create_json_message(parsed_response.content)
# The yield below must be preserved to keep backward compatibility.
#
# ref: https://github.com/langgenius/dify/pull/23456#issuecomment-3182413088
yield self.create_text_message(response.text)
if parsed_response.is_json and isinstance(parsed_response.content, dict):
yield self.create_json_message(parsed_response.content)
else:
# Convert to string if needed and create text message
text_response = (
+1 -7
View File
@@ -318,13 +318,7 @@ class ToolNode(BaseNode):
json.append(message.message.json_object)
elif message.type == ToolInvokeMessage.MessageType.LINK:
assert isinstance(message.message, ToolInvokeMessage.TextMessage)
# Check if this LINK message is a file link
file_obj = (message.meta or {}).get("file")
if isinstance(file_obj, File):
files.append(file_obj)
stream_text = f"File: {message.message.text}\n"
else:
stream_text = f"Link: {message.message.text}\n"
stream_text = f"Link: {message.message.text}\n"
text += stream_text
yield RunStreamChunkEvent(chunk_content=stream_text, from_variable_selector=[node_id, "text"])
elif message.type == ToolInvokeMessage.MessageType.VARIABLE:
+1 -1
View File
@@ -354,7 +354,7 @@ class Workflow(Base):
if not tenant_id:
return []
environment_variables_dict: dict[str, Any] = json.loads(self._environment_variables or "{}")
environment_variables_dict: dict[str, Any] = json.loads(self._environment_variables)
results = [
variable_factory.build_environment_variable_from_mapping(v) for v in environment_variables_dict.values()
]
-5
View File
@@ -1041,8 +1041,6 @@ class TenantService:
db.session.add(ta)
db.session.commit()
if dify_config.BILLING_ENABLED:
BillingService.clean_billing_info_cache(tenant.id)
return ta
@staticmethod
@@ -1201,9 +1199,6 @@ class TenantService:
db.session.delete(ta)
db.session.commit()
if dify_config.BILLING_ENABLED:
BillingService.clean_billing_info_cache(tenant.id)
@staticmethod
def update_member_role(tenant: Tenant, member: Account, new_role: str, operator: Account):
"""Update member role"""
-7
View File
@@ -20,7 +20,6 @@ from libs.login import current_user
from models.account import Account
from models.model import App, AppMode, AppModelConfig, Site
from models.tools import ApiToolProvider
from services.billing_service import BillingService
from services.enterprise.enterprise_service import EnterpriseService
from services.feature_service import FeatureService
from services.tag_service import TagService
@@ -163,9 +162,6 @@ class AppService:
# update web app setting as private
EnterpriseService.WebAppAuth.update_app_access_mode(app.id, "private")
if dify_config.BILLING_ENABLED:
BillingService.clean_billing_info_cache(app.tenant_id)
return app
def get_app(self, app: App) -> App:
@@ -341,9 +337,6 @@ class AppService:
if FeatureService.get_system_features().webapp_auth.enabled:
EnterpriseService.WebAppAuth.cleanup_webapp(app.id)
if dify_config.BILLING_ENABLED:
BillingService.clean_billing_info_cache(app.tenant_id)
# Trigger asynchronous deletion of app and related data
remove_app_and_related_data_task.delay(tenant_id=app.tenant_id, app_id=app.id)
-5
View File
@@ -5,7 +5,6 @@ import httpx
from tenacity import retry, retry_if_exception_type, stop_before_delay, wait_fixed
from extensions.ext_database import db
from extensions.ext_redis import redis_client
from libs.helper import RateLimiter
from models.account import Account, TenantAccountJoin, TenantAccountRole
@@ -174,7 +173,3 @@ class BillingService:
res = cls._send_request("POST", "/compliance/download", json=json)
cls.compliance_download_rate_limiter.increment_rate_limit(limiter_key)
return res
@classmethod
def clean_billing_info_cache(cls, tenant_id: str):
redis_client.delete(f"tenant:{tenant_id}:billing_info")
+18 -8
View File
@@ -46,17 +46,17 @@ class EnterpriseService:
class WebAppAuth:
@classmethod
def is_user_allowed_to_access_webapp(cls, user_id: str, app_id: str):
params = {"userId": user_id, "appId": app_id}
def is_user_allowed_to_access_webapp(cls, user_id: str, app_code: str):
params = {"userId": user_id, "appCode": app_code}
data = EnterpriseRequest.send_request("GET", "/webapp/permission", params=params)
return data.get("result", False)
@classmethod
def batch_is_user_allowed_to_access_webapps(cls, user_id: str, app_ids: list[str]):
if not app_ids:
def batch_is_user_allowed_to_access_webapps(cls, user_id: str, app_codes: list[str]):
if not app_codes:
return {}
body = {"userId": user_id, "appIds": app_ids}
body = {"userId": user_id, "appCodes": app_codes}
data = EnterpriseRequest.send_request("POST", "/webapp/permission/batch", json=body)
if not data:
raise ValueError("No data found.")
@@ -92,6 +92,16 @@ class EnterpriseService:
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):
if not app_id:
@@ -104,11 +114,11 @@ class EnterpriseService:
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.")
params = {"appId": app_id}
EnterpriseRequest.send_request("DELETE", "/webapp/clean", params=params)
body = {"appId": app_id}
EnterpriseRequest.send_request("DELETE", "/webapp/clean", json=body)
+1 -2
View File
@@ -172,8 +172,7 @@ class WebAppAuthService:
return WebAppAuthType.EXTERNAL
if app_code:
app_id = AppService.get_app_id_by_code(app_code)
webapp_settings = EnterpriseService.WebAppAuth.get_app_access_mode_by_id(app_id)
webapp_settings = EnterpriseService.WebAppAuth.get_app_access_mode_by_code(app_code)
return cls.get_app_auth_type(access_mode=webapp_settings.access_mode)
raise ValueError("Could not determine app authentication type.")
+1 -2
View File
@@ -452,8 +452,7 @@ class WorkflowService:
)
if not default_provider:
# plugin does not require credentials, skip
return
raise ValueError("No default credential found")
# Check credential policy compliance using the default credential ID
from core.helper.credential_utils import check_credential_policy_compliance
@@ -42,8 +42,7 @@
font-family: Inter;
font-style: normal;
font-weight: 600;
line-height: 120%;
/* 28.8px */
line-height: 120%; /* 28.8px */
}
.description {
@@ -52,8 +51,7 @@
font-family: Inter;
font-style: normal;
font-weight: 400;
line-height: 20px;
/* 142.857% */
line-height: 20px; /* 142.857% */
letter-spacing: -0.07px;
}
@@ -98,8 +96,7 @@
font-family: Inter;
font-style: normal;
font-weight: 400;
line-height: 20px;
/* 142.857% */
line-height: 20px; /* 142.857% */
letter-spacing: -0.07px;
}
</style>
@@ -110,7 +107,7 @@
<div class="header"></div>
<p class="title">Confirm Your New Email Address</p>
<div class="description">
<p class="content1">You're updating the email address linked to your account.</p>
<p class="content1">Youre updating the email address linked to your Dify account.</p>
<p class="content2">To confirm this action, please use the verification code below.</p>
<p class="content3">This code will only be valid for the next 5 minutes:</p>
</div>
@@ -121,4 +118,5 @@
</div>
</body>
</html>
</html>
@@ -42,8 +42,7 @@
font-family: Inter;
font-style: normal;
font-weight: 600;
line-height: 120%;
/* 28.8px */
line-height: 120%; /* 28.8px */
}
.description {
@@ -52,8 +51,7 @@
font-family: Inter;
font-style: normal;
font-weight: 400;
line-height: 20px;
/* 142.857% */
line-height: 20px; /* 142.857% */
letter-spacing: -0.07px;
}
@@ -98,8 +96,7 @@
font-family: Inter;
font-style: normal;
font-weight: 400;
line-height: 20px;
/* 142.857% */
line-height: 20px; /* 142.857% */
letter-spacing: -0.07px;
}
</style>
@@ -110,7 +107,7 @@
<div class="header"></div>
<p class="title">确认您的邮箱地址变更</p>
<div class="description">
<p class="content1">您正在更新与您的账户关联的邮箱地址。</p>
<p class="content1">您正在更新与您的 Dify 账户关联的邮箱地址。</p>
<p class="content2">为了确认此操作,请使用以下验证码。</p>
<p class="content3">此验证码仅在接下来的5分钟内有效:</p>
</div>
@@ -121,4 +118,5 @@
</div>
</body>
</html>
</html>
@@ -42,8 +42,7 @@
font-family: Inter;
font-style: normal;
font-weight: 600;
line-height: 120%;
/* 28.8px */
line-height: 120%; /* 28.8px */
}
.description {
@@ -52,8 +51,7 @@
font-family: Inter;
font-style: normal;
font-weight: 400;
line-height: 20px;
/* 142.857% */
line-height: 20px; /* 142.857% */
letter-spacing: -0.07px;
}
@@ -98,8 +96,7 @@
font-family: Inter;
font-style: normal;
font-weight: 400;
line-height: 20px;
/* 142.857% */
line-height: 20px; /* 142.857% */
letter-spacing: -0.07px;
}
</style>
@@ -110,7 +107,7 @@
<div class="header"></div>
<p class="title">Verify Your Request to Change Email</p>
<div class="description">
<p class="content1">We received a request to change the email address associated with your account.</p>
<p class="content1">We received a request to change the email address associated with your Dify account.</p>
<p class="content2">To confirm this action, please use the verification code below.</p>
<p class="content3">This code will only be valid for the next 5 minutes:</p>
</div>
@@ -121,4 +118,5 @@
</div>
</body>
</html>
</html>
@@ -42,8 +42,7 @@
font-family: Inter;
font-style: normal;
font-weight: 600;
line-height: 120%;
/* 28.8px */
line-height: 120%; /* 28.8px */
}
.description {
@@ -52,8 +51,7 @@
font-family: Inter;
font-style: normal;
font-weight: 400;
line-height: 20px;
/* 142.857% */
line-height: 20px; /* 142.857% */
letter-spacing: -0.07px;
}
@@ -98,8 +96,7 @@
font-family: Inter;
font-style: normal;
font-weight: 400;
line-height: 20px;
/* 142.857% */
line-height: 20px; /* 142.857% */
letter-spacing: -0.07px;
}
</style>
@@ -110,7 +107,7 @@
<div class="header"></div>
<p class="title">验证您的邮箱变更请求</p>
<div class="description">
<p class="content1">我们收到了一个变更您账户关联邮箱地址的请求。</p>
<p class="content1">我们收到了一个变更您 Dify 账户关联邮箱地址的请求。</p>
<p class="content3">此验证码仅在接下来的5分钟内有效:</p>
</div>
<div class="code-content">
@@ -120,4 +117,5 @@
</div>
</body>
</html>
</html>
@@ -1,6 +1,5 @@
<!DOCTYPE html>
<html>
<head>
<style>
body {
@@ -11,7 +10,6 @@
margin: 0;
padding: 0;
}
.container {
width: 504px;
min-height: 444px;
@@ -32,7 +30,6 @@
max-width: 63px;
height: auto;
}
.button {
display: block;
padding: 8px 12px;
@@ -48,56 +45,49 @@
font-size: 14px;
font-style: normal;
font-weight: 600;
line-height: 20px;
/* 142.857% */
line-height: 20px; /* 142.857% */
}
.button:hover {
background-color: #004AEB;
border: 0.5px solid rgba(16, 24, 40, 0.08);
box-shadow: 0px 1px 2px 0px rgba(9, 9, 11, 0.05);
}
.content {
color: #354052;
font-family: Inter;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 20px;
/* 142.857% */
line-height: 20px; /* 142.857% */
letter-spacing: -0.07px;
}
.content1 {
margin: 0;
padding-top: 24px;
padding-bottom: 12px;
font-weight: 500;
}
.content2 {
margin: 0;
padding-bottom: 12px;
}
</style>
</head>
<body>
<div class="container">
<div class="header"></div>
<div class="header">
<!-- Optional: Add a logo or a header image here -->
<img src="https://assets.dify.ai/images/logo.png" alt="Dify Logo">
</div>
<div class="content">
<p class="content1">Dear {{ to }},</p>
<p class="content2">{{ 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 class="content2">{{ 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 class="content2">Click the button below to log in to {{application_title}} and join the workspace.</p>
<p style="text-align: center; margin: 0; margin-bottom: 32px;"><a style="color: #fff; text-decoration: none"
class="button" href="{{ url }}">Login Here</a></p>
<p style="text-align: center; margin: 0; margin-bottom: 32px;"><a style="color: #fff; text-decoration: none" class="button" href="{{ url }}">Login Here</a></p>
<p class="content2">Best regards,</p>
<p class="content2">{{application_title}} Team</p>
</div>
</div>
</body>
</html>
</html>
@@ -42,8 +42,7 @@
font-family: Inter;
font-style: normal;
font-weight: 600;
line-height: 120%;
/* 28.8px */
line-height: 120%; /* 28.8px */
}
.description {
@@ -52,8 +51,7 @@
font-family: Inter;
font-style: normal;
font-weight: 400;
line-height: 20px;
/* 142.857% */
line-height: 20px; /* 142.857% */
letter-spacing: -0.07px;
}
@@ -82,9 +80,10 @@
<div class="description">
<p class="content1">You have been assigned as the new owner of the workspace "{{WorkspaceName}}".</p>
<p class="content2">As the new owner, you now have full administrative privileges for this workspace.</p>
<p class="content3">If you have any questions, please contact support.</p>
<p class="content3">If you have any questions, please contact support@dify.ai.</p>
</div>
</div>
</body>
</html>
</html>
@@ -42,8 +42,7 @@
font-family: Inter;
font-style: normal;
font-weight: 600;
line-height: 120%;
/* 28.8px */
line-height: 120%; /* 28.8px */
}
.description {
@@ -52,8 +51,7 @@
font-family: Inter;
font-style: normal;
font-weight: 400;
line-height: 20px;
/* 142.857% */
line-height: 20px; /* 142.857% */
letter-spacing: -0.07px;
}
@@ -82,9 +80,10 @@
<div class="description">
<p class="content1">您已被分配为工作空间“{{WorkspaceName}}”的新所有者。</p>
<p class="content2">作为新所有者,您现在对该工作空间拥有完全的管理权限。</p>
<p class="content3">如果您有任何问题,请联系支持团队</p>
<p class="content3">如果您有任何问题,请联系support@dify.ai</p>
</div>
</div>
</body>
</html>
</html>
@@ -42,8 +42,7 @@
font-family: Inter;
font-style: normal;
font-weight: 600;
line-height: 120%;
/* 28.8px */
line-height: 120%; /* 28.8px */
}
.description {
@@ -52,8 +51,7 @@
font-family: Inter;
font-style: normal;
font-weight: 400;
line-height: 20px;
/* 142.857% */
line-height: 20px; /* 142.857% */
letter-spacing: -0.07px;
}
@@ -99,8 +97,7 @@
font-family: Inter;
font-style: normal;
font-weight: 400;
line-height: 20px;
/* 142.857% */
line-height: 20px; /* 142.857% */
letter-spacing: -0.07px;
}
</style>
@@ -111,14 +108,12 @@
<div class="header"></div>
<p class="title">Workspace ownership has been transferred</p>
<div class="description">
<p class="content1">You have successfully transferred ownership of the workspace "{{WorkspaceName}}" to
{{NewOwnerEmail}}.</p>
<p class="content2">You no longer have owner privileges for this workspace. Your access level has been changed to
Admin.</p>
<p class="content3">If you did not initiate this transfer or have concerns about this change, please contact
support immediately.</p>
<p class="content1">You have successfully transferred ownership of the workspace "{{WorkspaceName}}" to {{NewOwnerEmail}}.</p>
<p class="content2">You no longer have owner privileges for this workspace. Your access level has been changed to Admin.</p>
<p class="content3">If you did not initiate this transfer or have concerns about this change, please contact support@dify.ai immediately.</p>
</div>
</div>
</body>
</html>
</html>
@@ -42,8 +42,7 @@
font-family: Inter;
font-style: normal;
font-weight: 600;
line-height: 120%;
/* 28.8px */
line-height: 120%; /* 28.8px */
}
.description {
@@ -52,8 +51,7 @@
font-family: Inter;
font-style: normal;
font-weight: 400;
line-height: 20px;
/* 142.857% */
line-height: 20px; /* 142.857% */
letter-spacing: -0.07px;
}
@@ -99,8 +97,7 @@
font-family: Inter;
font-style: normal;
font-weight: 400;
line-height: 20px;
/* 142.857% */
line-height: 20px; /* 142.857% */
letter-spacing: -0.07px;
}
</style>
@@ -113,9 +110,10 @@
<div class="description">
<p class="content1">您已成功将工作空间“{{WorkspaceName}}”的所有权转移给{{NewOwnerEmail}}。</p>
<p class="content2">您不再拥有此工作空间的拥有者权限。您的访问级别已更改为管理员。</p>
<p class="content3">如果您没有发起此转移或对此变更有任何疑问,请立即联系支持团队</p>
<p class="content3">如果您没有发起此转移或对此变更有任何疑问,请立即联系support@dify.ai</p>
</div>
</div>
</body>
</html>
</html>
@@ -1,14 +1,12 @@
"""Integration tests for ChatMessageApi permission verification."""
import uuid
from types import SimpleNamespace
from unittest import mock
import pytest
from flask.testing import FlaskClient
from controllers.console.app import completion as completion_api
from controllers.console.app import message as message_api
from controllers.console.app import wraps
from libs.datetime_utils import naive_utc_now
from models import Account, App, Tenant
@@ -101,106 +99,3 @@ class TestChatMessageApiPermissions:
)
assert response.status_code == status
@pytest.mark.parametrize(
("role", "status"),
[
(TenantAccountRole.OWNER, 200),
(TenantAccountRole.ADMIN, 200),
(TenantAccountRole.EDITOR, 200),
(TenantAccountRole.NORMAL, 403),
(TenantAccountRole.DATASET_OPERATOR, 403),
],
)
def test_get_requires_edit_permission(
self,
test_client: FlaskClient,
auth_header,
monkeypatch,
mock_app_model,
mock_account,
role: TenantAccountRole,
status: int,
):
"""Ensure GET chat-messages endpoint enforces edit permissions."""
mock_load_app_model = mock.Mock(return_value=mock_app_model)
monkeypatch.setattr(wraps, "_load_app_model", mock_load_app_model)
conversation_id = uuid.uuid4()
created_at = naive_utc_now()
mock_conversation = SimpleNamespace(id=str(conversation_id), app_id=str(mock_app_model.id))
mock_message = SimpleNamespace(
id=str(uuid.uuid4()),
conversation_id=str(conversation_id),
inputs=[],
query="hello",
message=[{"text": "hello"}],
message_tokens=0,
re_sign_file_url_answer="",
answer_tokens=0,
provider_response_latency=0.0,
from_source="console",
from_end_user_id=None,
from_account_id=mock_account.id,
feedbacks=[],
workflow_run_id=None,
annotation=None,
annotation_hit_history=None,
created_at=created_at,
agent_thoughts=[],
message_files=[],
message_metadata_dict={},
status="success",
error="",
parent_message_id=None,
)
class MockQuery:
def __init__(self, model):
self.model = model
def where(self, *args, **kwargs):
return self
def first(self):
if getattr(self.model, "__name__", "") == "Conversation":
return mock_conversation
return None
def order_by(self, *args, **kwargs):
return self
def limit(self, *_):
return self
def all(self):
if getattr(self.model, "__name__", "") == "Message":
return [mock_message]
return []
mock_session = mock.Mock()
mock_session.query.side_effect = MockQuery
mock_session.scalar.return_value = False
monkeypatch.setattr(message_api, "db", SimpleNamespace(session=mock_session))
monkeypatch.setattr(message_api, "current_user", mock_account)
class DummyPagination:
def __init__(self, data, limit, has_more):
self.data = data
self.limit = limit
self.has_more = has_more
monkeypatch.setattr(message_api, "InfiniteScrollPagination", DummyPagination)
mock_account.role = role
response = test_client.get(
f"/console/api/apps/{mock_app_model.id}/chat-messages",
headers=auth_header,
query_string={"conversation_id": str(conversation_id)},
)
assert response.status_code == status
@@ -35,7 +35,9 @@ class TestWebAppAuthService:
mock_enterprise_service.WebAppAuth.get_app_access_mode_by_id.return_value = type(
"MockWebAppAuth", (), {"access_mode": "private"}
)()
# Note: get_app_access_mode_by_code method was removed in refactoring
mock_enterprise_service.WebAppAuth.get_app_access_mode_by_code.return_value = type(
"MockWebAppAuth", (), {"access_mode": "private"}
)()
yield {
"passport_service": mock_passport_service,
@@ -864,7 +866,7 @@ class TestWebAppAuthService:
mock_webapp_auth = type("MockWebAppAuth", (), {"access_mode": "sso_verified"})()
mock_external_service_dependencies[
"enterprise_service"
].WebAppAuth.get_app_access_mode_by_id.return_value = mock_webapp_auth
].WebAppAuth.get_app_access_mode_by_code.return_value = mock_webapp_auth
# Act: Execute authentication type determination
result = WebAppAuthService.get_app_auth_type(app_code="mock_app_code")
@@ -875,7 +877,7 @@ class TestWebAppAuthService:
# Verify mock service was called correctly
mock_external_service_dependencies[
"enterprise_service"
].WebAppAuth.get_app_access_mode_by_id.assert_called_once_with("mock_app_id")
].WebAppAuth.get_app_access_mode_by_code.assert_called_once_with("mock_app_code")
def test_get_app_auth_type_no_parameters(self, db_session_with_containers, mock_external_service_dependencies):
"""
-249
View File
@@ -1,249 +0,0 @@
import json
import operator
from typing import TypeVar
from unittest.mock import Mock, patch
import httpx
import pytest
from core.tools.__base.tool_runtime import ToolRuntime
from core.tools.custom_tool.tool import ApiTool
from core.tools.entities.common_entities import I18nObject
from core.tools.entities.tool_bundle import ApiToolBundle
from core.tools.entities.tool_entities import (
ToolEntity,
ToolIdentity,
ToolInvokeMessage,
)
_T = TypeVar("_T")
def _get_message_by_type(msgs: list[ToolInvokeMessage], msg_type: type[_T]) -> ToolInvokeMessage | None:
return next((i for i in msgs if isinstance(i.message, msg_type)), None)
class TestApiToolInvoke:
"""Test suite for ApiTool._invoke method to ensure JSON responses are properly serialized."""
def setup_method(self):
"""Setup test fixtures."""
# Create a mock tool entity
self.mock_tool_identity = ToolIdentity(
author="test",
name="test_api_tool",
label=I18nObject(en_US="Test API Tool", zh_Hans="测试API工具"),
provider="test_provider",
)
self.mock_tool_entity = ToolEntity(identity=self.mock_tool_identity)
# Create a mock API bundle
self.mock_api_bundle = ApiToolBundle(
server_url="https://api.example.com/test",
method="GET",
openapi={},
operation_id="test_operation",
parameters=[],
author="test_author",
)
# Create a mock runtime
self.mock_runtime = Mock(spec=ToolRuntime)
self.mock_runtime.credentials = {"auth_type": "none"}
# Create the ApiTool instance
self.api_tool = ApiTool(
entity=self.mock_tool_entity,
api_bundle=self.mock_api_bundle,
runtime=self.mock_runtime,
provider_id="test_provider",
)
@patch("core.tools.custom_tool.tool.ssrf_proxy.get")
def test_invoke_with_json_response_creates_text_message_with_serialized_json(self, mock_get: Mock) -> None:
"""Test that when upstream returns JSON, the output Text message contains JSON-serialized string."""
# Setup mock response with JSON content
json_response_data = {
"key": "value",
"number": 123,
"nested": {"inner": "data"},
}
mock_response = Mock(spec=httpx.Response)
mock_response.status_code = 200
mock_response.content = json.dumps(json_response_data).encode("utf-8")
mock_response.json.return_value = json_response_data
mock_response.text = json.dumps(json_response_data)
mock_response.headers = {"content-type": "application/json"}
mock_get.return_value = mock_response
# Invoke the tool
result_generator = self.api_tool._invoke(user_id="test_user", tool_parameters={})
# Get the result from the generator
result = list(result_generator)
assert len(result) == 2
# Verify _invoke yields text message
text_message = _get_message_by_type(result, ToolInvokeMessage.TextMessage)
assert text_message is not None, "_invoke should yield a text message"
assert isinstance(text_message, ToolInvokeMessage)
assert text_message.type == ToolInvokeMessage.MessageType.TEXT
assert text_message.message is not None
# Verify the text contains the JSON-serialized string
# Check if message is a TextMessage
assert isinstance(text_message.message, ToolInvokeMessage.TextMessage)
# Verify it's a valid JSON string and equals to the mock response
parsed_back = json.loads(text_message.message.text)
assert parsed_back == json_response_data
# Verify _invoke yields json message
json_message = _get_message_by_type(result, ToolInvokeMessage.JsonMessage)
assert json_message is not None, "_invoke should yield a JSON message"
assert isinstance(json_message, ToolInvokeMessage)
assert json_message.type == ToolInvokeMessage.MessageType.JSON
assert json_message.message is not None
assert isinstance(json_message.message, ToolInvokeMessage.JsonMessage)
assert json_message.message.json_object == json_response_data
@patch("core.tools.custom_tool.tool.ssrf_proxy.get")
@pytest.mark.parametrize(
"test_case",
[
(
"array",
[
{"id": 1, "name": "Item 1", "active": True},
{"id": 2, "name": "Item 2", "active": False},
{"id": 3, "name": "项目 3", "active": True},
],
),
(
"string",
"string",
),
(
"number",
123.456,
),
(
"boolean",
True,
),
(
"null",
None,
),
],
ids=operator.itemgetter(0),
)
def test_invoke_with_non_dict_json_response_creates_text_message_with_serialized_json(
self, mock_get: Mock, test_case
) -> None:
"""Test that when upstream returns a non-dict JSON, the output Text message contains JSON-serialized string."""
# Setup mock response with non-dict JSON content
_, json_value = test_case
mock_response = Mock(spec=httpx.Response)
mock_response.status_code = 200
mock_response.content = json.dumps(json_value).encode("utf-8")
mock_response.json.return_value = json_value
mock_response.text = json.dumps(json_value)
mock_response.headers = {"content-type": "application/json"}
mock_get.return_value = mock_response
# Invoke the tool
result_generator = self.api_tool._invoke(user_id="test_user", tool_parameters={})
# Get the result from the generator
result = list(result_generator)
assert len(result) == 1
# Verify _invoke yields a text message
text_message = _get_message_by_type(result, ToolInvokeMessage.TextMessage)
assert text_message is not None, "_invoke should yield a text message containing the serialized JSON."
assert isinstance(text_message, ToolInvokeMessage)
assert text_message.type == ToolInvokeMessage.MessageType.TEXT
assert text_message.message is not None
# Verify the text contains the JSON-serialized string
# Check if message is a TextMessage
assert isinstance(text_message.message, ToolInvokeMessage.TextMessage)
# Verify it's a valid JSON string
parsed_back = json.loads(text_message.message.text)
assert parsed_back == json_value
# Verify _invoke yields json message
json_message = _get_message_by_type(result, ToolInvokeMessage.JsonMessage)
assert json_message is None, "_invoke should not yield a JSON message for JSON array response"
@patch("core.tools.custom_tool.tool.ssrf_proxy.get")
def test_invoke_with_text_response_creates_text_message_with_original_text(self, mock_get: Mock) -> None:
"""Test that when upstream returns plain text, the output Text message contains the original text."""
# Setup mock response with plain text content
text_response_data = "This is a plain text response"
mock_response = Mock(spec=httpx.Response)
mock_response.status_code = 200
mock_response.content = text_response_data.encode("utf-8")
mock_response.json.side_effect = json.JSONDecodeError("Expecting value", "doc", 0)
mock_response.text = text_response_data
mock_response.headers = {"content-type": "text/plain"}
mock_get.return_value = mock_response
# Invoke the tool
result_generator = self.api_tool._invoke(user_id="test_user", tool_parameters={})
# Get the result from the generator
result = list(result_generator)
assert len(result) == 1
# Verify it's a text message with the original text
message = result[0]
assert isinstance(message, ToolInvokeMessage)
assert message.type == ToolInvokeMessage.MessageType.TEXT
assert message.message is not None
# Check if message is a TextMessage
assert isinstance(message.message, ToolInvokeMessage.TextMessage)
assert message.message.text == text_response_data
@patch("core.tools.custom_tool.tool.ssrf_proxy.get")
def test_invoke_with_empty_response(self, mock_get: Mock) -> None:
"""Test that empty responses are handled correctly."""
# Setup mock response with empty content
mock_response = Mock(spec=httpx.Response)
mock_response.status_code = 200
mock_response.content = b""
mock_response.headers = {"content-type": "application/json"}
mock_get.return_value = mock_response
# Invoke the tool
result_generator = self.api_tool._invoke(user_id="test_user", tool_parameters={})
# Get the result from the generator
result = list(result_generator)
assert len(result) == 1
# Verify it's a text message with the empty response message
message = result[0]
assert isinstance(message, ToolInvokeMessage)
assert message.type == ToolInvokeMessage.MessageType.TEXT
assert message.message is not None
# Check if message is a TextMessage
assert isinstance(message.message, ToolInvokeMessage.TextMessage)
assert "Empty response from the tool" in message.message.text
@patch("core.tools.custom_tool.tool.ssrf_proxy.get")
def test_invoke_with_error_response(self, mock_get: Mock) -> None:
"""Test that error responses are handled correctly."""
# Setup mock response with error status code
mock_response = Mock(spec=httpx.Response)
mock_response.status_code = 404
mock_response.text = "Not Found"
mock_get.return_value = mock_response
result_generator = self.api_tool._invoke(user_id="test_user", tool_parameters={})
# Invoke the tool and expect an error
with pytest.raises(Exception) as exc_info:
list(result_generator) # Consume the generator to trigger the error
# Verify the error message
assert "Request failed with status code 404" in str(exc_info.value)
@@ -100,10 +100,7 @@ export default function MailAndPasswordAuth({ isEmailSetup }: MailAndPasswordAut
})
}
}
catch (e: any) {
if (e.code === 'authentication_failed')
Toast.notify({ type: 'error', message: e.message })
}
finally {
setIsLoading(false)
}
@@ -17,9 +17,12 @@ import type {
import { noop } from 'lodash-es'
export type EmbeddedChatbotContextValue = {
appMeta: AppMeta | null
appData: AppData | null
appParams: ChatConfig | null
userCanAccess?: boolean
appInfoError?: any
appInfoLoading?: boolean
appMeta?: AppMeta
appData?: AppData
appParams?: ChatConfig
appChatListDataLoading?: boolean
currentConversationId: string
currentConversationItem?: ConversationItem
@@ -56,10 +59,7 @@ export type EmbeddedChatbotContextValue = {
}
export const EmbeddedChatbotContext = createContext<EmbeddedChatbotContextValue>({
appData: null,
appMeta: null,
appParams: null,
appChatListDataLoading: false,
userCanAccess: false,
currentConversationId: '',
appPrevChatList: [],
pinnedConversationList: [],
@@ -18,6 +18,9 @@ import { CONVERSATION_ID_INFO } from '../constants'
import { buildChatItemTree, getProcessedInputsFromUrlParams, getProcessedSystemVariablesFromUrlParams, getProcessedUserVariablesFromUrlParams } from '../utils'
import { getProcessedFilesFromResponse } from '../../file-uploader/utils'
import {
fetchAppInfo,
fetchAppMeta,
fetchAppParams,
fetchChatList,
fetchConversations,
generationConversationName,
@@ -33,7 +36,8 @@ import { InputVarType } from '@/app/components/workflow/types'
import { TransferMethod } from '@/types/app'
import { addFileInfos, sortAgentSorts } from '@/app/components/tools/utils'
import { noop } from 'lodash-es'
import { useWebAppStore } from '@/context/web-app-context'
import { useGetUserCanAccessApp } from '@/service/access-control'
import { useGlobalPublicStore } from '@/context/global-public-context'
function getFormattedChatList(messages: any[]) {
const newChatList: ChatItem[] = []
@@ -63,10 +67,18 @@ function getFormattedChatList(messages: any[]) {
export const useEmbeddedChatbot = () => {
const isInstalledApp = false
const appInfo = useWebAppStore(s => s.appInfo)
const appMeta = useWebAppStore(s => s.appMeta)
const appParams = useWebAppStore(s => s.appParams)
const appId = useMemo(() => appInfo?.app_id, [appInfo])
const systemFeatures = useGlobalPublicStore(s => s.systemFeatures)
const { data: appInfo, isLoading: appInfoLoading, error: appInfoError } = useSWR('appInfo', fetchAppInfo)
const { isPending: isCheckingPermission, data: userCanAccessResult } = useGetUserCanAccessApp({
appId: appInfo?.app_id,
isInstalledApp,
enabled: systemFeatures.webapp_auth.enabled,
})
const appData = useMemo(() => {
return appInfo
}, [appInfo])
const appId = useMemo(() => appData?.app_id, [appData])
const [userId, setUserId] = useState<string>()
const [conversationId, setConversationId] = useState<string>()
@@ -133,6 +145,8 @@ export const useEmbeddedChatbot = () => {
return currentConversationId
}, [currentConversationId, newConversationId])
const { data: appParams } = useSWR(['appParams', isInstalledApp, appId], () => fetchAppParams(isInstalledApp, appId))
const { data: appMeta } = useSWR(['appMeta', isInstalledApp, appId], () => fetchAppMeta(isInstalledApp, appId))
const { data: appPinnedConversationData } = useSWR(['appConversationData', isInstalledApp, appId, true], () => fetchConversations(isInstalledApp, appId, undefined, true, 100))
const { data: appConversationData, isLoading: appConversationDataLoading, mutate: mutateAppConversationData } = useSWR(['appConversationData', isInstalledApp, appId, false], () => fetchConversations(isInstalledApp, appId, undefined, false, 100))
const { data: appChatListData, isLoading: appChatListDataLoading } = useSWR(chatShouldReloadKey ? ['appChatList', chatShouldReloadKey, isInstalledApp, appId] : null, () => fetchChatList(chatShouldReloadKey, isInstalledApp, appId))
@@ -381,13 +395,16 @@ export const useEmbeddedChatbot = () => {
}, [isInstalledApp, appId, t, notify])
return {
appInfoError,
appInfoLoading: appInfoLoading || (systemFeatures.webapp_auth.enabled && isCheckingPermission),
userCanAccess: systemFeatures.webapp_auth.enabled ? userCanAccessResult?.result : true,
isInstalledApp,
allowResetChat,
appId,
currentConversationId,
currentConversationItem,
handleConversationIdInfoChange,
appData: appInfo,
appData,
appParams: appParams || {} as ChatConfig,
appMeta,
appPinnedConversationData,
@@ -101,6 +101,7 @@ const EmbeddedChatbotWrapper = () => {
const {
appData,
userCanAccess,
appParams,
appMeta,
appChatListDataLoading,
@@ -134,6 +135,7 @@ const EmbeddedChatbotWrapper = () => {
} = useEmbeddedChatbot()
return <EmbeddedChatbotContext.Provider value={{
userCanAccess,
appData,
appParams,
appMeta,
+7
View File
@@ -26,9 +26,16 @@ export const useTags = (translateFromOut?: TFunction) => {
return acc
}, {} as Record<string, Tag>)
const getTagLabel = (name: string) => {
if (!tagsMap[name])
return name
return tagsMap[name].label
}
return {
tags,
tagsMap,
getTagLabel,
}
}
@@ -29,7 +29,7 @@ const CardWrapper = ({
setFalse: hideInstallFromMarketplace,
}] = useBoolean(false)
const { locale: localeFromLocale } = useI18N()
const { tagsMap } = useTags(t)
const { getTagLabel } = useTags(t)
if (showInstallButton) {
return (
@@ -43,7 +43,7 @@ const CardWrapper = ({
footer={
<CardMoreInfo
downloadCount={plugin.install_count}
tags={plugin.tags.map(tag => tagsMap[tag.name].label)}
tags={plugin.tags.map(tag => getTagLabel(tag.name))}
/>
}
/>
@@ -92,7 +92,7 @@ const CardWrapper = ({
footer={
<CardMoreInfo
downloadCount={plugin.install_count}
tags={plugin.tags.map(tag => tagsMap[tag.name].label)}
tags={plugin.tags.map(tag => getTagLabel(tag.name))}
/>
}
/>
@@ -7,6 +7,7 @@ import type {
PluginsSearchParams,
} from '@/app/components/plugins/marketplace/types'
import {
APP_VERSION,
MARKETPLACE_API_PREFIX,
} from '@/config'
import { getMarketplaceUrl } from '@/utils/var'
@@ -49,11 +50,15 @@ export const getMarketplacePluginsByCollectionId = async (collectionId: string,
try {
const url = `${MARKETPLACE_API_PREFIX}/collections/${collectionId}/plugins`
const headers = new Headers({
'X-Dify-Version': APP_VERSION,
})
const marketplaceCollectionPluginsData = await globalThis.fetch(
url,
{
cache: 'no-store',
method: 'POST',
headers,
body: JSON.stringify({
category: query?.category,
exclude: query?.exclude,
@@ -83,7 +88,10 @@ export const getMarketplaceCollectionsAndPlugins = async (query?: CollectionsAnd
marketplaceUrl += `&condition=${query.condition}`
if (query?.type)
marketplaceUrl += `&type=${query.type}`
const marketplaceCollectionsData = await globalThis.fetch(marketplaceUrl, { cache: 'no-store' })
const headers = new Headers({
'X-Dify-Version': APP_VERSION,
})
const marketplaceCollectionsData = await globalThis.fetch(marketplaceUrl, { headers, cache: 'no-store' })
const marketplaceCollectionsDataJson = await marketplaceCollectionsData.json()
marketplaceCollections = marketplaceCollectionsDataJson.data.collections
await Promise.all(marketplaceCollections.map(async (collection: MarketplaceCollection) => {
@@ -27,7 +27,7 @@ const TagsFilter = ({
const { t } = useTranslation()
const [open, setOpen] = useState(false)
const [searchText, setSearchText] = useState('')
const { tags: options, tagsMap } = useTags()
const { tags: options, getTagLabel } = useTags()
const filteredOptions = options.filter(option => option.name.toLowerCase().includes(searchText.toLowerCase()))
const handleCheck = (id: string) => {
if (value.includes(id))
@@ -59,7 +59,7 @@ const TagsFilter = ({
!selectedTagsLength && t('pluginTags.allTags')
}
{
!!selectedTagsLength && value.map(val => tagsMap[val].label).slice(0, 2).join(',')
!!selectedTagsLength && value.map(val => getTagLabel(val)).slice(0, 2).join(',')
}
{
selectedTagsLength > 2 && (
+2 -2
View File
@@ -135,8 +135,8 @@ const NormalForm = () => {
{!systemFeatures.branding.enabled && <p className='body-md-regular mt-2 text-text-tertiary'>{t('login.joinTipStart')}{workspaceName}{t('login.joinTipEnd')}</p>}
</div>
: <div className="mx-auto w-full">
<h2 className="title-4xl-semi-bold text-text-primary">{systemFeatures.branding.enabled ? t('login.pageTitleForE') : t('login.pageTitle')}</h2>
<p className='body-md-regular mt-2 text-text-tertiary'>{t('login.welcome')}</p>
<h2 className="title-4xl-semi-bold text-text-primary">{t('login.pageTitle')}</h2>
{!systemFeatures.branding.enabled && <p className='body-md-regular mt-2 text-text-tertiary'>{t('login.welcome')}</p>}
</div>}
<div className="relative">
<div className="mt-6 flex flex-col gap-3">
+3
View File
@@ -2,6 +2,7 @@ import { InputVarType } from '@/app/components/workflow/types'
import { AgentStrategy } from '@/types/app'
import { PromptRole } from '@/models/debug'
import { DatasetAttr } from '@/types/feature'
import pkg from '../package.json'
const getBooleanConfig = (envVar: string | undefined, dataAttrKey: DatasetAttr, defaultValue: boolean = true) => {
if (envVar !== undefined && envVar !== '')
@@ -294,3 +295,5 @@ export const ZENDESK_FIELD_IDS = {
WORKSPACE_ID: getStringConfig(process.env.NEXT_PUBLIC_ZENDESK_FIELD_ID_WORKSPACE_ID, DatasetAttr.NEXT_PUBLIC_ZENDESK_FIELD_ID_WORKSPACE_ID, ''),
PLAN: getStringConfig(process.env.NEXT_PUBLIC_ZENDESK_FIELD_ID_PLAN, DatasetAttr.NEXT_PUBLIC_ZENDESK_FIELD_ID_PLAN, ''),
}
export const APP_VERSION = pkg.version
-1
View File
@@ -1,6 +1,5 @@
const translation = {
pageTitle: 'Log in to Dify',
pageTitleForE: 'Hey, let\'s get started!',
welcome: '👋 Welcome! Please log in to get started.',
email: 'Email address',
emailPlaceholder: 'Your email',
-1
View File
@@ -1,6 +1,5 @@
const translation = {
pageTitle: 'Dify にログイン',
pageTitleForE: 'はじめましょう!',
welcome: '👋 ようこそ!まずはログインしてご利用ください。',
email: 'メールアドレス',
emailPlaceholder: 'メールアドレスを入力してください',
-1
View File
@@ -1,6 +1,5 @@
const translation = {
pageTitle: '登录 Dify',
pageTitleForE: '嗨,近来可好',
welcome: '👋 欢迎!请登录以开始使用。',
email: '邮箱',
emailPlaceholder: '输入邮箱地址',
-1
View File
@@ -1,6 +1,5 @@
const translation = {
pageTitle: '嗨,近來可好',
pageTitleForE: '嗨,近來可好',
welcome: '👋 歡迎來到 Dify, 登入以繼續',
email: '郵箱',
emailPlaceholder: '輸入郵箱地址',
+5 -1
View File
@@ -2,7 +2,7 @@ import type { AfterResponseHook, BeforeErrorHook, BeforeRequestHook, Hooks } fro
import ky from 'ky'
import type { IOtherOptions } from './base'
import Toast from '@/app/components/base/toast'
import { API_PREFIX, MARKETPLACE_API_PREFIX, PUBLIC_API_PREFIX } from '@/config'
import { API_PREFIX, APP_VERSION, MARKETPLACE_API_PREFIX, PUBLIC_API_PREFIX } from '@/config'
import { getInitialTokenV2, isTokenV1 } from '@/app/components/share/utils'
import { getProcessedSystemVariablesFromUrlParams } from '@/app/components/base/chat/utils'
@@ -151,6 +151,10 @@ async function base<T>(url: string, options: FetchOptionType = {}, otherOptions:
if (deleteContentType)
(headers as any).delete('Content-Type')
// ! For Marketplace API, help to filter tags added in new version
if (isMarketplaceAPI)
(headers as any).set('X-Dify-Version', APP_VERSION)
const client = baseClient.extend({
hooks: {
...baseHooks,