mirror of
https://github.com/langgenius/dify.git
synced 2026-01-04 21:47:22 +00:00
Compare commits
17 Commits
revert-256
...
release/e-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
98ecc0a4eb | ||
|
|
780f969121 | ||
|
|
338e0f74b9 | ||
|
|
cad6db5a1d | ||
|
|
2e9b3b8d44 | ||
|
|
5a80f5158f | ||
|
|
5a92e0feee | ||
|
|
4b5196f402 | ||
|
|
7b64569c8c | ||
|
|
6106207039 | ||
|
|
3b4e9b64af | ||
|
|
5073ce6e22 | ||
|
|
1277a57641 | ||
|
|
d3ac5b1dd8 | ||
|
|
39a0b89b9a | ||
|
|
7dadb33003 | ||
|
|
b5a7e64e19 |
@@ -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")
|
||||
|
||||
@@ -4,13 +4,13 @@ from collections.abc import Sequence
|
||||
from typing import cast
|
||||
|
||||
from flask import abort, request
|
||||
from flask_restx import Resource, inputs, marshal_with, reqparse
|
||||
from flask_restx import Resource, fields, inputs, marshal_with, reqparse
|
||||
from sqlalchemy.orm import Session
|
||||
from werkzeug.exceptions import Forbidden, InternalServerError, NotFound
|
||||
|
||||
import services
|
||||
from configs import dify_config
|
||||
from controllers.console import api
|
||||
from controllers.console import api, console_ns
|
||||
from controllers.console.app.error import ConversationCompletedError, DraftWorkflowNotExist, DraftWorkflowNotSync
|
||||
from controllers.console.app.wraps import get_app_model
|
||||
from controllers.console.wraps import account_initialization_required, setup_required
|
||||
@@ -57,7 +57,13 @@ def _parse_file(workflow: Workflow, files: list[dict] | None = None) -> Sequence
|
||||
return file_objs
|
||||
|
||||
|
||||
@console_ns.route("/apps/<uuid:app_id>/workflows/draft")
|
||||
class DraftWorkflowApi(Resource):
|
||||
@api.doc("get_draft_workflow")
|
||||
@api.doc(description="Get draft workflow for an application")
|
||||
@api.doc(params={"app_id": "Application ID"})
|
||||
@api.response(200, "Draft workflow retrieved successfully", workflow_fields)
|
||||
@api.response(404, "Draft workflow not found")
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@@ -86,6 +92,23 @@ class DraftWorkflowApi(Resource):
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@get_app_model(mode=[AppMode.ADVANCED_CHAT, AppMode.WORKFLOW])
|
||||
@api.doc("sync_draft_workflow")
|
||||
@api.doc(description="Sync draft workflow configuration")
|
||||
@api.expect(
|
||||
api.model(
|
||||
"SyncDraftWorkflowRequest",
|
||||
{
|
||||
"graph": fields.Raw(required=True, description="Workflow graph configuration"),
|
||||
"features": fields.Raw(required=True, description="Workflow features configuration"),
|
||||
"hash": fields.String(description="Workflow hash for validation"),
|
||||
"environment_variables": fields.List(fields.Raw, required=True, description="Environment variables"),
|
||||
"conversation_variables": fields.List(fields.Raw, description="Conversation variables"),
|
||||
},
|
||||
)
|
||||
)
|
||||
@api.response(200, "Draft workflow synced successfully", workflow_fields)
|
||||
@api.response(400, "Invalid workflow configuration")
|
||||
@api.response(403, "Permission denied")
|
||||
def post(self, app_model: App):
|
||||
"""
|
||||
Sync draft workflow
|
||||
@@ -159,7 +182,25 @@ class DraftWorkflowApi(Resource):
|
||||
}
|
||||
|
||||
|
||||
@console_ns.route("/apps/<uuid:app_id>/advanced-chat/workflows/draft/run")
|
||||
class AdvancedChatDraftWorkflowRunApi(Resource):
|
||||
@api.doc("run_advanced_chat_draft_workflow")
|
||||
@api.doc(description="Run draft workflow for advanced chat application")
|
||||
@api.doc(params={"app_id": "Application ID"})
|
||||
@api.expect(
|
||||
api.model(
|
||||
"AdvancedChatWorkflowRunRequest",
|
||||
{
|
||||
"query": fields.String(required=True, description="User query"),
|
||||
"inputs": fields.Raw(description="Input variables"),
|
||||
"files": fields.List(fields.Raw, description="File uploads"),
|
||||
"conversation_id": fields.String(description="Conversation ID"),
|
||||
},
|
||||
)
|
||||
)
|
||||
@api.response(200, "Workflow run started successfully")
|
||||
@api.response(400, "Invalid request parameters")
|
||||
@api.response(403, "Permission denied")
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@@ -208,7 +249,23 @@ class AdvancedChatDraftWorkflowRunApi(Resource):
|
||||
raise InternalServerError()
|
||||
|
||||
|
||||
@console_ns.route("/apps/<uuid:app_id>/advanced-chat/workflows/draft/iteration/nodes/<string:node_id>/run")
|
||||
class AdvancedChatDraftRunIterationNodeApi(Resource):
|
||||
@api.doc("run_advanced_chat_draft_iteration_node")
|
||||
@api.doc(description="Run draft workflow iteration node for advanced chat")
|
||||
@api.doc(params={"app_id": "Application ID", "node_id": "Node ID"})
|
||||
@api.expect(
|
||||
api.model(
|
||||
"IterationNodeRunRequest",
|
||||
{
|
||||
"task_id": fields.String(required=True, description="Task ID"),
|
||||
"inputs": fields.Raw(description="Input variables"),
|
||||
},
|
||||
)
|
||||
)
|
||||
@api.response(200, "Iteration node run started successfully")
|
||||
@api.response(403, "Permission denied")
|
||||
@api.response(404, "Node not found")
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@@ -244,7 +301,23 @@ class AdvancedChatDraftRunIterationNodeApi(Resource):
|
||||
raise InternalServerError()
|
||||
|
||||
|
||||
@console_ns.route("/apps/<uuid:app_id>/workflows/draft/iteration/nodes/<string:node_id>/run")
|
||||
class WorkflowDraftRunIterationNodeApi(Resource):
|
||||
@api.doc("run_workflow_draft_iteration_node")
|
||||
@api.doc(description="Run draft workflow iteration node")
|
||||
@api.doc(params={"app_id": "Application ID", "node_id": "Node ID"})
|
||||
@api.expect(
|
||||
api.model(
|
||||
"WorkflowIterationNodeRunRequest",
|
||||
{
|
||||
"task_id": fields.String(required=True, description="Task ID"),
|
||||
"inputs": fields.Raw(description="Input variables"),
|
||||
},
|
||||
)
|
||||
)
|
||||
@api.response(200, "Workflow iteration node run started successfully")
|
||||
@api.response(403, "Permission denied")
|
||||
@api.response(404, "Node not found")
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@@ -280,7 +353,23 @@ class WorkflowDraftRunIterationNodeApi(Resource):
|
||||
raise InternalServerError()
|
||||
|
||||
|
||||
@console_ns.route("/apps/<uuid:app_id>/advanced-chat/workflows/draft/loop/nodes/<string:node_id>/run")
|
||||
class AdvancedChatDraftRunLoopNodeApi(Resource):
|
||||
@api.doc("run_advanced_chat_draft_loop_node")
|
||||
@api.doc(description="Run draft workflow loop node for advanced chat")
|
||||
@api.doc(params={"app_id": "Application ID", "node_id": "Node ID"})
|
||||
@api.expect(
|
||||
api.model(
|
||||
"LoopNodeRunRequest",
|
||||
{
|
||||
"task_id": fields.String(required=True, description="Task ID"),
|
||||
"inputs": fields.Raw(description="Input variables"),
|
||||
},
|
||||
)
|
||||
)
|
||||
@api.response(200, "Loop node run started successfully")
|
||||
@api.response(403, "Permission denied")
|
||||
@api.response(404, "Node not found")
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@@ -317,7 +406,23 @@ class AdvancedChatDraftRunLoopNodeApi(Resource):
|
||||
raise InternalServerError()
|
||||
|
||||
|
||||
@console_ns.route("/apps/<uuid:app_id>/workflows/draft/loop/nodes/<string:node_id>/run")
|
||||
class WorkflowDraftRunLoopNodeApi(Resource):
|
||||
@api.doc("run_workflow_draft_loop_node")
|
||||
@api.doc(description="Run draft workflow loop node")
|
||||
@api.doc(params={"app_id": "Application ID", "node_id": "Node ID"})
|
||||
@api.expect(
|
||||
api.model(
|
||||
"WorkflowLoopNodeRunRequest",
|
||||
{
|
||||
"task_id": fields.String(required=True, description="Task ID"),
|
||||
"inputs": fields.Raw(description="Input variables"),
|
||||
},
|
||||
)
|
||||
)
|
||||
@api.response(200, "Workflow loop node run started successfully")
|
||||
@api.response(403, "Permission denied")
|
||||
@api.response(404, "Node not found")
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@@ -354,7 +459,22 @@ class WorkflowDraftRunLoopNodeApi(Resource):
|
||||
raise InternalServerError()
|
||||
|
||||
|
||||
@console_ns.route("/apps/<uuid:app_id>/workflows/draft/run")
|
||||
class DraftWorkflowRunApi(Resource):
|
||||
@api.doc("run_draft_workflow")
|
||||
@api.doc(description="Run draft workflow")
|
||||
@api.doc(params={"app_id": "Application ID"})
|
||||
@api.expect(
|
||||
api.model(
|
||||
"DraftWorkflowRunRequest",
|
||||
{
|
||||
"inputs": fields.Raw(required=True, description="Input variables"),
|
||||
"files": fields.List(fields.Raw, description="File uploads"),
|
||||
},
|
||||
)
|
||||
)
|
||||
@api.response(200, "Draft workflow run started successfully")
|
||||
@api.response(403, "Permission denied")
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@@ -393,7 +513,14 @@ class DraftWorkflowRunApi(Resource):
|
||||
raise InvokeRateLimitHttpError(ex.description)
|
||||
|
||||
|
||||
@console_ns.route("/apps/<uuid:app_id>/workflow-runs/tasks/<string:task_id>/stop")
|
||||
class WorkflowTaskStopApi(Resource):
|
||||
@api.doc("stop_workflow_task")
|
||||
@api.doc(description="Stop running workflow task")
|
||||
@api.doc(params={"app_id": "Application ID", "task_id": "Task ID"})
|
||||
@api.response(200, "Task stopped successfully")
|
||||
@api.response(404, "Task not found")
|
||||
@api.response(403, "Permission denied")
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@@ -414,7 +541,22 @@ class WorkflowTaskStopApi(Resource):
|
||||
return {"result": "success"}
|
||||
|
||||
|
||||
@console_ns.route("/apps/<uuid:app_id>/workflows/draft/nodes/<string:node_id>/run")
|
||||
class DraftWorkflowNodeRunApi(Resource):
|
||||
@api.doc("run_draft_workflow_node")
|
||||
@api.doc(description="Run draft workflow node")
|
||||
@api.doc(params={"app_id": "Application ID", "node_id": "Node ID"})
|
||||
@api.expect(
|
||||
api.model(
|
||||
"DraftWorkflowNodeRunRequest",
|
||||
{
|
||||
"inputs": fields.Raw(description="Input variables"),
|
||||
},
|
||||
)
|
||||
)
|
||||
@api.response(200, "Node run started successfully", workflow_run_node_execution_fields)
|
||||
@api.response(403, "Permission denied")
|
||||
@api.response(404, "Node not found")
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@@ -462,7 +604,13 @@ class DraftWorkflowNodeRunApi(Resource):
|
||||
return workflow_node_execution
|
||||
|
||||
|
||||
@console_ns.route("/apps/<uuid:app_id>/workflows/publish")
|
||||
class PublishedWorkflowApi(Resource):
|
||||
@api.doc("get_published_workflow")
|
||||
@api.doc(description="Get published workflow for an application")
|
||||
@api.doc(params={"app_id": "Application ID"})
|
||||
@api.response(200, "Published workflow retrieved successfully", workflow_fields)
|
||||
@api.response(404, "Published workflow not found")
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@@ -534,7 +682,12 @@ class PublishedWorkflowApi(Resource):
|
||||
}
|
||||
|
||||
|
||||
@console_ns.route("/apps/<uuid:app_id>/workflows/default-workflow-block-configs")
|
||||
class DefaultBlockConfigsApi(Resource):
|
||||
@api.doc("get_default_block_configs")
|
||||
@api.doc(description="Get default block configurations for workflow")
|
||||
@api.doc(params={"app_id": "Application ID"})
|
||||
@api.response(200, "Default block configurations retrieved successfully")
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@@ -555,7 +708,13 @@ 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>")
|
||||
class DefaultBlockConfigApi(Resource):
|
||||
@api.doc("get_default_block_config")
|
||||
@api.doc(description="Get default block configuration by type")
|
||||
@api.doc(params={"app_id": "Application ID", "block_type": "Block type"})
|
||||
@api.response(200, "Default block configuration retrieved successfully")
|
||||
@api.response(404, "Block type not found")
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@@ -588,7 +747,14 @@ class DefaultBlockConfigApi(Resource):
|
||||
return workflow_service.get_default_block_config(node_type=block_type, filters=filters)
|
||||
|
||||
|
||||
@console_ns.route("/apps/<uuid:app_id>/convert-to-workflow")
|
||||
class ConvertToWorkflowApi(Resource):
|
||||
@api.doc("convert_to_workflow")
|
||||
@api.doc(description="Convert application to workflow mode")
|
||||
@api.doc(params={"app_id": "Application ID"})
|
||||
@api.response(200, "Application converted to workflow successfully")
|
||||
@api.response(400, "Application cannot be converted")
|
||||
@api.response(403, "Permission denied")
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@@ -625,9 +791,14 @@ class ConvertToWorkflowApi(Resource):
|
||||
}
|
||||
|
||||
|
||||
@console_ns.route("/apps/<uuid:app_id>/workflows/draft/config")
|
||||
class WorkflowConfigApi(Resource):
|
||||
"""Resource for workflow configuration."""
|
||||
|
||||
@api.doc("get_workflow_config")
|
||||
@api.doc(description="Get workflow configuration")
|
||||
@api.doc(params={"app_id": "Application ID"})
|
||||
@api.response(200, "Workflow configuration retrieved successfully")
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@@ -638,7 +809,12 @@ class WorkflowConfigApi(Resource):
|
||||
}
|
||||
|
||||
|
||||
@console_ns.route("/apps/<uuid:app_id>/workflows")
|
||||
class PublishedAllWorkflowApi(Resource):
|
||||
@api.doc("get_all_published_workflows")
|
||||
@api.doc(description="Get all published workflows for an application")
|
||||
@api.doc(params={"app_id": "Application ID"})
|
||||
@api.response(200, "Published workflows retrieved successfully", workflow_pagination_fields)
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@@ -689,7 +865,23 @@ class PublishedAllWorkflowApi(Resource):
|
||||
}
|
||||
|
||||
|
||||
@console_ns.route("/apps/<uuid:app_id>/workflows/<string:workflow_id>")
|
||||
class WorkflowByIdApi(Resource):
|
||||
@api.doc("update_workflow_by_id")
|
||||
@api.doc(description="Update workflow by ID")
|
||||
@api.doc(params={"app_id": "Application ID", "workflow_id": "Workflow ID"})
|
||||
@api.expect(
|
||||
api.model(
|
||||
"UpdateWorkflowRequest",
|
||||
{
|
||||
"environment_variables": fields.List(fields.Raw, description="Environment variables"),
|
||||
"conversation_variables": fields.List(fields.Raw, description="Conversation variables"),
|
||||
},
|
||||
)
|
||||
)
|
||||
@api.response(200, "Workflow updated successfully", workflow_fields)
|
||||
@api.response(404, "Workflow not found")
|
||||
@api.response(403, "Permission denied")
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@@ -780,7 +972,14 @@ class WorkflowByIdApi(Resource):
|
||||
return None, 204
|
||||
|
||||
|
||||
@console_ns.route("/apps/<uuid:app_id>/workflows/draft/nodes/<string:node_id>/last-run")
|
||||
class DraftWorkflowNodeLastRunApi(Resource):
|
||||
@api.doc("get_draft_workflow_node_last_run")
|
||||
@api.doc(description="Get last run result for draft workflow node")
|
||||
@api.doc(params={"app_id": "Application ID", "node_id": "Node ID"})
|
||||
@api.response(200, "Node last run retrieved successfully", workflow_run_node_execution_fields)
|
||||
@api.response(404, "Node last run not found")
|
||||
@api.response(403, "Permission denied")
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@@ -799,73 +998,3 @@ class DraftWorkflowNodeLastRunApi(Resource):
|
||||
if node_exec is None:
|
||||
raise NotFound("last run not found")
|
||||
return node_exec
|
||||
|
||||
|
||||
api.add_resource(
|
||||
DraftWorkflowApi,
|
||||
"/apps/<uuid:app_id>/workflows/draft",
|
||||
)
|
||||
api.add_resource(
|
||||
WorkflowConfigApi,
|
||||
"/apps/<uuid:app_id>/workflows/draft/config",
|
||||
)
|
||||
api.add_resource(
|
||||
AdvancedChatDraftWorkflowRunApi,
|
||||
"/apps/<uuid:app_id>/advanced-chat/workflows/draft/run",
|
||||
)
|
||||
api.add_resource(
|
||||
DraftWorkflowRunApi,
|
||||
"/apps/<uuid:app_id>/workflows/draft/run",
|
||||
)
|
||||
api.add_resource(
|
||||
WorkflowTaskStopApi,
|
||||
"/apps/<uuid:app_id>/workflow-runs/tasks/<string:task_id>/stop",
|
||||
)
|
||||
api.add_resource(
|
||||
DraftWorkflowNodeRunApi,
|
||||
"/apps/<uuid:app_id>/workflows/draft/nodes/<string:node_id>/run",
|
||||
)
|
||||
api.add_resource(
|
||||
AdvancedChatDraftRunIterationNodeApi,
|
||||
"/apps/<uuid:app_id>/advanced-chat/workflows/draft/iteration/nodes/<string:node_id>/run",
|
||||
)
|
||||
api.add_resource(
|
||||
WorkflowDraftRunIterationNodeApi,
|
||||
"/apps/<uuid:app_id>/workflows/draft/iteration/nodes/<string:node_id>/run",
|
||||
)
|
||||
api.add_resource(
|
||||
AdvancedChatDraftRunLoopNodeApi,
|
||||
"/apps/<uuid:app_id>/advanced-chat/workflows/draft/loop/nodes/<string:node_id>/run",
|
||||
)
|
||||
api.add_resource(
|
||||
WorkflowDraftRunLoopNodeApi,
|
||||
"/apps/<uuid:app_id>/workflows/draft/loop/nodes/<string:node_id>/run",
|
||||
)
|
||||
api.add_resource(
|
||||
PublishedWorkflowApi,
|
||||
"/apps/<uuid:app_id>/workflows/publish",
|
||||
)
|
||||
api.add_resource(
|
||||
PublishedAllWorkflowApi,
|
||||
"/apps/<uuid:app_id>/workflows",
|
||||
)
|
||||
api.add_resource(
|
||||
DefaultBlockConfigsApi,
|
||||
"/apps/<uuid:app_id>/workflows/default-workflow-block-configs",
|
||||
)
|
||||
api.add_resource(
|
||||
DefaultBlockConfigApi,
|
||||
"/apps/<uuid:app_id>/workflows/default-workflow-block-configs/<string:block_type>",
|
||||
)
|
||||
api.add_resource(
|
||||
ConvertToWorkflowApi,
|
||||
"/apps/<uuid:app_id>/convert-to-workflow",
|
||||
)
|
||||
api.add_resource(
|
||||
WorkflowByIdApi,
|
||||
"/apps/<uuid:app_id>/workflows/<string:workflow_id>",
|
||||
)
|
||||
api.add_resource(
|
||||
DraftWorkflowNodeLastRunApi,
|
||||
"/apps/<uuid:app_id>/workflows/draft/nodes/<string:node_id>/last-run",
|
||||
)
|
||||
|
||||
@@ -3,7 +3,7 @@ from flask_restx import Resource, marshal_with, reqparse
|
||||
from flask_restx.inputs import int_range
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from controllers.console import api
|
||||
from controllers.console import api, console_ns
|
||||
from controllers.console.app.wraps import get_app_model
|
||||
from controllers.console.wraps import account_initialization_required, setup_required
|
||||
from core.workflow.entities.workflow_execution import WorkflowExecutionStatus
|
||||
@@ -15,7 +15,24 @@ from models.model import AppMode
|
||||
from services.workflow_app_service import WorkflowAppService
|
||||
|
||||
|
||||
@console_ns.route("/apps/<uuid:app_id>/workflow-app-logs")
|
||||
class WorkflowAppLogApi(Resource):
|
||||
@api.doc("get_workflow_app_logs")
|
||||
@api.doc(description="Get workflow application execution logs")
|
||||
@api.doc(params={"app_id": "Application ID"})
|
||||
@api.doc(
|
||||
params={
|
||||
"keyword": "Search keyword for filtering logs",
|
||||
"status": "Filter by execution status (succeeded, failed, stopped, partial-succeeded)",
|
||||
"created_at__before": "Filter logs created before this timestamp",
|
||||
"created_at__after": "Filter logs created after this timestamp",
|
||||
"created_by_end_user_session_id": "Filter by end user session ID",
|
||||
"created_by_account": "Filter by account",
|
||||
"page": "Page number (1-99999)",
|
||||
"limit": "Number of items per page (1-100)",
|
||||
}
|
||||
)
|
||||
@api.response(200, "Workflow app logs retrieved successfully", workflow_app_log_pagination_fields)
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@@ -78,6 +95,3 @@ class WorkflowAppLogApi(Resource):
|
||||
)
|
||||
|
||||
return workflow_app_log_pagination
|
||||
|
||||
|
||||
api.add_resource(WorkflowAppLogApi, "/apps/<uuid:app_id>/workflow-app-logs")
|
||||
|
||||
@@ -6,7 +6,7 @@ from flask_restx import Resource, fields, inputs, marshal, marshal_with, reqpars
|
||||
from sqlalchemy.orm import Session
|
||||
from werkzeug.exceptions import Forbidden
|
||||
|
||||
from controllers.console import api
|
||||
from controllers.console import api, console_ns
|
||||
from controllers.console.app.error import (
|
||||
DraftWorkflowNotExist,
|
||||
)
|
||||
@@ -144,7 +144,13 @@ def _api_prerequisite(f):
|
||||
return wrapper
|
||||
|
||||
|
||||
@console_ns.route("/apps/<uuid:app_id>/workflows/draft/variables")
|
||||
class WorkflowVariableCollectionApi(Resource):
|
||||
@api.doc("get_workflow_variables")
|
||||
@api.doc(description="Get draft workflow variables")
|
||||
@api.doc(params={"app_id": "Application ID"})
|
||||
@api.doc(params={"page": "Page number (1-100000)", "limit": "Number of items per page (1-100)"})
|
||||
@api.response(200, "Workflow variables retrieved successfully", _WORKFLOW_DRAFT_VARIABLE_LIST_WITHOUT_VALUE_FIELDS)
|
||||
@_api_prerequisite
|
||||
@marshal_with(_WORKFLOW_DRAFT_VARIABLE_LIST_WITHOUT_VALUE_FIELDS)
|
||||
def get(self, app_model: App):
|
||||
@@ -173,6 +179,9 @@ class WorkflowVariableCollectionApi(Resource):
|
||||
|
||||
return workflow_vars
|
||||
|
||||
@api.doc("delete_workflow_variables")
|
||||
@api.doc(description="Delete all draft workflow variables")
|
||||
@api.response(204, "Workflow variables deleted successfully")
|
||||
@_api_prerequisite
|
||||
def delete(self, app_model: App):
|
||||
draft_var_srv = WorkflowDraftVariableService(
|
||||
@@ -201,7 +210,12 @@ def validate_node_id(node_id: str) -> NoReturn | None:
|
||||
return None
|
||||
|
||||
|
||||
@console_ns.route("/apps/<uuid:app_id>/workflows/draft/nodes/<string:node_id>/variables")
|
||||
class NodeVariableCollectionApi(Resource):
|
||||
@api.doc("get_node_variables")
|
||||
@api.doc(description="Get variables for a specific node")
|
||||
@api.doc(params={"app_id": "Application ID", "node_id": "Node ID"})
|
||||
@api.response(200, "Node variables retrieved successfully", _WORKFLOW_DRAFT_VARIABLE_LIST_FIELDS)
|
||||
@_api_prerequisite
|
||||
@marshal_with(_WORKFLOW_DRAFT_VARIABLE_LIST_FIELDS)
|
||||
def get(self, app_model: App, node_id: str):
|
||||
@@ -214,6 +228,9 @@ class NodeVariableCollectionApi(Resource):
|
||||
|
||||
return node_vars
|
||||
|
||||
@api.doc("delete_node_variables")
|
||||
@api.doc(description="Delete all variables for a specific node")
|
||||
@api.response(204, "Node variables deleted successfully")
|
||||
@_api_prerequisite
|
||||
def delete(self, app_model: App, node_id: str):
|
||||
validate_node_id(node_id)
|
||||
@@ -223,10 +240,16 @@ class NodeVariableCollectionApi(Resource):
|
||||
return Response("", 204)
|
||||
|
||||
|
||||
@console_ns.route("/apps/<uuid:app_id>/workflows/draft/variables/<uuid:variable_id>")
|
||||
class VariableApi(Resource):
|
||||
_PATCH_NAME_FIELD = "name"
|
||||
_PATCH_VALUE_FIELD = "value"
|
||||
|
||||
@api.doc("get_variable")
|
||||
@api.doc(description="Get a specific workflow variable")
|
||||
@api.doc(params={"app_id": "Application ID", "variable_id": "Variable ID"})
|
||||
@api.response(200, "Variable retrieved successfully", _WORKFLOW_DRAFT_VARIABLE_FIELDS)
|
||||
@api.response(404, "Variable not found")
|
||||
@_api_prerequisite
|
||||
@marshal_with(_WORKFLOW_DRAFT_VARIABLE_FIELDS)
|
||||
def get(self, app_model: App, variable_id: str):
|
||||
@@ -240,6 +263,19 @@ class VariableApi(Resource):
|
||||
raise NotFoundError(description=f"variable not found, id={variable_id}")
|
||||
return variable
|
||||
|
||||
@api.doc("update_variable")
|
||||
@api.doc(description="Update a workflow variable")
|
||||
@api.expect(
|
||||
api.model(
|
||||
"UpdateVariableRequest",
|
||||
{
|
||||
"name": fields.String(description="Variable name"),
|
||||
"value": fields.Raw(description="Variable value"),
|
||||
},
|
||||
)
|
||||
)
|
||||
@api.response(200, "Variable updated successfully", _WORKFLOW_DRAFT_VARIABLE_FIELDS)
|
||||
@api.response(404, "Variable not found")
|
||||
@_api_prerequisite
|
||||
@marshal_with(_WORKFLOW_DRAFT_VARIABLE_FIELDS)
|
||||
def patch(self, app_model: App, variable_id: str):
|
||||
@@ -302,6 +338,10 @@ class VariableApi(Resource):
|
||||
db.session.commit()
|
||||
return variable
|
||||
|
||||
@api.doc("delete_variable")
|
||||
@api.doc(description="Delete a workflow variable")
|
||||
@api.response(204, "Variable deleted successfully")
|
||||
@api.response(404, "Variable not found")
|
||||
@_api_prerequisite
|
||||
def delete(self, app_model: App, variable_id: str):
|
||||
draft_var_srv = WorkflowDraftVariableService(
|
||||
@@ -317,7 +357,14 @@ class VariableApi(Resource):
|
||||
return Response("", 204)
|
||||
|
||||
|
||||
@console_ns.route("/apps/<uuid:app_id>/workflows/draft/variables/<uuid:variable_id>/reset")
|
||||
class VariableResetApi(Resource):
|
||||
@api.doc("reset_variable")
|
||||
@api.doc(description="Reset a workflow variable to its default value")
|
||||
@api.doc(params={"app_id": "Application ID", "variable_id": "Variable ID"})
|
||||
@api.response(200, "Variable reset successfully", _WORKFLOW_DRAFT_VARIABLE_FIELDS)
|
||||
@api.response(204, "Variable reset (no content)")
|
||||
@api.response(404, "Variable not found")
|
||||
@_api_prerequisite
|
||||
def put(self, app_model: App, variable_id: str):
|
||||
draft_var_srv = WorkflowDraftVariableService(
|
||||
@@ -358,7 +405,13 @@ def _get_variable_list(app_model: App, node_id) -> WorkflowDraftVariableList:
|
||||
return draft_vars
|
||||
|
||||
|
||||
@console_ns.route("/apps/<uuid:app_id>/workflows/draft/conversation-variables")
|
||||
class ConversationVariableCollectionApi(Resource):
|
||||
@api.doc("get_conversation_variables")
|
||||
@api.doc(description="Get conversation variables for workflow")
|
||||
@api.doc(params={"app_id": "Application ID"})
|
||||
@api.response(200, "Conversation variables retrieved successfully", _WORKFLOW_DRAFT_VARIABLE_LIST_FIELDS)
|
||||
@api.response(404, "Draft workflow not found")
|
||||
@_api_prerequisite
|
||||
@marshal_with(_WORKFLOW_DRAFT_VARIABLE_LIST_FIELDS)
|
||||
def get(self, app_model: App):
|
||||
@@ -374,14 +427,25 @@ class ConversationVariableCollectionApi(Resource):
|
||||
return _get_variable_list(app_model, CONVERSATION_VARIABLE_NODE_ID)
|
||||
|
||||
|
||||
@console_ns.route("/apps/<uuid:app_id>/workflows/draft/system-variables")
|
||||
class SystemVariableCollectionApi(Resource):
|
||||
@api.doc("get_system_variables")
|
||||
@api.doc(description="Get system variables for workflow")
|
||||
@api.doc(params={"app_id": "Application ID"})
|
||||
@api.response(200, "System variables retrieved successfully", _WORKFLOW_DRAFT_VARIABLE_LIST_FIELDS)
|
||||
@_api_prerequisite
|
||||
@marshal_with(_WORKFLOW_DRAFT_VARIABLE_LIST_FIELDS)
|
||||
def get(self, app_model: App):
|
||||
return _get_variable_list(app_model, SYSTEM_VARIABLE_NODE_ID)
|
||||
|
||||
|
||||
@console_ns.route("/apps/<uuid:app_id>/workflows/draft/environment-variables")
|
||||
class EnvironmentVariableCollectionApi(Resource):
|
||||
@api.doc("get_environment_variables")
|
||||
@api.doc(description="Get environment variables for workflow")
|
||||
@api.doc(params={"app_id": "Application ID"})
|
||||
@api.response(200, "Environment variables retrieved successfully")
|
||||
@api.response(404, "Draft workflow not found")
|
||||
@_api_prerequisite
|
||||
def get(self, app_model: App):
|
||||
"""
|
||||
@@ -413,16 +477,3 @@ class EnvironmentVariableCollectionApi(Resource):
|
||||
)
|
||||
|
||||
return {"items": env_vars_list}
|
||||
|
||||
|
||||
api.add_resource(
|
||||
WorkflowVariableCollectionApi,
|
||||
"/apps/<uuid:app_id>/workflows/draft/variables",
|
||||
)
|
||||
api.add_resource(NodeVariableCollectionApi, "/apps/<uuid:app_id>/workflows/draft/nodes/<string:node_id>/variables")
|
||||
api.add_resource(VariableApi, "/apps/<uuid:app_id>/workflows/draft/variables/<uuid:variable_id>")
|
||||
api.add_resource(VariableResetApi, "/apps/<uuid:app_id>/workflows/draft/variables/<uuid:variable_id>/reset")
|
||||
|
||||
api.add_resource(ConversationVariableCollectionApi, "/apps/<uuid:app_id>/workflows/draft/conversation-variables")
|
||||
api.add_resource(SystemVariableCollectionApi, "/apps/<uuid:app_id>/workflows/draft/system-variables")
|
||||
api.add_resource(EnvironmentVariableCollectionApi, "/apps/<uuid:app_id>/workflows/draft/environment-variables")
|
||||
|
||||
@@ -4,7 +4,7 @@ from flask_login import current_user
|
||||
from flask_restx import Resource, marshal_with, reqparse
|
||||
from flask_restx.inputs import int_range
|
||||
|
||||
from controllers.console import api
|
||||
from controllers.console import api, console_ns
|
||||
from controllers.console.app.wraps import get_app_model
|
||||
from controllers.console.wraps import account_initialization_required, setup_required
|
||||
from fields.workflow_run_fields import (
|
||||
@@ -19,7 +19,13 @@ from models import Account, App, AppMode, EndUser
|
||||
from services.workflow_run_service import WorkflowRunService
|
||||
|
||||
|
||||
@console_ns.route("/apps/<uuid:app_id>/advanced-chat/workflow-runs")
|
||||
class AdvancedChatAppWorkflowRunListApi(Resource):
|
||||
@api.doc("get_advanced_chat_workflow_runs")
|
||||
@api.doc(description="Get advanced chat workflow run list")
|
||||
@api.doc(params={"app_id": "Application ID"})
|
||||
@api.doc(params={"last_id": "Last run ID for pagination", "limit": "Number of items per page (1-100)"})
|
||||
@api.response(200, "Workflow runs retrieved successfully", advanced_chat_workflow_run_pagination_fields)
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@@ -40,7 +46,13 @@ class AdvancedChatAppWorkflowRunListApi(Resource):
|
||||
return result
|
||||
|
||||
|
||||
@console_ns.route("/apps/<uuid:app_id>/workflow-runs")
|
||||
class WorkflowRunListApi(Resource):
|
||||
@api.doc("get_workflow_runs")
|
||||
@api.doc(description="Get workflow run list")
|
||||
@api.doc(params={"app_id": "Application ID"})
|
||||
@api.doc(params={"last_id": "Last run ID for pagination", "limit": "Number of items per page (1-100)"})
|
||||
@api.response(200, "Workflow runs retrieved successfully", workflow_run_pagination_fields)
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@@ -61,7 +73,13 @@ class WorkflowRunListApi(Resource):
|
||||
return result
|
||||
|
||||
|
||||
@console_ns.route("/apps/<uuid:app_id>/workflow-runs/<uuid:run_id>")
|
||||
class WorkflowRunDetailApi(Resource):
|
||||
@api.doc("get_workflow_run_detail")
|
||||
@api.doc(description="Get workflow run detail")
|
||||
@api.doc(params={"app_id": "Application ID", "run_id": "Workflow run ID"})
|
||||
@api.response(200, "Workflow run detail retrieved successfully", workflow_run_detail_fields)
|
||||
@api.response(404, "Workflow run not found")
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@@ -79,7 +97,13 @@ class WorkflowRunDetailApi(Resource):
|
||||
return workflow_run
|
||||
|
||||
|
||||
@console_ns.route("/apps/<uuid:app_id>/workflow-runs/<uuid:run_id>/node-executions")
|
||||
class WorkflowRunNodeExecutionListApi(Resource):
|
||||
@api.doc("get_workflow_run_node_executions")
|
||||
@api.doc(description="Get workflow run node execution list")
|
||||
@api.doc(params={"app_id": "Application ID", "run_id": "Workflow run ID"})
|
||||
@api.response(200, "Node executions retrieved successfully", workflow_run_node_execution_list_fields)
|
||||
@api.response(404, "Workflow run not found")
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@@ -100,9 +124,3 @@ class WorkflowRunNodeExecutionListApi(Resource):
|
||||
)
|
||||
|
||||
return {"data": node_executions}
|
||||
|
||||
|
||||
api.add_resource(AdvancedChatAppWorkflowRunListApi, "/apps/<uuid:app_id>/advanced-chat/workflow-runs")
|
||||
api.add_resource(WorkflowRunListApi, "/apps/<uuid:app_id>/workflow-runs")
|
||||
api.add_resource(WorkflowRunDetailApi, "/apps/<uuid:app_id>/workflow-runs/<uuid:run_id>")
|
||||
api.add_resource(WorkflowRunNodeExecutionListApi, "/apps/<uuid:app_id>/workflow-runs/<uuid:run_id>/node-executions")
|
||||
|
||||
@@ -7,7 +7,7 @@ from flask import jsonify
|
||||
from flask_login import current_user
|
||||
from flask_restx import Resource, reqparse
|
||||
|
||||
from controllers.console import api
|
||||
from controllers.console import api, console_ns
|
||||
from controllers.console.app.wraps import get_app_model
|
||||
from controllers.console.wraps import account_initialization_required, setup_required
|
||||
from extensions.ext_database import db
|
||||
@@ -17,7 +17,13 @@ from models.enums import WorkflowRunTriggeredFrom
|
||||
from models.model import AppMode
|
||||
|
||||
|
||||
@console_ns.route("/apps/<uuid:app_id>/workflow/statistics/daily-conversations")
|
||||
class WorkflowDailyRunsStatistic(Resource):
|
||||
@api.doc("get_workflow_daily_runs_statistic")
|
||||
@api.doc(description="Get workflow daily runs statistics")
|
||||
@api.doc(params={"app_id": "Application ID"})
|
||||
@api.doc(params={"start": "Start date and time (YYYY-MM-DD HH:MM)", "end": "End date and time (YYYY-MM-DD HH:MM)"})
|
||||
@api.response(200, "Daily runs statistics retrieved successfully")
|
||||
@get_app_model
|
||||
@setup_required
|
||||
@login_required
|
||||
@@ -79,7 +85,13 @@ WHERE
|
||||
return jsonify({"data": response_data})
|
||||
|
||||
|
||||
@console_ns.route("/apps/<uuid:app_id>/workflow/statistics/daily-terminals")
|
||||
class WorkflowDailyTerminalsStatistic(Resource):
|
||||
@api.doc("get_workflow_daily_terminals_statistic")
|
||||
@api.doc(description="Get workflow daily terminals statistics")
|
||||
@api.doc(params={"app_id": "Application ID"})
|
||||
@api.doc(params={"start": "Start date and time (YYYY-MM-DD HH:MM)", "end": "End date and time (YYYY-MM-DD HH:MM)"})
|
||||
@api.response(200, "Daily terminals statistics retrieved successfully")
|
||||
@get_app_model
|
||||
@setup_required
|
||||
@login_required
|
||||
@@ -141,7 +153,13 @@ WHERE
|
||||
return jsonify({"data": response_data})
|
||||
|
||||
|
||||
@console_ns.route("/apps/<uuid:app_id>/workflow/statistics/token-costs")
|
||||
class WorkflowDailyTokenCostStatistic(Resource):
|
||||
@api.doc("get_workflow_daily_token_cost_statistic")
|
||||
@api.doc(description="Get workflow daily token cost statistics")
|
||||
@api.doc(params={"app_id": "Application ID"})
|
||||
@api.doc(params={"start": "Start date and time (YYYY-MM-DD HH:MM)", "end": "End date and time (YYYY-MM-DD HH:MM)"})
|
||||
@api.response(200, "Daily token cost statistics retrieved successfully")
|
||||
@get_app_model
|
||||
@setup_required
|
||||
@login_required
|
||||
@@ -208,7 +226,13 @@ WHERE
|
||||
return jsonify({"data": response_data})
|
||||
|
||||
|
||||
@console_ns.route("/apps/<uuid:app_id>/workflow/statistics/average-app-interactions")
|
||||
class WorkflowAverageAppInteractionStatistic(Resource):
|
||||
@api.doc("get_workflow_average_app_interaction_statistic")
|
||||
@api.doc(description="Get workflow average app interaction statistics")
|
||||
@api.doc(params={"app_id": "Application ID"})
|
||||
@api.doc(params={"start": "Start date and time (YYYY-MM-DD HH:MM)", "end": "End date and time (YYYY-MM-DD HH:MM)"})
|
||||
@api.response(200, "Average app interaction statistics retrieved successfully")
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@@ -285,11 +309,3 @@ GROUP BY
|
||||
)
|
||||
|
||||
return jsonify({"data": response_data})
|
||||
|
||||
|
||||
api.add_resource(WorkflowDailyRunsStatistic, "/apps/<uuid:app_id>/workflow/statistics/daily-conversations")
|
||||
api.add_resource(WorkflowDailyTerminalsStatistic, "/apps/<uuid:app_id>/workflow/statistics/daily-terminals")
|
||||
api.add_resource(WorkflowDailyTokenCostStatistic, "/apps/<uuid:app_id>/workflow/statistics/token-costs")
|
||||
api.add_resource(
|
||||
WorkflowAverageAppInteractionStatistic, "/apps/<uuid:app_id>/workflow/statistics/average-app-interactions"
|
||||
)
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import flask_restx
|
||||
from flask import request
|
||||
from flask_login import current_user
|
||||
from flask_restx import Resource, marshal, marshal_with, reqparse
|
||||
from flask_restx import Resource, fields, marshal, marshal_with, reqparse
|
||||
from sqlalchemy import select
|
||||
from werkzeug.exceptions import Forbidden, NotFound
|
||||
|
||||
import services
|
||||
from configs import dify_config
|
||||
from controllers.console import api
|
||||
from controllers.console import api, console_ns
|
||||
from controllers.console.apikey import api_key_fields, api_key_list
|
||||
from controllers.console.app.error import ProviderNotInitializeError
|
||||
from controllers.console.datasets.error import DatasetInUseError, DatasetNameDuplicateError, IndexingEstimateError
|
||||
@@ -48,7 +48,21 @@ def _validate_description_length(description):
|
||||
return description
|
||||
|
||||
|
||||
@console_ns.route("/datasets")
|
||||
class DatasetListApi(Resource):
|
||||
@api.doc("get_datasets")
|
||||
@api.doc(description="Get list of datasets")
|
||||
@api.doc(
|
||||
params={
|
||||
"page": "Page number (default: 1)",
|
||||
"limit": "Number of items per page (default: 20)",
|
||||
"ids": "Filter by dataset IDs (list)",
|
||||
"keyword": "Search keyword",
|
||||
"tag_ids": "Filter by tag IDs (list)",
|
||||
"include_all": "Include all datasets (default: false)",
|
||||
}
|
||||
)
|
||||
@api.response(200, "Datasets retrieved successfully")
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@@ -100,6 +114,24 @@ class DatasetListApi(Resource):
|
||||
response = {"data": data, "has_more": len(datasets) == limit, "limit": limit, "total": total, "page": page}
|
||||
return response, 200
|
||||
|
||||
@api.doc("create_dataset")
|
||||
@api.doc(description="Create a new dataset")
|
||||
@api.expect(
|
||||
api.model(
|
||||
"CreateDatasetRequest",
|
||||
{
|
||||
"name": fields.String(required=True, description="Dataset name (1-40 characters)"),
|
||||
"description": fields.String(description="Dataset description (max 400 characters)"),
|
||||
"indexing_technique": fields.String(description="Indexing technique"),
|
||||
"permission": fields.String(description="Dataset permission"),
|
||||
"provider": fields.String(description="Provider"),
|
||||
"external_knowledge_api_id": fields.String(description="External knowledge API ID"),
|
||||
"external_knowledge_id": fields.String(description="External knowledge ID"),
|
||||
},
|
||||
)
|
||||
)
|
||||
@api.response(201, "Dataset created successfully")
|
||||
@api.response(400, "Invalid request parameters")
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@@ -172,7 +204,14 @@ class DatasetListApi(Resource):
|
||||
return marshal(dataset, dataset_detail_fields), 201
|
||||
|
||||
|
||||
@console_ns.route("/datasets/<uuid:dataset_id>")
|
||||
class DatasetApi(Resource):
|
||||
@api.doc("get_dataset")
|
||||
@api.doc(description="Get dataset details")
|
||||
@api.doc(params={"dataset_id": "Dataset ID"})
|
||||
@api.response(200, "Dataset retrieved successfully", dataset_detail_fields)
|
||||
@api.response(404, "Dataset not found")
|
||||
@api.response(403, "Permission denied")
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@@ -215,6 +254,23 @@ class DatasetApi(Resource):
|
||||
|
||||
return data, 200
|
||||
|
||||
@api.doc("update_dataset")
|
||||
@api.doc(description="Update dataset details")
|
||||
@api.expect(
|
||||
api.model(
|
||||
"UpdateDatasetRequest",
|
||||
{
|
||||
"name": fields.String(description="Dataset name"),
|
||||
"description": fields.String(description="Dataset description"),
|
||||
"permission": fields.String(description="Dataset permission"),
|
||||
"indexing_technique": fields.String(description="Indexing technique"),
|
||||
"external_retrieval_model": fields.Raw(description="External retrieval model settings"),
|
||||
},
|
||||
)
|
||||
)
|
||||
@api.response(200, "Dataset updated successfully", dataset_detail_fields)
|
||||
@api.response(404, "Dataset not found")
|
||||
@api.response(403, "Permission denied")
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@@ -344,7 +400,12 @@ class DatasetApi(Resource):
|
||||
raise DatasetInUseError()
|
||||
|
||||
|
||||
@console_ns.route("/datasets/<uuid:dataset_id>/use-check")
|
||||
class DatasetUseCheckApi(Resource):
|
||||
@api.doc("check_dataset_use")
|
||||
@api.doc(description="Check if dataset is in use")
|
||||
@api.doc(params={"dataset_id": "Dataset ID"})
|
||||
@api.response(200, "Dataset use status retrieved successfully")
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@@ -355,7 +416,12 @@ class DatasetUseCheckApi(Resource):
|
||||
return {"is_using": dataset_is_using}, 200
|
||||
|
||||
|
||||
@console_ns.route("/datasets/<uuid:dataset_id>/queries")
|
||||
class DatasetQueryApi(Resource):
|
||||
@api.doc("get_dataset_queries")
|
||||
@api.doc(description="Get dataset query history")
|
||||
@api.doc(params={"dataset_id": "Dataset ID"})
|
||||
@api.response(200, "Query history retrieved successfully", dataset_query_detail_fields)
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@@ -385,7 +451,11 @@ class DatasetQueryApi(Resource):
|
||||
return response, 200
|
||||
|
||||
|
||||
@console_ns.route("/datasets/indexing-estimate")
|
||||
class DatasetIndexingEstimateApi(Resource):
|
||||
@api.doc("estimate_dataset_indexing")
|
||||
@api.doc(description="Estimate dataset indexing cost")
|
||||
@api.response(200, "Indexing estimate calculated successfully")
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@@ -486,7 +556,12 @@ class DatasetIndexingEstimateApi(Resource):
|
||||
return response.model_dump(), 200
|
||||
|
||||
|
||||
@console_ns.route("/datasets/<uuid:dataset_id>/related-apps")
|
||||
class DatasetRelatedAppListApi(Resource):
|
||||
@api.doc("get_dataset_related_apps")
|
||||
@api.doc(description="Get applications related to dataset")
|
||||
@api.doc(params={"dataset_id": "Dataset ID"})
|
||||
@api.response(200, "Related apps retrieved successfully", related_app_list)
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@@ -513,7 +588,12 @@ class DatasetRelatedAppListApi(Resource):
|
||||
return {"data": related_apps, "total": len(related_apps)}, 200
|
||||
|
||||
|
||||
@console_ns.route("/datasets/<uuid:dataset_id>/indexing-status")
|
||||
class DatasetIndexingStatusApi(Resource):
|
||||
@api.doc("get_dataset_indexing_status")
|
||||
@api.doc(description="Get dataset indexing status")
|
||||
@api.doc(params={"dataset_id": "Dataset ID"})
|
||||
@api.response(200, "Indexing status retrieved successfully")
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@@ -560,11 +640,15 @@ class DatasetIndexingStatusApi(Resource):
|
||||
return data, 200
|
||||
|
||||
|
||||
@console_ns.route("/datasets/api-keys")
|
||||
class DatasetApiKeyApi(Resource):
|
||||
max_keys = 10
|
||||
token_prefix = "dataset-"
|
||||
resource_type = "dataset"
|
||||
|
||||
@api.doc("get_dataset_api_keys")
|
||||
@api.doc(description="Get dataset API keys")
|
||||
@api.response(200, "API keys retrieved successfully", api_key_list)
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@@ -609,9 +693,14 @@ class DatasetApiKeyApi(Resource):
|
||||
return api_token, 200
|
||||
|
||||
|
||||
@console_ns.route("/datasets/api-keys/<uuid:api_key_id>")
|
||||
class DatasetApiDeleteApi(Resource):
|
||||
resource_type = "dataset"
|
||||
|
||||
@api.doc("delete_dataset_api_key")
|
||||
@api.doc(description="Delete dataset API key")
|
||||
@api.doc(params={"api_key_id": "API key ID"})
|
||||
@api.response(204, "API key deleted successfully")
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@@ -641,7 +730,11 @@ class DatasetApiDeleteApi(Resource):
|
||||
return {"result": "success"}, 204
|
||||
|
||||
|
||||
@console_ns.route("/datasets/api-base-info")
|
||||
class DatasetApiBaseUrlApi(Resource):
|
||||
@api.doc("get_dataset_api_base_info")
|
||||
@api.doc(description="Get dataset API base information")
|
||||
@api.response(200, "API base info retrieved successfully")
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@@ -649,7 +742,11 @@ class DatasetApiBaseUrlApi(Resource):
|
||||
return {"api_base_url": (dify_config.SERVICE_API_URL or request.host_url.rstrip("/")) + "/v1"}
|
||||
|
||||
|
||||
@console_ns.route("/datasets/retrieval-setting")
|
||||
class DatasetRetrievalSettingApi(Resource):
|
||||
@api.doc("get_dataset_retrieval_setting")
|
||||
@api.doc(description="Get dataset retrieval settings")
|
||||
@api.response(200, "Retrieval settings retrieved successfully")
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@@ -700,7 +797,12 @@ class DatasetRetrievalSettingApi(Resource):
|
||||
raise ValueError(f"Unsupported vector db type {vector_type}.")
|
||||
|
||||
|
||||
@console_ns.route("/datasets/retrieval-setting/<string:vector_type>")
|
||||
class DatasetRetrievalSettingMockApi(Resource):
|
||||
@api.doc("get_dataset_retrieval_setting_mock")
|
||||
@api.doc(description="Get mock dataset retrieval settings by vector type")
|
||||
@api.doc(params={"vector_type": "Vector store type"})
|
||||
@api.response(200, "Mock retrieval settings retrieved successfully")
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@@ -749,7 +851,13 @@ class DatasetRetrievalSettingMockApi(Resource):
|
||||
raise ValueError(f"Unsupported vector db type {vector_type}.")
|
||||
|
||||
|
||||
@console_ns.route("/datasets/<uuid:dataset_id>/error-docs")
|
||||
class DatasetErrorDocs(Resource):
|
||||
@api.doc("get_dataset_error_docs")
|
||||
@api.doc(description="Get dataset error documents")
|
||||
@api.doc(params={"dataset_id": "Dataset ID"})
|
||||
@api.response(200, "Error documents retrieved successfully")
|
||||
@api.response(404, "Dataset not found")
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@@ -763,7 +871,14 @@ class DatasetErrorDocs(Resource):
|
||||
return {"data": [marshal(item, document_status_fields) for item in results], "total": len(results)}, 200
|
||||
|
||||
|
||||
@console_ns.route("/datasets/<uuid:dataset_id>/permission-part-users")
|
||||
class DatasetPermissionUserListApi(Resource):
|
||||
@api.doc("get_dataset_permission_users")
|
||||
@api.doc(description="Get dataset permission user list")
|
||||
@api.doc(params={"dataset_id": "Dataset ID"})
|
||||
@api.response(200, "Permission users retrieved successfully")
|
||||
@api.response(404, "Dataset not found")
|
||||
@api.response(403, "Permission denied")
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@@ -784,7 +899,13 @@ class DatasetPermissionUserListApi(Resource):
|
||||
}, 200
|
||||
|
||||
|
||||
@console_ns.route("/datasets/<uuid:dataset_id>/auto-disable-logs")
|
||||
class DatasetAutoDisableLogApi(Resource):
|
||||
@api.doc("get_dataset_auto_disable_logs")
|
||||
@api.doc(description="Get dataset auto disable logs")
|
||||
@api.doc(params={"dataset_id": "Dataset ID"})
|
||||
@api.response(200, "Auto disable logs retrieved successfully")
|
||||
@api.response(404, "Dataset not found")
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@@ -794,20 +915,3 @@ class DatasetAutoDisableLogApi(Resource):
|
||||
if dataset is None:
|
||||
raise NotFound("Dataset not found.")
|
||||
return DatasetService.get_dataset_auto_disable_logs(dataset_id_str), 200
|
||||
|
||||
|
||||
api.add_resource(DatasetListApi, "/datasets")
|
||||
api.add_resource(DatasetApi, "/datasets/<uuid:dataset_id>")
|
||||
api.add_resource(DatasetUseCheckApi, "/datasets/<uuid:dataset_id>/use-check")
|
||||
api.add_resource(DatasetQueryApi, "/datasets/<uuid:dataset_id>/queries")
|
||||
api.add_resource(DatasetErrorDocs, "/datasets/<uuid:dataset_id>/error-docs")
|
||||
api.add_resource(DatasetIndexingEstimateApi, "/datasets/indexing-estimate")
|
||||
api.add_resource(DatasetRelatedAppListApi, "/datasets/<uuid:dataset_id>/related-apps")
|
||||
api.add_resource(DatasetIndexingStatusApi, "/datasets/<uuid:dataset_id>/indexing-status")
|
||||
api.add_resource(DatasetApiKeyApi, "/datasets/api-keys")
|
||||
api.add_resource(DatasetApiDeleteApi, "/datasets/api-keys/<uuid:api_key_id>")
|
||||
api.add_resource(DatasetApiBaseUrlApi, "/datasets/api-base-info")
|
||||
api.add_resource(DatasetRetrievalSettingApi, "/datasets/retrieval-setting")
|
||||
api.add_resource(DatasetRetrievalSettingMockApi, "/datasets/retrieval-setting/<string:vector_type>")
|
||||
api.add_resource(DatasetPermissionUserListApi, "/datasets/<uuid:dataset_id>/permission-part-users")
|
||||
api.add_resource(DatasetAutoDisableLogApi, "/datasets/<uuid:dataset_id>/auto-disable-logs")
|
||||
|
||||
@@ -5,12 +5,12 @@ from typing import Literal, cast
|
||||
|
||||
from flask import request
|
||||
from flask_login import current_user
|
||||
from flask_restx import Resource, marshal, marshal_with, reqparse
|
||||
from flask_restx import Resource, fields, marshal, marshal_with, reqparse
|
||||
from sqlalchemy import asc, desc, select
|
||||
from werkzeug.exceptions import Forbidden, NotFound
|
||||
|
||||
import services
|
||||
from controllers.console import api
|
||||
from controllers.console import api, console_ns
|
||||
from controllers.console.app.error import (
|
||||
ProviderModelCurrentlyNotSupportError,
|
||||
ProviderNotInitializeError,
|
||||
@@ -98,7 +98,12 @@ class DocumentResource(Resource):
|
||||
return documents
|
||||
|
||||
|
||||
@console_ns.route("/datasets/process-rule")
|
||||
class GetProcessRuleApi(Resource):
|
||||
@api.doc("get_process_rule")
|
||||
@api.doc(description="Get dataset document processing rules")
|
||||
@api.doc(params={"document_id": "Document ID (optional)"})
|
||||
@api.response(200, "Process rules retrieved successfully")
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@@ -140,7 +145,21 @@ class GetProcessRuleApi(Resource):
|
||||
return {"mode": mode, "rules": rules, "limits": limits}
|
||||
|
||||
|
||||
@console_ns.route("/datasets/<uuid:dataset_id>/documents")
|
||||
class DatasetDocumentListApi(Resource):
|
||||
@api.doc("get_dataset_documents")
|
||||
@api.doc(description="Get documents in a dataset")
|
||||
@api.doc(
|
||||
params={
|
||||
"dataset_id": "Dataset ID",
|
||||
"page": "Page number (default: 1)",
|
||||
"limit": "Number of items per page (default: 20)",
|
||||
"keyword": "Search keyword",
|
||||
"sort": "Sort order (default: -created_at)",
|
||||
"fetch": "Fetch full details (default: false)",
|
||||
}
|
||||
)
|
||||
@api.response(200, "Documents retrieved successfully")
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@@ -324,7 +343,23 @@ class DatasetDocumentListApi(Resource):
|
||||
return {"result": "success"}, 204
|
||||
|
||||
|
||||
@console_ns.route("/datasets/init")
|
||||
class DatasetInitApi(Resource):
|
||||
@api.doc("init_dataset")
|
||||
@api.doc(description="Initialize dataset with documents")
|
||||
@api.expect(
|
||||
api.model(
|
||||
"DatasetInitRequest",
|
||||
{
|
||||
"upload_file_id": fields.String(required=True, description="Upload file ID"),
|
||||
"indexing_technique": fields.String(description="Indexing technique"),
|
||||
"process_rule": fields.Raw(description="Processing rules"),
|
||||
"data_source": fields.Raw(description="Data source configuration"),
|
||||
},
|
||||
)
|
||||
)
|
||||
@api.response(201, "Dataset initialized successfully", dataset_and_document_fields)
|
||||
@api.response(400, "Invalid request parameters")
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@@ -394,7 +429,14 @@ class DatasetInitApi(Resource):
|
||||
return response
|
||||
|
||||
|
||||
@console_ns.route("/datasets/<uuid:dataset_id>/documents/<uuid:document_id>/indexing-estimate")
|
||||
class DocumentIndexingEstimateApi(DocumentResource):
|
||||
@api.doc("estimate_document_indexing")
|
||||
@api.doc(description="Estimate document indexing cost")
|
||||
@api.doc(params={"dataset_id": "Dataset ID", "document_id": "Document ID"})
|
||||
@api.response(200, "Indexing estimate calculated successfully")
|
||||
@api.response(404, "Document not found")
|
||||
@api.response(400, "Document already finished")
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@@ -457,6 +499,7 @@ 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
|
||||
@@ -549,6 +592,7 @@ 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
|
||||
@@ -593,7 +637,13 @@ class DocumentBatchIndexingStatusApi(DocumentResource):
|
||||
return data
|
||||
|
||||
|
||||
@console_ns.route("/datasets/<uuid:dataset_id>/documents/<uuid:document_id>/indexing-status")
|
||||
class DocumentIndexingStatusApi(DocumentResource):
|
||||
@api.doc("get_document_indexing_status")
|
||||
@api.doc(description="Get document indexing status")
|
||||
@api.doc(params={"dataset_id": "Dataset ID", "document_id": "Document ID"})
|
||||
@api.response(200, "Indexing status retrieved successfully")
|
||||
@api.response(404, "Document not found")
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@@ -635,9 +685,21 @@ class DocumentIndexingStatusApi(DocumentResource):
|
||||
return marshal(document_dict, document_status_fields)
|
||||
|
||||
|
||||
@console_ns.route("/datasets/<uuid:dataset_id>/documents/<uuid:document_id>")
|
||||
class DocumentApi(DocumentResource):
|
||||
METADATA_CHOICES = {"all", "only", "without"}
|
||||
|
||||
@api.doc("get_document")
|
||||
@api.doc(description="Get document details")
|
||||
@api.doc(
|
||||
params={
|
||||
"dataset_id": "Dataset ID",
|
||||
"document_id": "Document ID",
|
||||
"metadata": "Metadata inclusion (all/only/without)",
|
||||
}
|
||||
)
|
||||
@api.response(200, "Document retrieved successfully")
|
||||
@api.response(404, "Document not found")
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@@ -746,7 +808,16 @@ class DocumentApi(DocumentResource):
|
||||
return {"result": "success"}, 204
|
||||
|
||||
|
||||
@console_ns.route("/datasets/<uuid:dataset_id>/documents/<uuid:document_id>/processing/<string:action>")
|
||||
class DocumentProcessingApi(DocumentResource):
|
||||
@api.doc("update_document_processing")
|
||||
@api.doc(description="Update document processing status (pause/resume)")
|
||||
@api.doc(
|
||||
params={"dataset_id": "Dataset ID", "document_id": "Document ID", "action": "Action to perform (pause/resume)"}
|
||||
)
|
||||
@api.response(200, "Processing status updated successfully")
|
||||
@api.response(404, "Document not found")
|
||||
@api.response(400, "Invalid action")
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@@ -781,7 +852,23 @@ class DocumentProcessingApi(DocumentResource):
|
||||
return {"result": "success"}, 200
|
||||
|
||||
|
||||
@console_ns.route("/datasets/<uuid:dataset_id>/documents/<uuid:document_id>/metadata")
|
||||
class DocumentMetadataApi(DocumentResource):
|
||||
@api.doc("update_document_metadata")
|
||||
@api.doc(description="Update document metadata")
|
||||
@api.doc(params={"dataset_id": "Dataset ID", "document_id": "Document ID"})
|
||||
@api.expect(
|
||||
api.model(
|
||||
"UpdateDocumentMetadataRequest",
|
||||
{
|
||||
"doc_type": fields.String(description="Document type"),
|
||||
"doc_metadata": fields.Raw(description="Document metadata"),
|
||||
},
|
||||
)
|
||||
)
|
||||
@api.response(200, "Document metadata updated successfully")
|
||||
@api.response(404, "Document not found")
|
||||
@api.response(403, "Permission denied")
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@@ -825,6 +912,7 @@ 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
|
||||
@@ -861,6 +949,7 @@ 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
|
||||
@@ -894,6 +983,7 @@ 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
|
||||
@@ -924,6 +1014,7 @@ class DocumentRecoverApi(DocumentResource):
|
||||
return {"result": "success"}, 204
|
||||
|
||||
|
||||
@console_ns.route("/datasets/<uuid:dataset_id>/retry")
|
||||
class DocumentRetryApi(DocumentResource):
|
||||
@setup_required
|
||||
@login_required
|
||||
@@ -967,6 +1058,7 @@ 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
|
||||
@@ -990,6 +1082,7 @@ 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
|
||||
@@ -1015,26 +1108,3 @@ class WebsiteDocumentSyncApi(DocumentResource):
|
||||
DocumentService.sync_website_document(dataset_id, document)
|
||||
|
||||
return {"result": "success"}, 200
|
||||
|
||||
|
||||
api.add_resource(GetProcessRuleApi, "/datasets/process-rule")
|
||||
api.add_resource(DatasetDocumentListApi, "/datasets/<uuid:dataset_id>/documents")
|
||||
api.add_resource(DatasetInitApi, "/datasets/init")
|
||||
api.add_resource(
|
||||
DocumentIndexingEstimateApi, "/datasets/<uuid:dataset_id>/documents/<uuid:document_id>/indexing-estimate"
|
||||
)
|
||||
api.add_resource(DocumentBatchIndexingEstimateApi, "/datasets/<uuid:dataset_id>/batch/<string:batch>/indexing-estimate")
|
||||
api.add_resource(DocumentBatchIndexingStatusApi, "/datasets/<uuid:dataset_id>/batch/<string:batch>/indexing-status")
|
||||
api.add_resource(DocumentIndexingStatusApi, "/datasets/<uuid:dataset_id>/documents/<uuid:document_id>/indexing-status")
|
||||
api.add_resource(DocumentApi, "/datasets/<uuid:dataset_id>/documents/<uuid:document_id>")
|
||||
api.add_resource(
|
||||
DocumentProcessingApi, "/datasets/<uuid:dataset_id>/documents/<uuid:document_id>/processing/<string:action>"
|
||||
)
|
||||
api.add_resource(DocumentMetadataApi, "/datasets/<uuid:dataset_id>/documents/<uuid:document_id>/metadata")
|
||||
api.add_resource(DocumentStatusApi, "/datasets/<uuid:dataset_id>/documents/status/<string:action>/batch")
|
||||
api.add_resource(DocumentPauseApi, "/datasets/<uuid:dataset_id>/documents/<uuid:document_id>/processing/pause")
|
||||
api.add_resource(DocumentRecoverApi, "/datasets/<uuid:dataset_id>/documents/<uuid:document_id>/processing/resume")
|
||||
api.add_resource(DocumentRetryApi, "/datasets/<uuid:dataset_id>/retry")
|
||||
api.add_resource(DocumentRenameApi, "/datasets/<uuid:dataset_id>/documents/<uuid:document_id>/rename")
|
||||
|
||||
api.add_resource(WebsiteDocumentSyncApi, "/datasets/<uuid:dataset_id>/documents/<uuid:document_id>/website-sync")
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
from flask import request
|
||||
from flask_login import current_user
|
||||
from flask_restx import Resource, marshal, reqparse
|
||||
from flask_restx import Resource, fields, marshal, reqparse
|
||||
from werkzeug.exceptions import Forbidden, InternalServerError, NotFound
|
||||
|
||||
import services
|
||||
from controllers.console import api
|
||||
from controllers.console import api, console_ns
|
||||
from controllers.console.datasets.error import DatasetNameDuplicateError
|
||||
from controllers.console.wraps import account_initialization_required, setup_required
|
||||
from fields.dataset_fields import dataset_detail_fields
|
||||
@@ -21,7 +21,18 @@ def _validate_name(name):
|
||||
return name
|
||||
|
||||
|
||||
@console_ns.route("/datasets/external-knowledge-api")
|
||||
class ExternalApiTemplateListApi(Resource):
|
||||
@api.doc("get_external_api_templates")
|
||||
@api.doc(description="Get external knowledge API templates")
|
||||
@api.doc(
|
||||
params={
|
||||
"page": "Page number (default: 1)",
|
||||
"limit": "Number of items per page (default: 20)",
|
||||
"keyword": "Search keyword",
|
||||
}
|
||||
)
|
||||
@api.response(200, "External API templates retrieved successfully")
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@@ -79,7 +90,13 @@ class ExternalApiTemplateListApi(Resource):
|
||||
return external_knowledge_api.to_dict(), 201
|
||||
|
||||
|
||||
@console_ns.route("/datasets/external-knowledge-api/<uuid:external_knowledge_api_id>")
|
||||
class ExternalApiTemplateApi(Resource):
|
||||
@api.doc("get_external_api_template")
|
||||
@api.doc(description="Get external knowledge API template details")
|
||||
@api.doc(params={"external_knowledge_api_id": "External knowledge API ID"})
|
||||
@api.response(200, "External API template retrieved successfully")
|
||||
@api.response(404, "Template not found")
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@@ -138,7 +155,12 @@ class ExternalApiTemplateApi(Resource):
|
||||
return {"result": "success"}, 204
|
||||
|
||||
|
||||
@console_ns.route("/datasets/external-knowledge-api/<uuid:external_knowledge_api_id>/use-check")
|
||||
class ExternalApiUseCheckApi(Resource):
|
||||
@api.doc("check_external_api_usage")
|
||||
@api.doc(description="Check if external knowledge API is being used")
|
||||
@api.doc(params={"external_knowledge_api_id": "External knowledge API ID"})
|
||||
@api.response(200, "Usage check completed successfully")
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@@ -151,7 +173,24 @@ class ExternalApiUseCheckApi(Resource):
|
||||
return {"is_using": external_knowledge_api_is_using, "count": count}, 200
|
||||
|
||||
|
||||
@console_ns.route("/datasets/external")
|
||||
class ExternalDatasetCreateApi(Resource):
|
||||
@api.doc("create_external_dataset")
|
||||
@api.doc(description="Create external knowledge dataset")
|
||||
@api.expect(
|
||||
api.model(
|
||||
"CreateExternalDatasetRequest",
|
||||
{
|
||||
"external_knowledge_api_id": fields.String(required=True, description="External knowledge API ID"),
|
||||
"external_knowledge_id": fields.String(required=True, description="External knowledge ID"),
|
||||
"name": fields.String(required=True, description="Dataset name"),
|
||||
"description": fields.String(description="Dataset description"),
|
||||
},
|
||||
)
|
||||
)
|
||||
@api.response(201, "External dataset created successfully", dataset_detail_fields)
|
||||
@api.response(400, "Invalid parameters")
|
||||
@api.response(403, "Permission denied")
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@@ -191,7 +230,24 @@ class ExternalDatasetCreateApi(Resource):
|
||||
return marshal(dataset, dataset_detail_fields), 201
|
||||
|
||||
|
||||
@console_ns.route("/datasets/<uuid:dataset_id>/external-hit-testing")
|
||||
class ExternalKnowledgeHitTestingApi(Resource):
|
||||
@api.doc("test_external_knowledge_retrieval")
|
||||
@api.doc(description="Test external knowledge retrieval for dataset")
|
||||
@api.doc(params={"dataset_id": "Dataset ID"})
|
||||
@api.expect(
|
||||
api.model(
|
||||
"ExternalHitTestingRequest",
|
||||
{
|
||||
"query": fields.String(required=True, description="Query text for testing"),
|
||||
"retrieval_model": fields.Raw(description="Retrieval model configuration"),
|
||||
"external_retrieval_model": fields.Raw(description="External retrieval model configuration"),
|
||||
},
|
||||
)
|
||||
)
|
||||
@api.response(200, "External hit testing completed successfully")
|
||||
@api.response(404, "Dataset not found")
|
||||
@api.response(400, "Invalid parameters")
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@@ -228,8 +284,22 @@ class ExternalKnowledgeHitTestingApi(Resource):
|
||||
raise InternalServerError(str(e))
|
||||
|
||||
|
||||
@console_ns.route("/test/retrieval")
|
||||
class BedrockRetrievalApi(Resource):
|
||||
# this api is only for internal testing
|
||||
@api.doc("bedrock_retrieval_test")
|
||||
@api.doc(description="Bedrock retrieval test (internal use only)")
|
||||
@api.expect(
|
||||
api.model(
|
||||
"BedrockRetrievalTestRequest",
|
||||
{
|
||||
"retrieval_setting": fields.Raw(required=True, description="Retrieval settings"),
|
||||
"query": fields.String(required=True, description="Query text"),
|
||||
"knowledge_id": fields.String(required=True, description="Knowledge ID"),
|
||||
},
|
||||
)
|
||||
)
|
||||
@api.response(200, "Bedrock retrieval test completed")
|
||||
def post(self):
|
||||
parser = reqparse.RequestParser()
|
||||
parser.add_argument("retrieval_setting", nullable=False, required=True, type=dict, location="json")
|
||||
@@ -247,12 +317,3 @@ class BedrockRetrievalApi(Resource):
|
||||
args["retrieval_setting"], args["query"], args["knowledge_id"]
|
||||
)
|
||||
return result, 200
|
||||
|
||||
|
||||
api.add_resource(ExternalKnowledgeHitTestingApi, "/datasets/<uuid:dataset_id>/external-hit-testing")
|
||||
api.add_resource(ExternalDatasetCreateApi, "/datasets/external")
|
||||
api.add_resource(ExternalApiTemplateListApi, "/datasets/external-knowledge-api")
|
||||
api.add_resource(ExternalApiTemplateApi, "/datasets/external-knowledge-api/<uuid:external_knowledge_api_id>")
|
||||
api.add_resource(ExternalApiUseCheckApi, "/datasets/external-knowledge-api/<uuid:external_knowledge_api_id>/use-check")
|
||||
# this api is only for internal test
|
||||
api.add_resource(BedrockRetrievalApi, "/test/retrieval")
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from flask_restx import Resource
|
||||
from flask_restx import Resource, fields
|
||||
|
||||
from controllers.console import api
|
||||
from controllers.console import api, console_ns
|
||||
from controllers.console.datasets.hit_testing_base import DatasetsHitTestingBase
|
||||
from controllers.console.wraps import (
|
||||
account_initialization_required,
|
||||
@@ -10,7 +10,25 @@ from controllers.console.wraps import (
|
||||
from libs.login import login_required
|
||||
|
||||
|
||||
@console_ns.route("/datasets/<uuid:dataset_id>/hit-testing")
|
||||
class HitTestingApi(Resource, DatasetsHitTestingBase):
|
||||
@api.doc("test_dataset_retrieval")
|
||||
@api.doc(description="Test dataset knowledge retrieval")
|
||||
@api.doc(params={"dataset_id": "Dataset ID"})
|
||||
@api.expect(
|
||||
api.model(
|
||||
"HitTestingRequest",
|
||||
{
|
||||
"query": fields.String(required=True, description="Query text for testing"),
|
||||
"retrieval_model": fields.Raw(description="Retrieval model configuration"),
|
||||
"top_k": fields.Integer(description="Number of top results to return"),
|
||||
"score_threshold": fields.Float(description="Score threshold for filtering results"),
|
||||
},
|
||||
)
|
||||
)
|
||||
@api.response(200, "Hit testing completed successfully")
|
||||
@api.response(404, "Dataset not found")
|
||||
@api.response(400, "Invalid parameters")
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@@ -23,6 +41,3 @@ class HitTestingApi(Resource, DatasetsHitTestingBase):
|
||||
self.hit_testing_args_check(args)
|
||||
|
||||
return self.perform_hit_testing(dataset, args)
|
||||
|
||||
|
||||
api.add_resource(HitTestingApi, "/datasets/<uuid:dataset_id>/hit-testing")
|
||||
|
||||
@@ -1,13 +1,32 @@
|
||||
from flask_restx import Resource, reqparse
|
||||
from flask_restx import Resource, fields, reqparse
|
||||
|
||||
from controllers.console import api
|
||||
from controllers.console import api, console_ns
|
||||
from controllers.console.datasets.error import WebsiteCrawlError
|
||||
from controllers.console.wraps import account_initialization_required, setup_required
|
||||
from libs.login import login_required
|
||||
from services.website_service import WebsiteCrawlApiRequest, WebsiteCrawlStatusApiRequest, WebsiteService
|
||||
|
||||
|
||||
@console_ns.route("/website/crawl")
|
||||
class WebsiteCrawlApi(Resource):
|
||||
@api.doc("crawl_website")
|
||||
@api.doc(description="Crawl website content")
|
||||
@api.expect(
|
||||
api.model(
|
||||
"WebsiteCrawlRequest",
|
||||
{
|
||||
"provider": fields.String(
|
||||
required=True,
|
||||
description="Crawl provider (firecrawl/watercrawl/jinareader)",
|
||||
enum=["firecrawl", "watercrawl", "jinareader"],
|
||||
),
|
||||
"url": fields.String(required=True, description="URL to crawl"),
|
||||
"options": fields.Raw(required=True, description="Crawl options"),
|
||||
},
|
||||
)
|
||||
)
|
||||
@api.response(200, "Website crawl initiated successfully")
|
||||
@api.response(400, "Invalid crawl parameters")
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@@ -39,7 +58,14 @@ class WebsiteCrawlApi(Resource):
|
||||
return result, 200
|
||||
|
||||
|
||||
@console_ns.route("/website/crawl/status/<string:job_id>")
|
||||
class WebsiteCrawlStatusApi(Resource):
|
||||
@api.doc("get_crawl_status")
|
||||
@api.doc(description="Get website crawl status")
|
||||
@api.doc(params={"job_id": "Crawl job ID", "provider": "Crawl provider (firecrawl/watercrawl/jinareader)"})
|
||||
@api.response(200, "Crawl status retrieved successfully")
|
||||
@api.response(404, "Crawl job not found")
|
||||
@api.response(400, "Invalid provider")
|
||||
@setup_required
|
||||
@login_required
|
||||
@account_initialization_required
|
||||
@@ -62,7 +88,3 @@ class WebsiteCrawlStatusApi(Resource):
|
||||
except Exception as e:
|
||||
raise WebsiteCrawlError(str(e))
|
||||
return result, 200
|
||||
|
||||
|
||||
api.add_resource(WebsiteCrawlApi, "/website/crawl")
|
||||
api.add_resource(WebsiteCrawlStatusApi, "/website/crawl/status/<string:job_id>")
|
||||
|
||||
@@ -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()
|
||||
]
|
||||
|
||||
@@ -1041,6 +1041,8 @@ class TenantService:
|
||||
db.session.add(ta)
|
||||
|
||||
db.session.commit()
|
||||
if dify_config.BILLING_ENABLED:
|
||||
BillingService.clean_billing_info_cache(tenant.id)
|
||||
return ta
|
||||
|
||||
@staticmethod
|
||||
@@ -1199,6 +1201,9 @@ 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"""
|
||||
|
||||
@@ -20,6 +20,7 @@ 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
|
||||
@@ -162,6 +163,9 @@ 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:
|
||||
@@ -337,6 +341,9 @@ 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,6 +5,7 @@ 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
|
||||
|
||||
@@ -173,3 +174,7 @@ 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")
|
||||
|
||||
@@ -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:
|
||||
@@ -114,11 +104,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.")
|
||||
|
||||
body = {"appId": app_id}
|
||||
EnterpriseRequest.send_request("DELETE", "/webapp/clean", json=body)
|
||||
params = {"appId": app_id}
|
||||
EnterpriseRequest.send_request("DELETE", "/webapp/clean", params=params)
|
||||
|
||||
@@ -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