mirror of
https://gitee.com/dify_ai/dify.git
synced 2025-12-06 19:42:42 +08:00
Compare commits
14 Commits
feat/mcp-m
...
release/e-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
780f969121 | ||
|
|
338e0f74b9 | ||
|
|
cad6db5a1d | ||
|
|
2e9b3b8d44 | ||
|
|
5a80f5158f | ||
|
|
5a92e0feee | ||
|
|
4b5196f402 | ||
|
|
7b64569c8c | ||
|
|
6106207039 | ||
|
|
3b4e9b64af | ||
|
|
5073ce6e22 | ||
|
|
1277a57641 | ||
|
|
d3ac5b1dd8 | ||
|
|
39a0b89b9a |
@@ -62,6 +62,9 @@ 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")
|
||||
|
||||
@@ -15,7 +15,6 @@ 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
|
||||
|
||||
@@ -68,31 +67,26 @@ 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_codes=app_codes,
|
||||
app_ids=app_ids,
|
||||
)
|
||||
|
||||
# Keep only allowed apps
|
||||
res = []
|
||||
for installed_app in filtered_installed_apps:
|
||||
app_id = installed_app["app"].id
|
||||
app_code = app_id_to_app_code[app_id]
|
||||
if permissions.get(app_code):
|
||||
if permissions.get(app_id):
|
||||
res.append(installed_app)
|
||||
|
||||
installed_app_list = res
|
||||
|
||||
@@ -11,7 +11,6 @@ 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
|
||||
|
||||
@@ -57,10 +56,9 @@ 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_code=app_code,
|
||||
app_id=app_id,
|
||||
)
|
||||
if not res:
|
||||
raise AppAccessDeniedError()
|
||||
|
||||
@@ -30,7 +30,6 @@ 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
|
||||
@@ -299,9 +298,6 @@ 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(),
|
||||
@@ -391,8 +387,6 @@ 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(),
|
||||
|
||||
@@ -160,9 +160,8 @@ 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_code)
|
||||
res = EnterpriseService.WebAppAuth.is_user_allowed_to_access_webapp(str(user_id), app_id)
|
||||
return {"result": res}
|
||||
|
||||
@@ -12,6 +12,7 @@ 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
|
||||
@@ -38,7 +39,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:
|
||||
@@ -48,7 +49,7 @@ class PassportResource(Resource):
|
||||
)
|
||||
|
||||
if system_features.webapp_auth.enabled:
|
||||
app_settings = EnterpriseService.WebAppAuth.get_app_access_mode_by_code(app_code=app_code)
|
||||
app_settings = EnterpriseService.WebAppAuth.get_app_access_mode_by_id(app_id=app_id)
|
||||
if not app_settings or not app_settings.access_mode == "public":
|
||||
raise WebAppAuthRequiredError()
|
||||
|
||||
@@ -126,6 +127,8 @@ 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.")
|
||||
|
||||
@@ -169,8 +172,11 @@ def exchange_token_for_existing_web_user(app_code: str, enterprise_user_decoded:
|
||||
)
|
||||
db.session.add(end_user)
|
||||
db.session.commit()
|
||||
exp_dt = datetime.now(UTC) + timedelta(minutes=dify_config.ACCESS_TOKEN_EXPIRE_MINUTES)
|
||||
exp = int(exp_dt.timestamp())
|
||||
|
||||
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)
|
||||
|
||||
payload = {
|
||||
"iss": site.id,
|
||||
"sub": "Web API Passport",
|
||||
|
||||
@@ -13,6 +13,7 @@ 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
|
||||
@@ -37,7 +38,11 @@ def validate_jwt_token(view: Callable[Concatenate[App, EndUser, P], R] | None =
|
||||
|
||||
def decode_jwt_token():
|
||||
system_features = FeatureService.get_system_features()
|
||||
app_code = str(request.headers.get("X-App-Code"))
|
||||
app_code = request.headers.get("X-App-Code")
|
||||
if not app_code:
|
||||
app_code = None
|
||||
else:
|
||||
app_code = str(app_code)
|
||||
try:
|
||||
auth_header = request.headers.get("Authorization")
|
||||
if auth_header is None:
|
||||
@@ -51,15 +56,30 @@ 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)
|
||||
app_code = decoded.get("app_code")
|
||||
# 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_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 app_code or not site:
|
||||
if not site:
|
||||
raise BadRequest("Site URL is no longer valid.")
|
||||
if app_model.enable_site is False:
|
||||
raise BadRequest("Site is disabled.")
|
||||
@@ -72,7 +92,12 @@ def decode_jwt_token():
|
||||
app_web_auth_enabled = False
|
||||
webapp_settings = None
|
||||
if system_features.webapp_auth.enabled:
|
||||
webapp_settings = EnterpriseService.WebAppAuth.get_app_access_mode_by_code(app_code=app_code)
|
||||
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)
|
||||
if not webapp_settings:
|
||||
raise NotFound("Web app settings not found.")
|
||||
app_web_auth_enabled = webapp_settings.access_mode != "public"
|
||||
@@ -87,8 +112,11 @@ 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_code(app_code=str(app_code)).access_mode != "public"
|
||||
EnterpriseService.WebAppAuth.get_app_access_mode_by_id(app_id=app_id).access_mode != "public"
|
||||
)
|
||||
if app_web_auth_enabled:
|
||||
raise WebAppAuthRequiredError()
|
||||
@@ -129,7 +157,10 @@ 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 EnterpriseService.WebAppAuth.is_user_allowed_to_access_webapp(user_id, app_code=app_code):
|
||||
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):
|
||||
raise WebAppAuthAccessDeniedError()
|
||||
|
||||
auth_type = decoded.get("auth_type")
|
||||
|
||||
@@ -1140,6 +1140,15 @@ 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()
|
||||
|
||||
@@ -1173,6 +1182,14 @@ 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.
|
||||
|
||||
@@ -394,8 +394,14 @@ class ApiTool(Tool):
|
||||
parsed_response = self.validate_and_parse_response(response)
|
||||
|
||||
# assemble invoke message based on response type
|
||||
if parsed_response.is_json and isinstance(parsed_response.content, dict):
|
||||
yield self.create_json_message(parsed_response.content)
|
||||
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)
|
||||
else:
|
||||
# Convert to string if needed and create text message
|
||||
text_response = (
|
||||
|
||||
@@ -318,7 +318,13 @@ class ToolNode(BaseNode):
|
||||
json.append(message.message.json_object)
|
||||
elif message.type == ToolInvokeMessage.MessageType.LINK:
|
||||
assert isinstance(message.message, ToolInvokeMessage.TextMessage)
|
||||
stream_text = f"Link: {message.message.text}\n"
|
||||
# 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"
|
||||
text += stream_text
|
||||
yield RunStreamChunkEvent(chunk_content=stream_text, from_variable_selector=[node_id, "text"])
|
||||
elif message.type == ToolInvokeMessage.MessageType.VARIABLE:
|
||||
|
||||
@@ -354,7 +354,7 @@ class Workflow(Base):
|
||||
if not tenant_id:
|
||||
return []
|
||||
|
||||
environment_variables_dict: dict[str, Any] = json.loads(self._environment_variables)
|
||||
environment_variables_dict: dict[str, Any] = json.loads(self._environment_variables or "{}")
|
||||
results = [
|
||||
variable_factory.build_environment_variable_from_mapping(v) for v in environment_variables_dict.values()
|
||||
]
|
||||
|
||||
@@ -46,17 +46,17 @@ class EnterpriseService:
|
||||
|
||||
class WebAppAuth:
|
||||
@classmethod
|
||||
def is_user_allowed_to_access_webapp(cls, user_id: str, app_code: str):
|
||||
params = {"userId": user_id, "appCode": app_code}
|
||||
def is_user_allowed_to_access_webapp(cls, user_id: str, app_id: str):
|
||||
params = {"userId": user_id, "appId": app_id}
|
||||
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_codes: list[str]):
|
||||
if not app_codes:
|
||||
def batch_is_user_allowed_to_access_webapps(cls, user_id: str, app_ids: list[str]):
|
||||
if not app_ids:
|
||||
return {}
|
||||
body = {"userId": user_id, "appCodes": app_codes}
|
||||
body = {"userId": user_id, "appIds": app_ids}
|
||||
data = EnterpriseRequest.send_request("POST", "/webapp/permission/batch", json=body)
|
||||
if not data:
|
||||
raise ValueError("No data found.")
|
||||
@@ -92,16 +92,6 @@ 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:
|
||||
|
||||
@@ -172,7 +172,8 @@ class WebAppAuthService:
|
||||
return WebAppAuthType.EXTERNAL
|
||||
|
||||
if app_code:
|
||||
webapp_settings = EnterpriseService.WebAppAuth.get_app_access_mode_by_code(app_code)
|
||||
app_id = AppService.get_app_id_by_code(app_code)
|
||||
webapp_settings = EnterpriseService.WebAppAuth.get_app_access_mode_by_id(app_id)
|
||||
return cls.get_app_auth_type(access_mode=webapp_settings.access_mode)
|
||||
|
||||
raise ValueError("Could not determine app authentication type.")
|
||||
|
||||
@@ -452,7 +452,8 @@ class WorkflowService:
|
||||
)
|
||||
|
||||
if not default_provider:
|
||||
raise ValueError("No default credential found")
|
||||
# plugin does not require credentials, skip
|
||||
return
|
||||
|
||||
# Check credential policy compliance using the default credential ID
|
||||
from core.helper.credential_utils import check_credential_policy_compliance
|
||||
|
||||
@@ -42,7 +42,8 @@
|
||||
font-family: Inter;
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
line-height: 120%; /* 28.8px */
|
||||
line-height: 120%;
|
||||
/* 28.8px */
|
||||
}
|
||||
|
||||
.description {
|
||||
@@ -51,7 +52,8 @@
|
||||
font-family: Inter;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 20px; /* 142.857% */
|
||||
line-height: 20px;
|
||||
/* 142.857% */
|
||||
letter-spacing: -0.07px;
|
||||
}
|
||||
|
||||
@@ -96,7 +98,8 @@
|
||||
font-family: Inter;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 20px; /* 142.857% */
|
||||
line-height: 20px;
|
||||
/* 142.857% */
|
||||
letter-spacing: -0.07px;
|
||||
}
|
||||
</style>
|
||||
@@ -107,7 +110,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 Dify account.</p>
|
||||
<p class="content1">You're updating the email address linked to your 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>
|
||||
@@ -118,5 +121,4 @@
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
||||
</html>
|
||||
@@ -42,7 +42,8 @@
|
||||
font-family: Inter;
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
line-height: 120%; /* 28.8px */
|
||||
line-height: 120%;
|
||||
/* 28.8px */
|
||||
}
|
||||
|
||||
.description {
|
||||
@@ -51,7 +52,8 @@
|
||||
font-family: Inter;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 20px; /* 142.857% */
|
||||
line-height: 20px;
|
||||
/* 142.857% */
|
||||
letter-spacing: -0.07px;
|
||||
}
|
||||
|
||||
@@ -96,7 +98,8 @@
|
||||
font-family: Inter;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 20px; /* 142.857% */
|
||||
line-height: 20px;
|
||||
/* 142.857% */
|
||||
letter-spacing: -0.07px;
|
||||
}
|
||||
</style>
|
||||
@@ -107,7 +110,7 @@
|
||||
<div class="header"></div>
|
||||
<p class="title">确认您的邮箱地址变更</p>
|
||||
<div class="description">
|
||||
<p class="content1">您正在更新与您的 Dify 账户关联的邮箱地址。</p>
|
||||
<p class="content1">您正在更新与您的账户关联的邮箱地址。</p>
|
||||
<p class="content2">为了确认此操作,请使用以下验证码。</p>
|
||||
<p class="content3">此验证码仅在接下来的5分钟内有效:</p>
|
||||
</div>
|
||||
@@ -118,5 +121,4 @@
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
||||
</html>
|
||||
@@ -42,7 +42,8 @@
|
||||
font-family: Inter;
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
line-height: 120%; /* 28.8px */
|
||||
line-height: 120%;
|
||||
/* 28.8px */
|
||||
}
|
||||
|
||||
.description {
|
||||
@@ -51,7 +52,8 @@
|
||||
font-family: Inter;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 20px; /* 142.857% */
|
||||
line-height: 20px;
|
||||
/* 142.857% */
|
||||
letter-spacing: -0.07px;
|
||||
}
|
||||
|
||||
@@ -96,7 +98,8 @@
|
||||
font-family: Inter;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 20px; /* 142.857% */
|
||||
line-height: 20px;
|
||||
/* 142.857% */
|
||||
letter-spacing: -0.07px;
|
||||
}
|
||||
</style>
|
||||
@@ -107,7 +110,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 Dify account.</p>
|
||||
<p class="content1">We received a request to change the email address associated with your 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>
|
||||
@@ -118,5 +121,4 @@
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
||||
</html>
|
||||
@@ -42,7 +42,8 @@
|
||||
font-family: Inter;
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
line-height: 120%; /* 28.8px */
|
||||
line-height: 120%;
|
||||
/* 28.8px */
|
||||
}
|
||||
|
||||
.description {
|
||||
@@ -51,7 +52,8 @@
|
||||
font-family: Inter;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 20px; /* 142.857% */
|
||||
line-height: 20px;
|
||||
/* 142.857% */
|
||||
letter-spacing: -0.07px;
|
||||
}
|
||||
|
||||
@@ -96,7 +98,8 @@
|
||||
font-family: Inter;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 20px; /* 142.857% */
|
||||
line-height: 20px;
|
||||
/* 142.857% */
|
||||
letter-spacing: -0.07px;
|
||||
}
|
||||
</style>
|
||||
@@ -107,7 +110,7 @@
|
||||
<div class="header"></div>
|
||||
<p class="title">验证您的邮箱变更请求</p>
|
||||
<div class="description">
|
||||
<p class="content1">我们收到了一个变更您 Dify 账户关联邮箱地址的请求。</p>
|
||||
<p class="content1">我们收到了一个变更您账户关联邮箱地址的请求。</p>
|
||||
<p class="content3">此验证码仅在接下来的5分钟内有效:</p>
|
||||
</div>
|
||||
<div class="code-content">
|
||||
@@ -117,5 +120,4 @@
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
||||
</html>
|
||||
@@ -1,5 +1,6 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<style>
|
||||
body {
|
||||
@@ -10,6 +11,7 @@
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.container {
|
||||
width: 504px;
|
||||
min-height: 444px;
|
||||
@@ -30,6 +32,7 @@
|
||||
max-width: 63px;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.button {
|
||||
display: block;
|
||||
padding: 8px 12px;
|
||||
@@ -45,49 +48,56 @@
|
||||
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">
|
||||
<!-- Optional: Add a logo or a header image here -->
|
||||
<img src="https://assets.dify.ai/images/logo.png" alt="Dify Logo">
|
||||
</div>
|
||||
<div class="header"></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,7 +42,8 @@
|
||||
font-family: Inter;
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
line-height: 120%; /* 28.8px */
|
||||
line-height: 120%;
|
||||
/* 28.8px */
|
||||
}
|
||||
|
||||
.description {
|
||||
@@ -51,7 +52,8 @@
|
||||
font-family: Inter;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 20px; /* 142.857% */
|
||||
line-height: 20px;
|
||||
/* 142.857% */
|
||||
letter-spacing: -0.07px;
|
||||
}
|
||||
|
||||
@@ -80,10 +82,9 @@
|
||||
<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@dify.ai.</p>
|
||||
<p class="content3">If you have any questions, please contact support.</p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
||||
</html>
|
||||
@@ -42,7 +42,8 @@
|
||||
font-family: Inter;
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
line-height: 120%; /* 28.8px */
|
||||
line-height: 120%;
|
||||
/* 28.8px */
|
||||
}
|
||||
|
||||
.description {
|
||||
@@ -51,7 +52,8 @@
|
||||
font-family: Inter;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 20px; /* 142.857% */
|
||||
line-height: 20px;
|
||||
/* 142.857% */
|
||||
letter-spacing: -0.07px;
|
||||
}
|
||||
|
||||
@@ -80,10 +82,9 @@
|
||||
<div class="description">
|
||||
<p class="content1">您已被分配为工作空间“{{WorkspaceName}}”的新所有者。</p>
|
||||
<p class="content2">作为新所有者,您现在对该工作空间拥有完全的管理权限。</p>
|
||||
<p class="content3">如果您有任何问题,请联系support@dify.ai。</p>
|
||||
<p class="content3">如果您有任何问题,请联系支持团队。</p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
||||
</html>
|
||||
@@ -42,7 +42,8 @@
|
||||
font-family: Inter;
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
line-height: 120%; /* 28.8px */
|
||||
line-height: 120%;
|
||||
/* 28.8px */
|
||||
}
|
||||
|
||||
.description {
|
||||
@@ -51,7 +52,8 @@
|
||||
font-family: Inter;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 20px; /* 142.857% */
|
||||
line-height: 20px;
|
||||
/* 142.857% */
|
||||
letter-spacing: -0.07px;
|
||||
}
|
||||
|
||||
@@ -97,7 +99,8 @@
|
||||
font-family: Inter;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 20px; /* 142.857% */
|
||||
line-height: 20px;
|
||||
/* 142.857% */
|
||||
letter-spacing: -0.07px;
|
||||
}
|
||||
</style>
|
||||
@@ -108,12 +111,14 @@
|
||||
<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@dify.ai 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 immediately.</p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
||||
</html>
|
||||
@@ -42,7 +42,8 @@
|
||||
font-family: Inter;
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
line-height: 120%; /* 28.8px */
|
||||
line-height: 120%;
|
||||
/* 28.8px */
|
||||
}
|
||||
|
||||
.description {
|
||||
@@ -51,7 +52,8 @@
|
||||
font-family: Inter;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 20px; /* 142.857% */
|
||||
line-height: 20px;
|
||||
/* 142.857% */
|
||||
letter-spacing: -0.07px;
|
||||
}
|
||||
|
||||
@@ -97,7 +99,8 @@
|
||||
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,10 +113,9 @@
|
||||
<div class="description">
|
||||
<p class="content1">您已成功将工作空间“{{WorkspaceName}}”的所有权转移给{{NewOwnerEmail}}。</p>
|
||||
<p class="content2">您不再拥有此工作空间的拥有者权限。您的访问级别已更改为管理员。</p>
|
||||
<p class="content3">如果您没有发起此转移或对此变更有任何疑问,请立即联系support@dify.ai。</p>
|
||||
<p class="content3">如果您没有发起此转移或对此变更有任何疑问,请立即联系支持团队。</p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
||||
</html>
|
||||
@@ -1,12 +1,14 @@
|
||||
"""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
|
||||
@@ -99,3 +101,106 @@ 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,9 +35,7 @@ class TestWebAppAuthService:
|
||||
mock_enterprise_service.WebAppAuth.get_app_access_mode_by_id.return_value = type(
|
||||
"MockWebAppAuth", (), {"access_mode": "private"}
|
||||
)()
|
||||
mock_enterprise_service.WebAppAuth.get_app_access_mode_by_code.return_value = type(
|
||||
"MockWebAppAuth", (), {"access_mode": "private"}
|
||||
)()
|
||||
# Note: get_app_access_mode_by_code method was removed in refactoring
|
||||
|
||||
yield {
|
||||
"passport_service": mock_passport_service,
|
||||
@@ -866,7 +864,7 @@ class TestWebAppAuthService:
|
||||
mock_webapp_auth = type("MockWebAppAuth", (), {"access_mode": "sso_verified"})()
|
||||
mock_external_service_dependencies[
|
||||
"enterprise_service"
|
||||
].WebAppAuth.get_app_access_mode_by_code.return_value = mock_webapp_auth
|
||||
].WebAppAuth.get_app_access_mode_by_id.return_value = mock_webapp_auth
|
||||
|
||||
# Act: Execute authentication type determination
|
||||
result = WebAppAuthService.get_app_auth_type(app_code="mock_app_code")
|
||||
@@ -877,7 +875,7 @@ class TestWebAppAuthService:
|
||||
# Verify mock service was called correctly
|
||||
mock_external_service_dependencies[
|
||||
"enterprise_service"
|
||||
].WebAppAuth.get_app_access_mode_by_code.assert_called_once_with("mock_app_code")
|
||||
].WebAppAuth.get_app_access_mode_by_id.assert_called_once_with("mock_app_id")
|
||||
|
||||
def test_get_app_auth_type_no_parameters(self, db_session_with_containers, mock_external_service_dependencies):
|
||||
"""
|
||||
|
||||
249
api/tests/unit_tests/tools/test_api_tool.py
Normal file
249
api/tests/unit_tests/tools/test_api_tool.py
Normal file
@@ -0,0 +1,249 @@
|
||||
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,7 +100,10 @@ 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,12 +17,9 @@ import type {
|
||||
import { noop } from 'lodash-es'
|
||||
|
||||
export type EmbeddedChatbotContextValue = {
|
||||
userCanAccess?: boolean
|
||||
appInfoError?: any
|
||||
appInfoLoading?: boolean
|
||||
appMeta?: AppMeta
|
||||
appData?: AppData
|
||||
appParams?: ChatConfig
|
||||
appMeta: AppMeta | null
|
||||
appData: AppData | null
|
||||
appParams: ChatConfig | null
|
||||
appChatListDataLoading?: boolean
|
||||
currentConversationId: string
|
||||
currentConversationItem?: ConversationItem
|
||||
@@ -59,7 +56,10 @@ export type EmbeddedChatbotContextValue = {
|
||||
}
|
||||
|
||||
export const EmbeddedChatbotContext = createContext<EmbeddedChatbotContextValue>({
|
||||
userCanAccess: false,
|
||||
appData: null,
|
||||
appMeta: null,
|
||||
appParams: null,
|
||||
appChatListDataLoading: false,
|
||||
currentConversationId: '',
|
||||
appPrevChatList: [],
|
||||
pinnedConversationList: [],
|
||||
|
||||
@@ -18,9 +18,6 @@ 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,
|
||||
@@ -36,8 +33,7 @@ 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 { useGetUserCanAccessApp } from '@/service/access-control'
|
||||
import { useGlobalPublicStore } from '@/context/global-public-context'
|
||||
import { useWebAppStore } from '@/context/web-app-context'
|
||||
|
||||
function getFormattedChatList(messages: any[]) {
|
||||
const newChatList: ChatItem[] = []
|
||||
@@ -67,18 +63,10 @@ function getFormattedChatList(messages: any[]) {
|
||||
|
||||
export const useEmbeddedChatbot = () => {
|
||||
const isInstalledApp = false
|
||||
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 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 [userId, setUserId] = useState<string>()
|
||||
const [conversationId, setConversationId] = useState<string>()
|
||||
@@ -145,8 +133,6 @@ 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))
|
||||
@@ -395,16 +381,13 @@ 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,
|
||||
appData: appInfo,
|
||||
appParams: appParams || {} as ChatConfig,
|
||||
appMeta,
|
||||
appPinnedConversationData,
|
||||
|
||||
@@ -101,7 +101,6 @@ const EmbeddedChatbotWrapper = () => {
|
||||
|
||||
const {
|
||||
appData,
|
||||
userCanAccess,
|
||||
appParams,
|
||||
appMeta,
|
||||
appChatListDataLoading,
|
||||
@@ -135,7 +134,6 @@ const EmbeddedChatbotWrapper = () => {
|
||||
} = useEmbeddedChatbot()
|
||||
|
||||
return <EmbeddedChatbotContext.Provider value={{
|
||||
userCanAccess,
|
||||
appData,
|
||||
appParams,
|
||||
appMeta,
|
||||
|
||||
@@ -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">{t('login.pageTitle')}</h2>
|
||||
{!systemFeatures.branding.enabled && <p className='body-md-regular mt-2 text-text-tertiary'>{t('login.welcome')}</p>}
|
||||
<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>
|
||||
</div>}
|
||||
<div className="relative">
|
||||
<div className="mt-6 flex flex-col gap-3">
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
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,5 +1,6 @@
|
||||
const translation = {
|
||||
pageTitle: 'Dify にログイン',
|
||||
pageTitleForE: 'はじめましょう!',
|
||||
welcome: '👋 ようこそ!まずはログインしてご利用ください。',
|
||||
email: 'メールアドレス',
|
||||
emailPlaceholder: 'メールアドレスを入力してください',
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
const translation = {
|
||||
pageTitle: '登录 Dify',
|
||||
pageTitleForE: '嗨,近来可好',
|
||||
welcome: '👋 欢迎!请登录以开始使用。',
|
||||
email: '邮箱',
|
||||
emailPlaceholder: '输入邮箱地址',
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
const translation = {
|
||||
pageTitle: '嗨,近來可好',
|
||||
pageTitleForE: '嗨,近來可好',
|
||||
welcome: '👋 歡迎來到 Dify, 登入以繼續',
|
||||
email: '郵箱',
|
||||
emailPlaceholder: '輸入郵箱地址',
|
||||
|
||||
Reference in New Issue
Block a user