# Conflicts: # api/core/app/entities/queue_entities.py # api/core/workflow/graph_engine/entities/event.pytags/2.0.0-beta.1
| @@ -0,0 +1,19 @@ | |||
| { | |||
| "permissions": { | |||
| "allow": [], | |||
| "deny": [] | |||
| }, | |||
| "env": { | |||
| "__comment": "Environment variables for MCP servers. Override in .claude/settings.local.json with actual values.", | |||
| "GITHUB_PERSONAL_ACCESS_TOKEN": "ghp_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" | |||
| }, | |||
| "enabledMcpjsonServers": [ | |||
| "context7", | |||
| "sequential-thinking", | |||
| "github", | |||
| "fetch", | |||
| "playwright", | |||
| "ide" | |||
| ], | |||
| "enableAllProjectMcpServers": true | |||
| } | |||
| @@ -0,0 +1,34 @@ | |||
| { | |||
| "mcpServers": { | |||
| "context7": { | |||
| "type": "http", | |||
| "url": "https://mcp.context7.com/mcp" | |||
| }, | |||
| "sequential-thinking": { | |||
| "type": "stdio", | |||
| "command": "npx", | |||
| "args": ["-y", "@modelcontextprotocol/server-sequential-thinking"], | |||
| "env": {} | |||
| }, | |||
| "github": { | |||
| "type": "stdio", | |||
| "command": "npx", | |||
| "args": ["-y", "@modelcontextprotocol/server-github"], | |||
| "env": { | |||
| "GITHUB_PERSONAL_ACCESS_TOKEN": "${GITHUB_PERSONAL_ACCESS_TOKEN}" | |||
| } | |||
| }, | |||
| "fetch": { | |||
| "type": "stdio", | |||
| "command": "uvx", | |||
| "args": ["mcp-server-fetch"], | |||
| "env": {} | |||
| }, | |||
| "playwright": { | |||
| "type": "stdio", | |||
| "command": "npx", | |||
| "args": ["-y", "@playwright/mcp@latest"], | |||
| "env": {} | |||
| } | |||
| } | |||
| } | |||
| @@ -86,3 +86,4 @@ pnpm test # Run Jest tests | |||
| ## Project-Specific Conventions | |||
| - All async tasks use Celery with Redis as broker | |||
| - **Internationalization**: Frontend supports multiple languages with English (`web/i18n/en-US/`) as the source. All user-facing text must use i18n keys, no hardcoded strings. Edit corresponding module files in `en-US/` directory for translations. | |||
| @@ -95,18 +95,22 @@ class ChatMessageListApi(Resource): | |||
| .all() | |||
| ) | |||
| # Initialize has_more based on whether we have a full page | |||
| if len(history_messages) == args["limit"]: | |||
| current_page_first_message = history_messages[-1] | |||
| has_more = db.session.scalar( | |||
| select( | |||
| exists().where( | |||
| Message.conversation_id == conversation.id, | |||
| Message.created_at < current_page_first_message.created_at, | |||
| Message.id != current_page_first_message.id, | |||
| # Check if there are more messages before the current page | |||
| has_more = db.session.scalar( | |||
| select( | |||
| exists().where( | |||
| Message.conversation_id == conversation.id, | |||
| Message.created_at < current_page_first_message.created_at, | |||
| Message.id != current_page_first_message.id, | |||
| ) | |||
| ) | |||
| ) | |||
| ) | |||
| else: | |||
| # If we don't have a full page, there are no more messages | |||
| has_more = False | |||
| history_messages = list(reversed(history_messages)) | |||
| @@ -1,6 +1,6 @@ | |||
| from typing import Literal | |||
| from flask_login import current_user # type: ignore | |||
| from flask_login import current_user | |||
| from flask_restx import marshal, reqparse | |||
| from werkzeug.exceptions import NotFound | |||
| @@ -6,7 +6,7 @@ from functools import wraps | |||
| from typing import Optional | |||
| from flask import current_app, request | |||
| from flask_login import user_logged_in # type: ignore | |||
| from flask_login import user_logged_in | |||
| from flask_restx import Resource | |||
| from pydantic import BaseModel | |||
| from sqlalchemy import select, update | |||
| @@ -5,7 +5,7 @@ from flask_restx import Resource, marshal_with, reqparse | |||
| from werkzeug.exceptions import Unauthorized | |||
| from controllers.common import fields | |||
| from controllers.web import api | |||
| from controllers.web import web_ns | |||
| from controllers.web.error import AppUnavailableError | |||
| from controllers.web.wraps import WebApiResource | |||
| from core.app.app_config.common.parameters_mapping import get_parameters_from_feature_dict | |||
| @@ -19,9 +19,22 @@ from services.webapp_auth_service import WebAppAuthService | |||
| logger = logging.getLogger(__name__) | |||
| @web_ns.route("/parameters") | |||
| class AppParameterApi(WebApiResource): | |||
| """Resource for app variables.""" | |||
| @web_ns.doc("Get App Parameters") | |||
| @web_ns.doc(description="Retrieve the parameters for a specific app.") | |||
| @web_ns.doc( | |||
| responses={ | |||
| 200: "Success", | |||
| 400: "Bad Request", | |||
| 401: "Unauthorized", | |||
| 403: "Forbidden", | |||
| 404: "App Not Found", | |||
| 500: "Internal Server Error", | |||
| } | |||
| ) | |||
| @marshal_with(fields.parameters_fields) | |||
| def get(self, app_model: App, end_user): | |||
| """Retrieve app parameters.""" | |||
| @@ -44,13 +57,42 @@ class AppParameterApi(WebApiResource): | |||
| return get_parameters_from_feature_dict(features_dict=features_dict, user_input_form=user_input_form) | |||
| @web_ns.route("/meta") | |||
| class AppMeta(WebApiResource): | |||
| @web_ns.doc("Get App Meta") | |||
| @web_ns.doc(description="Retrieve the metadata for a specific app.") | |||
| @web_ns.doc( | |||
| responses={ | |||
| 200: "Success", | |||
| 400: "Bad Request", | |||
| 401: "Unauthorized", | |||
| 403: "Forbidden", | |||
| 404: "App Not Found", | |||
| 500: "Internal Server Error", | |||
| } | |||
| ) | |||
| def get(self, app_model: App, end_user): | |||
| """Get app meta""" | |||
| return AppService().get_app_meta(app_model) | |||
| @web_ns.route("/webapp/access-mode") | |||
| class AppAccessMode(Resource): | |||
| @web_ns.doc("Get App Access Mode") | |||
| @web_ns.doc(description="Retrieve the access mode for a web application (public or restricted).") | |||
| @web_ns.doc( | |||
| params={ | |||
| "appId": {"description": "Application ID", "type": "string", "required": False}, | |||
| "appCode": {"description": "Application code", "type": "string", "required": False}, | |||
| } | |||
| ) | |||
| @web_ns.doc( | |||
| responses={ | |||
| 200: "Success", | |||
| 400: "Bad Request", | |||
| 500: "Internal Server Error", | |||
| } | |||
| ) | |||
| def get(self): | |||
| parser = reqparse.RequestParser() | |||
| parser.add_argument("appId", type=str, required=False, location="args") | |||
| @@ -74,7 +116,19 @@ class AppAccessMode(Resource): | |||
| return {"accessMode": res.access_mode} | |||
| @web_ns.route("/webapp/permission") | |||
| class AppWebAuthPermission(Resource): | |||
| @web_ns.doc("Check App Permission") | |||
| @web_ns.doc(description="Check if user has permission to access a web application.") | |||
| @web_ns.doc(params={"appId": {"description": "Application ID", "type": "string", "required": True}}) | |||
| @web_ns.doc( | |||
| responses={ | |||
| 200: "Success", | |||
| 400: "Bad Request", | |||
| 401: "Unauthorized", | |||
| 500: "Internal Server Error", | |||
| } | |||
| ) | |||
| def get(self): | |||
| user_id = "visitor" | |||
| try: | |||
| @@ -112,10 +166,3 @@ class AppWebAuthPermission(Resource): | |||
| 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) | |||
| return {"result": res} | |||
| api.add_resource(AppParameterApi, "/parameters") | |||
| api.add_resource(AppMeta, "/meta") | |||
| # webapp auth apis | |||
| api.add_resource(AppAccessMode, "/webapp/access-mode") | |||
| api.add_resource(AppWebAuthPermission, "/webapp/permission") | |||
| @@ -1,6 +1,7 @@ | |||
| import logging | |||
| from flask import request | |||
| from flask_restx import fields, marshal_with, reqparse | |||
| from werkzeug.exceptions import InternalServerError | |||
| import services | |||
| @@ -32,7 +33,26 @@ logger = logging.getLogger(__name__) | |||
| class AudioApi(WebApiResource): | |||
| audio_to_text_response_fields = { | |||
| "text": fields.String, | |||
| } | |||
| @marshal_with(audio_to_text_response_fields) | |||
| @api.doc("Audio to Text") | |||
| @api.doc(description="Convert audio file to text using speech-to-text service.") | |||
| @api.doc( | |||
| responses={ | |||
| 200: "Success", | |||
| 400: "Bad Request", | |||
| 401: "Unauthorized", | |||
| 403: "Forbidden", | |||
| 413: "Audio file too large", | |||
| 415: "Unsupported audio type", | |||
| 500: "Internal Server Error", | |||
| } | |||
| ) | |||
| def post(self, app_model: App, end_user): | |||
| """Convert audio to text""" | |||
| file = request.files["file"] | |||
| try: | |||
| @@ -66,9 +86,25 @@ class AudioApi(WebApiResource): | |||
| class TextApi(WebApiResource): | |||
| def post(self, app_model: App, end_user): | |||
| from flask_restx import reqparse | |||
| text_to_audio_response_fields = { | |||
| "audio_url": fields.String, | |||
| "duration": fields.Float, | |||
| } | |||
| @marshal_with(text_to_audio_response_fields) | |||
| @api.doc("Text to Audio") | |||
| @api.doc(description="Convert text to audio using text-to-speech service.") | |||
| @api.doc( | |||
| responses={ | |||
| 200: "Success", | |||
| 400: "Bad Request", | |||
| 401: "Unauthorized", | |||
| 403: "Forbidden", | |||
| 500: "Internal Server Error", | |||
| } | |||
| ) | |||
| def post(self, app_model: App, end_user): | |||
| """Convert text to audio""" | |||
| try: | |||
| parser = reqparse.RequestParser() | |||
| parser.add_argument("message_id", type=str, required=False, location="json") | |||
| @@ -36,6 +36,32 @@ logger = logging.getLogger(__name__) | |||
| # define completion api for user | |||
| class CompletionApi(WebApiResource): | |||
| @api.doc("Create Completion Message") | |||
| @api.doc(description="Create a completion message for text generation applications.") | |||
| @api.doc( | |||
| params={ | |||
| "inputs": {"description": "Input variables for the completion", "type": "object", "required": True}, | |||
| "query": {"description": "Query text for completion", "type": "string", "required": False}, | |||
| "files": {"description": "Files to be processed", "type": "array", "required": False}, | |||
| "response_mode": { | |||
| "description": "Response mode: blocking or streaming", | |||
| "type": "string", | |||
| "enum": ["blocking", "streaming"], | |||
| "required": False, | |||
| }, | |||
| "retriever_from": {"description": "Source of retriever", "type": "string", "required": False}, | |||
| } | |||
| ) | |||
| @api.doc( | |||
| responses={ | |||
| 200: "Success", | |||
| 400: "Bad Request", | |||
| 401: "Unauthorized", | |||
| 403: "Forbidden", | |||
| 404: "App Not Found", | |||
| 500: "Internal Server Error", | |||
| } | |||
| ) | |||
| def post(self, app_model, end_user): | |||
| if app_model.mode != "completion": | |||
| raise NotCompletionAppError() | |||
| @@ -81,6 +107,19 @@ class CompletionApi(WebApiResource): | |||
| class CompletionStopApi(WebApiResource): | |||
| @api.doc("Stop Completion Message") | |||
| @api.doc(description="Stop a running completion message task.") | |||
| @api.doc(params={"task_id": {"description": "Task ID to stop", "type": "string", "required": True}}) | |||
| @api.doc( | |||
| responses={ | |||
| 200: "Success", | |||
| 400: "Bad Request", | |||
| 401: "Unauthorized", | |||
| 403: "Forbidden", | |||
| 404: "Task Not Found", | |||
| 500: "Internal Server Error", | |||
| } | |||
| ) | |||
| def post(self, app_model, end_user, task_id): | |||
| if app_model.mode != "completion": | |||
| raise NotCompletionAppError() | |||
| @@ -91,6 +130,34 @@ class CompletionStopApi(WebApiResource): | |||
| class ChatApi(WebApiResource): | |||
| @api.doc("Create Chat Message") | |||
| @api.doc(description="Create a chat message for conversational applications.") | |||
| @api.doc( | |||
| params={ | |||
| "inputs": {"description": "Input variables for the chat", "type": "object", "required": True}, | |||
| "query": {"description": "User query/message", "type": "string", "required": True}, | |||
| "files": {"description": "Files to be processed", "type": "array", "required": False}, | |||
| "response_mode": { | |||
| "description": "Response mode: blocking or streaming", | |||
| "type": "string", | |||
| "enum": ["blocking", "streaming"], | |||
| "required": False, | |||
| }, | |||
| "conversation_id": {"description": "Conversation UUID", "type": "string", "required": False}, | |||
| "parent_message_id": {"description": "Parent message UUID", "type": "string", "required": False}, | |||
| "retriever_from": {"description": "Source of retriever", "type": "string", "required": False}, | |||
| } | |||
| ) | |||
| @api.doc( | |||
| responses={ | |||
| 200: "Success", | |||
| 400: "Bad Request", | |||
| 401: "Unauthorized", | |||
| 403: "Forbidden", | |||
| 404: "App Not Found", | |||
| 500: "Internal Server Error", | |||
| } | |||
| ) | |||
| def post(self, app_model, end_user): | |||
| app_mode = AppMode.value_of(app_model.mode) | |||
| if app_mode not in {AppMode.CHAT, AppMode.AGENT_CHAT, AppMode.ADVANCED_CHAT}: | |||
| @@ -141,6 +208,19 @@ class ChatApi(WebApiResource): | |||
| class ChatStopApi(WebApiResource): | |||
| @api.doc("Stop Chat Message") | |||
| @api.doc(description="Stop a running chat message task.") | |||
| @api.doc(params={"task_id": {"description": "Task ID to stop", "type": "string", "required": True}}) | |||
| @api.doc( | |||
| responses={ | |||
| 200: "Success", | |||
| 400: "Bad Request", | |||
| 401: "Unauthorized", | |||
| 403: "Forbidden", | |||
| 404: "Task Not Found", | |||
| 500: "Internal Server Error", | |||
| } | |||
| ) | |||
| def post(self, app_model, end_user, task_id): | |||
| app_mode = AppMode.value_of(app_model.mode) | |||
| if app_mode not in {AppMode.CHAT, AppMode.AGENT_CHAT, AppMode.ADVANCED_CHAT}: | |||
| @@ -1,4 +1,4 @@ | |||
| from flask_restx import marshal_with, reqparse | |||
| from flask_restx import fields, marshal_with, reqparse | |||
| from flask_restx.inputs import int_range | |||
| from sqlalchemy.orm import Session | |||
| from werkzeug.exceptions import NotFound | |||
| @@ -58,6 +58,11 @@ class ConversationListApi(WebApiResource): | |||
| class ConversationApi(WebApiResource): | |||
| delete_response_fields = { | |||
| "result": fields.String, | |||
| } | |||
| @marshal_with(delete_response_fields) | |||
| def delete(self, app_model, end_user, c_id): | |||
| app_mode = AppMode.value_of(app_model.mode) | |||
| if app_mode not in {AppMode.CHAT, AppMode.AGENT_CHAT, AppMode.ADVANCED_CHAT}: | |||
| @@ -94,6 +99,11 @@ class ConversationRenameApi(WebApiResource): | |||
| class ConversationPinApi(WebApiResource): | |||
| pin_response_fields = { | |||
| "result": fields.String, | |||
| } | |||
| @marshal_with(pin_response_fields) | |||
| def patch(self, app_model, end_user, c_id): | |||
| app_mode = AppMode.value_of(app_model.mode) | |||
| if app_mode not in {AppMode.CHAT, AppMode.AGENT_CHAT, AppMode.ADVANCED_CHAT}: | |||
| @@ -110,6 +120,11 @@ class ConversationPinApi(WebApiResource): | |||
| class ConversationUnPinApi(WebApiResource): | |||
| unpin_response_fields = { | |||
| "result": fields.String, | |||
| } | |||
| @marshal_with(unpin_response_fields) | |||
| def patch(self, app_model, end_user, c_id): | |||
| app_mode = AppMode.value_of(app_model.mode) | |||
| if app_mode not in {AppMode.CHAT, AppMode.AGENT_CHAT, AppMode.ADVANCED_CHAT}: | |||
| @@ -1,5 +1,5 @@ | |||
| from flask_restx import Resource, reqparse | |||
| from jwt import InvalidTokenError # type: ignore | |||
| from jwt import InvalidTokenError | |||
| import services | |||
| from controllers.console.auth.error import ( | |||
| @@ -85,6 +85,11 @@ class MessageListApi(WebApiResource): | |||
| class MessageFeedbackApi(WebApiResource): | |||
| feedback_response_fields = { | |||
| "result": fields.String, | |||
| } | |||
| @marshal_with(feedback_response_fields) | |||
| def post(self, app_model, end_user, message_id): | |||
| message_id = str(message_id) | |||
| @@ -152,6 +157,11 @@ class MessageMoreLikeThisApi(WebApiResource): | |||
| class MessageSuggestedQuestionApi(WebApiResource): | |||
| suggested_questions_response_fields = { | |||
| "data": fields.List(fields.String), | |||
| } | |||
| @marshal_with(suggested_questions_response_fields) | |||
| def get(self, app_model, end_user, message_id): | |||
| app_mode = AppMode.value_of(app_model.mode) | |||
| if app_mode not in {AppMode.CHAT, AppMode.AGENT_CHAT, AppMode.ADVANCED_CHAT}: | |||
| @@ -30,6 +30,10 @@ class SavedMessageListApi(WebApiResource): | |||
| "data": fields.List(fields.Nested(message_fields)), | |||
| } | |||
| post_response_fields = { | |||
| "result": fields.String, | |||
| } | |||
| @marshal_with(saved_message_infinite_scroll_pagination_fields) | |||
| def get(self, app_model, end_user): | |||
| if app_model.mode != "completion": | |||
| @@ -42,6 +46,7 @@ class SavedMessageListApi(WebApiResource): | |||
| return SavedMessageService.pagination_by_last_id(app_model, end_user, args["last_id"], args["limit"]) | |||
| @marshal_with(post_response_fields) | |||
| def post(self, app_model, end_user): | |||
| if app_model.mode != "completion": | |||
| raise NotCompletionAppError() | |||
| @@ -59,6 +64,11 @@ class SavedMessageListApi(WebApiResource): | |||
| class SavedMessageApi(WebApiResource): | |||
| delete_response_fields = { | |||
| "result": fields.String, | |||
| } | |||
| @marshal_with(delete_response_fields) | |||
| def delete(self, app_model, end_user, message_id): | |||
| message_id = str(message_id) | |||
| @@ -53,6 +53,18 @@ class AppSiteApi(WebApiResource): | |||
| "custom_config": fields.Raw(attribute="custom_config"), | |||
| } | |||
| @api.doc("Get App Site Info") | |||
| @api.doc(description="Retrieve app site information and configuration.") | |||
| @api.doc( | |||
| responses={ | |||
| 200: "Success", | |||
| 400: "Bad Request", | |||
| 401: "Unauthorized", | |||
| 403: "Forbidden", | |||
| 404: "App Not Found", | |||
| 500: "Internal Server Error", | |||
| } | |||
| ) | |||
| @marshal_with(app_fields) | |||
| def get(self, app_model, end_user): | |||
| """Retrieve app site info.""" | |||
| @@ -31,6 +31,24 @@ logger = logging.getLogger(__name__) | |||
| class WorkflowRunApi(WebApiResource): | |||
| @api.doc("Run Workflow") | |||
| @api.doc(description="Execute a workflow with provided inputs and files.") | |||
| @api.doc( | |||
| params={ | |||
| "inputs": {"description": "Input variables for the workflow", "type": "object", "required": True}, | |||
| "files": {"description": "Files to be processed by the workflow", "type": "array", "required": False}, | |||
| } | |||
| ) | |||
| @api.doc( | |||
| responses={ | |||
| 200: "Success", | |||
| 400: "Bad Request", | |||
| 401: "Unauthorized", | |||
| 403: "Forbidden", | |||
| 404: "App Not Found", | |||
| 500: "Internal Server Error", | |||
| } | |||
| ) | |||
| def post(self, app_model: App, end_user: EndUser): | |||
| """ | |||
| Run workflow | |||
| @@ -68,6 +86,23 @@ class WorkflowRunApi(WebApiResource): | |||
| class WorkflowTaskStopApi(WebApiResource): | |||
| @api.doc("Stop Workflow Task") | |||
| @api.doc(description="Stop a running workflow task.") | |||
| @api.doc( | |||
| params={ | |||
| "task_id": {"description": "Task ID to stop", "type": "string", "required": True}, | |||
| } | |||
| ) | |||
| @api.doc( | |||
| responses={ | |||
| 200: "Success", | |||
| 400: "Bad Request", | |||
| 401: "Unauthorized", | |||
| 403: "Forbidden", | |||
| 404: "Task Not Found", | |||
| 500: "Internal Server Error", | |||
| } | |||
| ) | |||
| def post(self, app_model: App, end_user: EndUser, task_id: str): | |||
| """ | |||
| Stop workflow task | |||
| @@ -169,7 +169,7 @@ class QueueLoopNextEvent(AppQueueEvent): | |||
| parent_parallel_start_node_id: Optional[str] = None | |||
| """parent parallel start node id if node is in parallel""" | |||
| parallel_mode_run_id: Optional[str] = None | |||
| """iteratoin run in parallel mode run id""" | |||
| """iteration run in parallel mode run id""" | |||
| node_run_index: int | |||
| output: Optional[Any] = None # output for the current loop | |||
| @@ -1,8 +1,8 @@ | |||
| class TaskPipilineError(ValueError): | |||
| class TaskPipelineError(ValueError): | |||
| pass | |||
| class RecordNotFoundError(TaskPipilineError): | |||
| class RecordNotFoundError(TaskPipelineError): | |||
| def __init__(self, record_name: str, record_id: str): | |||
| super().__init__(f"{record_name} with id {record_id} not found") | |||
| @@ -43,7 +43,7 @@ class GPT2Tokenizer: | |||
| except Exception: | |||
| from os.path import abspath, dirname, join | |||
| from transformers import GPT2Tokenizer as TransformerGPT2Tokenizer # type: ignore | |||
| from transformers import GPT2Tokenizer as TransformerGPT2Tokenizer | |||
| base_path = abspath(__file__) | |||
| gpt2_tokenizer_path = join(dirname(base_path), "gpt2") | |||
| @@ -332,7 +332,7 @@ class OpsTraceManager: | |||
| except KeyError: | |||
| raise ValueError(f"Invalid tracing provider: {tracing_provider}") | |||
| else: | |||
| if tracing_provider is not None: | |||
| if tracing_provider is None: | |||
| raise ValueError(f"Invalid tracing provider: {tracing_provider}") | |||
| app_config: Optional[App] = db.session.query(App).where(App.id == app_id).first() | |||
| @@ -375,16 +375,16 @@ Here is the extra instruction you need to follow: | |||
| # merge lines into messages with max tokens | |||
| messages: list[str] = [] | |||
| for i in new_lines: # type: ignore | |||
| for line in new_lines: | |||
| if len(messages) == 0: | |||
| messages.append(i) # type: ignore | |||
| messages.append(line) | |||
| else: | |||
| if len(messages[-1]) + len(i) < max_tokens * 0.5: # type: ignore | |||
| messages[-1] += i # type: ignore | |||
| if get_prompt_tokens(messages[-1] + i) > max_tokens * 0.7: # type: ignore | |||
| messages.append(i) # type: ignore | |||
| if len(messages[-1]) + len(line) < max_tokens * 0.5: | |||
| messages[-1] += line | |||
| if get_prompt_tokens(messages[-1] + line) > max_tokens * 0.7: | |||
| messages.append(line) | |||
| else: | |||
| messages[-1] += i # type: ignore | |||
| messages[-1] += line | |||
| summaries = [] | |||
| for i in range(len(messages)): | |||
| @@ -3,8 +3,8 @@ import uuid | |||
| from contextlib import contextmanager | |||
| from typing import Any | |||
| import psycopg2.extras # type: ignore | |||
| import psycopg2.pool # type: ignore | |||
| import psycopg2.extras | |||
| import psycopg2.pool | |||
| from pydantic import BaseModel, model_validator | |||
| from core.rag.models.document import Document | |||
| @@ -3,8 +3,8 @@ import uuid | |||
| from contextlib import contextmanager | |||
| from typing import Any | |||
| import psycopg2.extras # type: ignore | |||
| import psycopg2.pool # type: ignore | |||
| import psycopg2.extras | |||
| import psycopg2.pool | |||
| from pydantic import BaseModel, model_validator | |||
| from configs import dify_config | |||
| @@ -48,7 +48,7 @@ class OpenSearchConfig(BaseModel): | |||
| return values | |||
| def create_aws_managed_iam_auth(self) -> Urllib3AWSV4SignerAuth: | |||
| import boto3 # type: ignore | |||
| import boto3 | |||
| return Urllib3AWSV4SignerAuth( | |||
| credentials=boto3.Session().get_credentials(), | |||
| @@ -6,8 +6,8 @@ from contextlib import contextmanager | |||
| from typing import Any | |||
| import psycopg2.errors | |||
| import psycopg2.extras # type: ignore | |||
| import psycopg2.pool # type: ignore | |||
| import psycopg2.extras | |||
| import psycopg2.pool | |||
| from pydantic import BaseModel, model_validator | |||
| from configs import dify_config | |||
| @@ -3,8 +3,8 @@ import uuid | |||
| from contextlib import contextmanager | |||
| from typing import Any | |||
| import psycopg2.extras # type: ignore | |||
| import psycopg2.pool # type: ignore | |||
| import psycopg2.extras | |||
| import psycopg2.pool | |||
| from pydantic import BaseModel, model_validator | |||
| from configs import dify_config | |||
| @@ -3,7 +3,7 @@ import os | |||
| import uuid | |||
| from collections.abc import Generator, Iterable, Sequence | |||
| from itertools import islice | |||
| from typing import TYPE_CHECKING, Any, Optional, Union, cast | |||
| from typing import TYPE_CHECKING, Any, Optional, Union | |||
| import qdrant_client | |||
| import requests | |||
| @@ -398,7 +398,6 @@ class TidbOnQdrantVector(BaseVector): | |||
| def _reload_if_needed(self): | |||
| if isinstance(self._client, QdrantLocal): | |||
| self._client = cast(QdrantLocal, self._client) | |||
| self._client._load() | |||
| @classmethod | |||
| @@ -4,7 +4,7 @@ import os | |||
| from typing import Optional, cast | |||
| import pandas as pd | |||
| from openpyxl import load_workbook # type: ignore | |||
| from openpyxl import load_workbook | |||
| from core.rag.extractor.extractor_base import BaseExtractor | |||
| from core.rag.models.document import Document | |||
| @@ -73,8 +73,8 @@ class ExtractProcessor: | |||
| suffix = "." + match.group(1) | |||
| else: | |||
| suffix = "" | |||
| # FIXME mypy: Cannot determine type of 'tempfile._get_candidate_names' better not use it here | |||
| file_path = f"{temp_dir}/{next(tempfile._get_candidate_names())}{suffix}" # type: ignore | |||
| # https://stackoverflow.com/questions/26541416/generate-temporary-file-names-without-creating-actual-file-in-python#comment90414256_26541521 | |||
| file_path = f"{temp_dir}/{tempfile.gettempdir()}{suffix}" | |||
| Path(file_path).write_bytes(response.content) | |||
| extract_setting = ExtractSetting(datasource_type="upload_file", document_model="text_model") | |||
| if return_text: | |||
| @@ -1,6 +1,6 @@ | |||
| """Abstract interface for document loader implementations.""" | |||
| from bs4 import BeautifulSoup # type: ignore | |||
| from bs4 import BeautifulSoup | |||
| from core.rag.extractor.extractor_base import BaseExtractor | |||
| from core.rag.models.document import Document | |||
| @@ -3,7 +3,7 @@ import contextlib | |||
| import logging | |||
| from typing import Optional | |||
| from bs4 import BeautifulSoup # type: ignore | |||
| from bs4 import BeautifulSoup | |||
| from core.rag.extractor.extractor_base import BaseExtractor | |||
| from core.rag.models.document import Document | |||
| @@ -144,7 +144,7 @@ class TextSplitter(BaseDocumentTransformer, ABC): | |||
| def from_huggingface_tokenizer(cls, tokenizer: Any, **kwargs: Any) -> TextSplitter: | |||
| """Text splitter that uses HuggingFace tokenizer to count length.""" | |||
| try: | |||
| from transformers import PreTrainedTokenizerBase # type: ignore | |||
| from transformers import PreTrainedTokenizerBase | |||
| if not isinstance(tokenizer, PreTrainedTokenizerBase): | |||
| raise ValueError("Tokenizer received was not an instance of PreTrainedTokenizerBase") | |||
| @@ -6,7 +6,7 @@ from typing import Optional | |||
| from flask import request | |||
| from requests import get | |||
| from yaml import YAMLError, safe_load # type: ignore | |||
| from yaml import YAMLError, safe_load | |||
| from core.tools.entities.common_entities import I18nObject | |||
| from core.tools.entities.tool_bundle import ApiToolBundle | |||
| @@ -514,14 +514,14 @@ def _extract_text_from_excel(file_content: bytes) -> str: | |||
| df.dropna(how="all", inplace=True) | |||
| # Combine multi-line text in each cell into a single line | |||
| df = df.applymap(lambda x: " ".join(str(x).splitlines()) if isinstance(x, str) else x) # type: ignore | |||
| df = df.map(lambda x: " ".join(str(x).splitlines()) if isinstance(x, str) else x) | |||
| # Combine multi-line text in column names into a single line | |||
| df.columns = pd.Index([" ".join(str(col).splitlines()) for col in df.columns]) | |||
| # Manually construct the Markdown table | |||
| markdown_table += _construct_markdown_table(df) + "\n\n" | |||
| except Exception as e: | |||
| except Exception: | |||
| continue | |||
| return markdown_table | |||
| except Exception as e: | |||
| @@ -5,7 +5,7 @@ from dify_app import DifyApp | |||
| def init_app(app: DifyApp): | |||
| # register blueprint routers | |||
| from flask_cors import CORS # type: ignore | |||
| from flask_cors import CORS | |||
| from controllers.console import bp as console_app_bp | |||
| from controllers.files import bp as files_bp | |||
| @@ -9,7 +9,7 @@ from typing import Union | |||
| import flask | |||
| from celery.signals import worker_init | |||
| from flask_login import user_loaded_from_request, user_logged_in # type: ignore | |||
| from flask_login import user_loaded_from_request, user_logged_in | |||
| from configs import dify_config | |||
| from dify_app import DifyApp | |||
| @@ -1,9 +1,9 @@ | |||
| import logging | |||
| from collections.abc import Generator | |||
| import boto3 # type: ignore | |||
| from botocore.client import Config # type: ignore | |||
| from botocore.exceptions import ClientError # type: ignore | |||
| import boto3 | |||
| from botocore.client import Config | |||
| from botocore.exceptions import ClientError | |||
| from configs import dify_config | |||
| from extensions.storage.base_storage import BaseStorage | |||
| @@ -4,7 +4,7 @@ from datetime import datetime | |||
| from typing import Optional, cast | |||
| import sqlalchemy as sa | |||
| from flask_login import UserMixin # type: ignore | |||
| from flask_login import UserMixin | |||
| from sqlalchemy import DateTime, String, func, select | |||
| from sqlalchemy.orm import Mapped, mapped_column, reconstructor | |||
| @@ -67,7 +67,7 @@ dependencies = [ | |||
| "pydantic~=2.11.4", | |||
| "pydantic-extra-types~=2.10.3", | |||
| "pydantic-settings~=2.9.1", | |||
| "pyjwt~=2.8.0", | |||
| "pyjwt~=2.10.1", | |||
| "pypdfium2==4.30.0", | |||
| "python-docx~=1.1.0", | |||
| "python-dotenv==1.0.1", | |||
| @@ -180,7 +180,7 @@ storage = [ | |||
| "google-cloud-storage==2.16.0", | |||
| "opendal~=0.45.16", | |||
| "oss2==2.18.5", | |||
| "supabase~=2.8.1", | |||
| "supabase~=2.18.1", | |||
| "tos~=2.7.1", | |||
| ] | |||
| @@ -1,7 +1,7 @@ | |||
| import enum | |||
| import secrets | |||
| from datetime import UTC, datetime, timedelta | |||
| from typing import Any, Optional, cast | |||
| from typing import Any, Optional | |||
| from werkzeug.exceptions import NotFound, Unauthorized | |||
| @@ -42,7 +42,7 @@ class WebAppAuthService: | |||
| if account.password is None or not compare_password(password, account.password, account.password_salt): | |||
| raise AccountPasswordError("Invalid email or password.") | |||
| return cast(Account, account) | |||
| return account | |||
| @classmethod | |||
| def login(cls, account: Account) -> str: | |||
| @@ -2,7 +2,7 @@ import logging | |||
| import time | |||
| import click | |||
| from celery import shared_task # type: ignore | |||
| from celery import shared_task | |||
| from extensions.ext_database import db | |||
| from models import ConversationVariable | |||
| @@ -3,15 +3,12 @@ from collections.abc import Callable | |||
| import pytest | |||
| # import monkeypatch | |||
| from _pytest.monkeypatch import MonkeyPatch | |||
| from core.plugin.impl.model import PluginModelClient | |||
| from tests.integration_tests.model_runtime.__mock.plugin_model import MockModelClass | |||
| def mock_plugin_daemon( | |||
| monkeypatch: MonkeyPatch, | |||
| monkeypatch: pytest.MonkeyPatch, | |||
| ) -> Callable[[], None]: | |||
| """ | |||
| mock openai module | |||
| @@ -34,7 +31,7 @@ MOCK = os.getenv("MOCK_SWITCH", "false").lower() == "true" | |||
| @pytest.fixture | |||
| def setup_model_mock(monkeypatch): | |||
| def setup_model_mock(monkeypatch: pytest.MonkeyPatch): | |||
| if MOCK: | |||
| unpatch = mock_plugin_daemon(monkeypatch) | |||
| @@ -3,7 +3,6 @@ from typing import Literal | |||
| import pytest | |||
| import requests | |||
| from _pytest.monkeypatch import MonkeyPatch | |||
| from core.plugin.entities.plugin_daemon import PluginDaemonBasicResponse | |||
| from core.tools.entities.common_entities import I18nObject | |||
| @@ -53,7 +52,7 @@ MOCK_SWITCH = os.getenv("MOCK_SWITCH", "false").lower() == "true" | |||
| @pytest.fixture | |||
| def setup_http_mock(request, monkeypatch: MonkeyPatch): | |||
| def setup_http_mock(request, monkeypatch: pytest.MonkeyPatch): | |||
| if MOCK_SWITCH: | |||
| monkeypatch.setattr(requests, "request", MockedHttp.requests_request) | |||
| @@ -3,7 +3,6 @@ from typing import Literal | |||
| import httpx | |||
| import pytest | |||
| from _pytest.monkeypatch import MonkeyPatch | |||
| from core.helper import ssrf_proxy | |||
| @@ -30,7 +29,7 @@ class MockedHttp: | |||
| @pytest.fixture | |||
| def setup_http_mock(request, monkeypatch: MonkeyPatch): | |||
| def setup_http_mock(request, monkeypatch: pytest.MonkeyPatch): | |||
| monkeypatch.setattr(ssrf_proxy, "make_request", MockedHttp.httpx_request) | |||
| yield | |||
| monkeypatch.undo() | |||
| @@ -1,6 +1,6 @@ | |||
| import time | |||
| import psycopg2 # type: ignore | |||
| import psycopg2 | |||
| from core.rag.datasource.vdb.opengauss.opengauss import OpenGauss, OpenGaussConfig | |||
| from tests.integration_tests.vdb.test_vector_store import ( | |||
| @@ -45,6 +45,7 @@ class DifyTestContainers: | |||
| self.postgres: Optional[PostgresContainer] = None | |||
| self.redis: Optional[RedisContainer] = None | |||
| self.dify_sandbox: Optional[DockerContainer] = None | |||
| self.dify_plugin_daemon: Optional[DockerContainer] = None | |||
| self._containers_started = False | |||
| logger.info("DifyTestContainers initialized - ready to manage test containers") | |||
| @@ -110,6 +111,25 @@ class DifyTestContainers: | |||
| except Exception as e: | |||
| logger.warning("Failed to install uuid-ossp extension: %s", e) | |||
| # Create plugin database for dify-plugin-daemon | |||
| logger.info("Creating plugin database...") | |||
| try: | |||
| conn = psycopg2.connect( | |||
| host=db_host, | |||
| port=db_port, | |||
| user=self.postgres.username, | |||
| password=self.postgres.password, | |||
| database=self.postgres.dbname, | |||
| ) | |||
| conn.autocommit = True | |||
| cursor = conn.cursor() | |||
| cursor.execute("CREATE DATABASE dify_plugin;") | |||
| cursor.close() | |||
| conn.close() | |||
| logger.info("Plugin database created successfully") | |||
| except Exception as e: | |||
| logger.warning("Failed to create plugin database: %s", e) | |||
| # Set up storage environment variables | |||
| os.environ["STORAGE_TYPE"] = "opendal" | |||
| os.environ["OPENDAL_SCHEME"] = "fs" | |||
| @@ -151,6 +171,62 @@ class DifyTestContainers: | |||
| wait_for_logs(self.dify_sandbox, "config init success", timeout=60) | |||
| logger.info("Dify Sandbox container is ready and accepting connections") | |||
| # Start Dify Plugin Daemon container for plugin management | |||
| # Dify Plugin Daemon provides plugin lifecycle management and execution | |||
| logger.info("Initializing Dify Plugin Daemon container...") | |||
| self.dify_plugin_daemon = DockerContainer(image="langgenius/dify-plugin-daemon:0.2.0-local") | |||
| self.dify_plugin_daemon.with_exposed_ports(5002) | |||
| self.dify_plugin_daemon.env = { | |||
| "DB_HOST": db_host, | |||
| "DB_PORT": str(db_port), | |||
| "DB_USERNAME": self.postgres.username, | |||
| "DB_PASSWORD": self.postgres.password, | |||
| "DB_DATABASE": "dify_plugin", | |||
| "REDIS_HOST": redis_host, | |||
| "REDIS_PORT": str(redis_port), | |||
| "REDIS_PASSWORD": "", | |||
| "SERVER_PORT": "5002", | |||
| "SERVER_KEY": "test_plugin_daemon_key", | |||
| "MAX_PLUGIN_PACKAGE_SIZE": "52428800", | |||
| "PPROF_ENABLED": "false", | |||
| "DIFY_INNER_API_URL": f"http://{db_host}:5001", | |||
| "DIFY_INNER_API_KEY": "test_inner_api_key", | |||
| "PLUGIN_REMOTE_INSTALLING_HOST": "0.0.0.0", | |||
| "PLUGIN_REMOTE_INSTALLING_PORT": "5003", | |||
| "PLUGIN_WORKING_PATH": "/app/storage/cwd", | |||
| "FORCE_VERIFYING_SIGNATURE": "false", | |||
| "PYTHON_ENV_INIT_TIMEOUT": "120", | |||
| "PLUGIN_MAX_EXECUTION_TIMEOUT": "600", | |||
| "PLUGIN_STDIO_BUFFER_SIZE": "1024", | |||
| "PLUGIN_STDIO_MAX_BUFFER_SIZE": "5242880", | |||
| "PLUGIN_STORAGE_TYPE": "local", | |||
| "PLUGIN_STORAGE_LOCAL_ROOT": "/app/storage", | |||
| "PLUGIN_INSTALLED_PATH": "plugin", | |||
| "PLUGIN_PACKAGE_CACHE_PATH": "plugin_packages", | |||
| "PLUGIN_MEDIA_CACHE_PATH": "assets", | |||
| } | |||
| try: | |||
| self.dify_plugin_daemon.start() | |||
| plugin_daemon_host = self.dify_plugin_daemon.get_container_host_ip() | |||
| plugin_daemon_port = self.dify_plugin_daemon.get_exposed_port(5002) | |||
| os.environ["PLUGIN_DAEMON_URL"] = f"http://{plugin_daemon_host}:{plugin_daemon_port}" | |||
| os.environ["PLUGIN_DAEMON_KEY"] = "test_plugin_daemon_key" | |||
| logger.info( | |||
| "Dify Plugin Daemon container started successfully - Host: %s, Port: %s", | |||
| plugin_daemon_host, | |||
| plugin_daemon_port, | |||
| ) | |||
| # Wait for Dify Plugin Daemon to be ready | |||
| logger.info("Waiting for Dify Plugin Daemon to be ready to accept connections...") | |||
| wait_for_logs(self.dify_plugin_daemon, "start plugin manager daemon", timeout=60) | |||
| logger.info("Dify Plugin Daemon container is ready and accepting connections") | |||
| except Exception as e: | |||
| logger.warning("Failed to start Dify Plugin Daemon container: %s", e) | |||
| logger.info("Continuing without plugin daemon - some tests may be limited") | |||
| self.dify_plugin_daemon = None | |||
| self._containers_started = True | |||
| logger.info("All test containers started successfully") | |||
| @@ -166,7 +242,7 @@ class DifyTestContainers: | |||
| return | |||
| logger.info("Stopping and cleaning up test containers...") | |||
| containers = [self.redis, self.postgres, self.dify_sandbox] | |||
| containers = [self.redis, self.postgres, self.dify_sandbox, self.dify_plugin_daemon] | |||
| for container in containers: | |||
| if container: | |||
| try: | |||
| @@ -8,7 +8,7 @@ from yarl import URL | |||
| from configs.app_config import DifyConfig | |||
| def test_dify_config(monkeypatch): | |||
| def test_dify_config(monkeypatch: pytest.MonkeyPatch): | |||
| # clear system environment variables | |||
| os.environ.clear() | |||
| @@ -48,7 +48,7 @@ def test_dify_config(monkeypatch): | |||
| # NOTE: If there is a `.env` file in your Workspace, this test might not succeed as expected. | |||
| # This is due to `pymilvus` loading all the variables from the `.env` file into `os.environ`. | |||
| def test_flask_configs(monkeypatch): | |||
| def test_flask_configs(monkeypatch: pytest.MonkeyPatch): | |||
| flask_app = Flask("app") | |||
| # clear system environment variables | |||
| os.environ.clear() | |||
| @@ -101,7 +101,7 @@ def test_flask_configs(monkeypatch): | |||
| assert str(URL(str(config["CODE_EXECUTION_ENDPOINT"])) / "v1") == "http://127.0.0.1:8194/v1" | |||
| def test_inner_api_config_exist(monkeypatch): | |||
| def test_inner_api_config_exist(monkeypatch: pytest.MonkeyPatch): | |||
| # Set environment variables using monkeypatch | |||
| monkeypatch.setenv("CONSOLE_API_URL", "https://example.com") | |||
| monkeypatch.setenv("CONSOLE_WEB_URL", "https://example.com") | |||
| @@ -119,7 +119,7 @@ def test_inner_api_config_exist(monkeypatch): | |||
| assert len(config.INNER_API_KEY) > 0 | |||
| def test_db_extras_options_merging(monkeypatch): | |||
| def test_db_extras_options_merging(monkeypatch: pytest.MonkeyPatch): | |||
| """Test that DB_EXTRAS options are properly merged with default timezone setting""" | |||
| # Set environment variables | |||
| monkeypatch.setenv("DB_USERNAME", "postgres") | |||
| @@ -164,7 +164,13 @@ def test_db_extras_options_merging(monkeypatch): | |||
| ], | |||
| ) | |||
| def test_celery_broker_url_with_special_chars_password( | |||
| monkeypatch, broker_url, expected_host, expected_port, expected_username, expected_password, expected_db | |||
| monkeypatch: pytest.MonkeyPatch, | |||
| broker_url, | |||
| expected_host, | |||
| expected_port, | |||
| expected_username, | |||
| expected_password, | |||
| expected_db, | |||
| ): | |||
| """Test that CELERY_BROKER_URL with various formats are handled correctly.""" | |||
| from kombu.utils.url import parse_url | |||
| @@ -0,0 +1,313 @@ | |||
| from collections.abc import Generator | |||
| from unittest.mock import Mock, patch | |||
| import pytest | |||
| from extensions.storage.supabase_storage import SupabaseStorage | |||
| class TestSupabaseStorage: | |||
| """Test suite for SupabaseStorage class.""" | |||
| def test_init_success_with_all_config(self): | |||
| """Test successful initialization when all required config is provided.""" | |||
| with patch("extensions.storage.supabase_storage.dify_config") as mock_config: | |||
| mock_config.SUPABASE_URL = "https://test.supabase.co" | |||
| mock_config.SUPABASE_API_KEY = "test-api-key" | |||
| mock_config.SUPABASE_BUCKET_NAME = "test-bucket" | |||
| with patch("extensions.storage.supabase_storage.Client") as mock_client_class: | |||
| mock_client = Mock() | |||
| mock_client_class.return_value = mock_client | |||
| # Mock bucket_exists to return True so create_bucket is not called | |||
| with patch.object(SupabaseStorage, "bucket_exists", return_value=True): | |||
| storage = SupabaseStorage() | |||
| assert storage.bucket_name == "test-bucket" | |||
| mock_client_class.assert_called_once_with( | |||
| supabase_url="https://test.supabase.co", supabase_key="test-api-key" | |||
| ) | |||
| def test_init_raises_error_when_url_missing(self): | |||
| """Test initialization raises ValueError when SUPABASE_URL is None.""" | |||
| with patch("extensions.storage.supabase_storage.dify_config") as mock_config: | |||
| mock_config.SUPABASE_URL = None | |||
| mock_config.SUPABASE_API_KEY = "test-api-key" | |||
| mock_config.SUPABASE_BUCKET_NAME = "test-bucket" | |||
| with pytest.raises(ValueError, match="SUPABASE_URL is not set"): | |||
| SupabaseStorage() | |||
| def test_init_raises_error_when_api_key_missing(self): | |||
| """Test initialization raises ValueError when SUPABASE_API_KEY is None.""" | |||
| with patch("extensions.storage.supabase_storage.dify_config") as mock_config: | |||
| mock_config.SUPABASE_URL = "https://test.supabase.co" | |||
| mock_config.SUPABASE_API_KEY = None | |||
| mock_config.SUPABASE_BUCKET_NAME = "test-bucket" | |||
| with pytest.raises(ValueError, match="SUPABASE_API_KEY is not set"): | |||
| SupabaseStorage() | |||
| def test_init_raises_error_when_bucket_name_missing(self): | |||
| """Test initialization raises ValueError when SUPABASE_BUCKET_NAME is None.""" | |||
| with patch("extensions.storage.supabase_storage.dify_config") as mock_config: | |||
| mock_config.SUPABASE_URL = "https://test.supabase.co" | |||
| mock_config.SUPABASE_API_KEY = "test-api-key" | |||
| mock_config.SUPABASE_BUCKET_NAME = None | |||
| with pytest.raises(ValueError, match="SUPABASE_BUCKET_NAME is not set"): | |||
| SupabaseStorage() | |||
| def test_create_bucket_when_not_exists(self): | |||
| """Test create_bucket creates bucket when it doesn't exist.""" | |||
| with patch("extensions.storage.supabase_storage.dify_config") as mock_config: | |||
| mock_config.SUPABASE_URL = "https://test.supabase.co" | |||
| mock_config.SUPABASE_API_KEY = "test-api-key" | |||
| mock_config.SUPABASE_BUCKET_NAME = "test-bucket" | |||
| with patch("extensions.storage.supabase_storage.Client") as mock_client_class: | |||
| mock_client = Mock() | |||
| mock_client_class.return_value = mock_client | |||
| with patch.object(SupabaseStorage, "bucket_exists", return_value=False): | |||
| storage = SupabaseStorage() | |||
| mock_client.storage.create_bucket.assert_called_once_with(id="test-bucket", name="test-bucket") | |||
| def test_create_bucket_when_exists(self): | |||
| """Test create_bucket does not create bucket when it already exists.""" | |||
| with patch("extensions.storage.supabase_storage.dify_config") as mock_config: | |||
| mock_config.SUPABASE_URL = "https://test.supabase.co" | |||
| mock_config.SUPABASE_API_KEY = "test-api-key" | |||
| mock_config.SUPABASE_BUCKET_NAME = "test-bucket" | |||
| with patch("extensions.storage.supabase_storage.Client") as mock_client_class: | |||
| mock_client = Mock() | |||
| mock_client_class.return_value = mock_client | |||
| with patch.object(SupabaseStorage, "bucket_exists", return_value=True): | |||
| storage = SupabaseStorage() | |||
| mock_client.storage.create_bucket.assert_not_called() | |||
| @pytest.fixture | |||
| def storage_with_mock_client(self): | |||
| """Fixture providing SupabaseStorage with mocked client.""" | |||
| with patch("extensions.storage.supabase_storage.dify_config") as mock_config: | |||
| mock_config.SUPABASE_URL = "https://test.supabase.co" | |||
| mock_config.SUPABASE_API_KEY = "test-api-key" | |||
| mock_config.SUPABASE_BUCKET_NAME = "test-bucket" | |||
| with patch("extensions.storage.supabase_storage.Client") as mock_client_class: | |||
| mock_client = Mock() | |||
| mock_client_class.return_value = mock_client | |||
| with patch.object(SupabaseStorage, "bucket_exists", return_value=True): | |||
| storage = SupabaseStorage() | |||
| # Create fresh mock for each test | |||
| mock_client.reset_mock() | |||
| yield storage, mock_client | |||
| def test_save(self, storage_with_mock_client): | |||
| """Test save calls client.storage.from_(bucket).upload(path, data).""" | |||
| storage, mock_client = storage_with_mock_client | |||
| filename = "test.txt" | |||
| data = b"test data" | |||
| storage.save(filename, data) | |||
| mock_client.storage.from_.assert_called_once_with("test-bucket") | |||
| mock_client.storage.from_().upload.assert_called_once_with(filename, data) | |||
| def test_load_once_returns_bytes(self, storage_with_mock_client): | |||
| """Test load_once returns bytes.""" | |||
| storage, mock_client = storage_with_mock_client | |||
| expected_data = b"test content" | |||
| mock_client.storage.from_().download.return_value = expected_data | |||
| result = storage.load_once("test.txt") | |||
| assert result == expected_data | |||
| # Verify the correct calls were made | |||
| assert "test-bucket" in [call[0][0] for call in mock_client.storage.from_.call_args_list if call[0]] | |||
| mock_client.storage.from_().download.assert_called_with("test.txt") | |||
| def test_load_stream_yields_chunks(self, storage_with_mock_client): | |||
| """Test load_stream yields chunks.""" | |||
| storage, mock_client = storage_with_mock_client | |||
| test_data = b"test content for streaming" | |||
| mock_client.storage.from_().download.return_value = test_data | |||
| result = storage.load_stream("test.txt") | |||
| assert isinstance(result, Generator) | |||
| # Collect all chunks | |||
| chunks = list(result) | |||
| # Verify chunks contain the expected data | |||
| assert b"".join(chunks) == test_data | |||
| # Verify the correct calls were made | |||
| assert "test-bucket" in [call[0][0] for call in mock_client.storage.from_.call_args_list if call[0]] | |||
| mock_client.storage.from_().download.assert_called_with("test.txt") | |||
| def test_download_writes_bytes_to_disk(self, storage_with_mock_client, tmp_path): | |||
| """Test download writes expected bytes to disk.""" | |||
| storage, mock_client = storage_with_mock_client | |||
| test_data = b"test file content" | |||
| mock_client.storage.from_().download.return_value = test_data | |||
| target_file = tmp_path / "downloaded_file.txt" | |||
| storage.download("test.txt", str(target_file)) | |||
| # Verify file was written with correct content | |||
| assert target_file.read_bytes() == test_data | |||
| # Verify the correct calls were made | |||
| assert "test-bucket" in [call[0][0] for call in mock_client.storage.from_.call_args_list if call[0]] | |||
| mock_client.storage.from_().download.assert_called_with("test.txt") | |||
| def test_exists_with_list_containing_items(self, storage_with_mock_client): | |||
| """Test exists returns True when list() returns items (using len() > 0).""" | |||
| storage, mock_client = storage_with_mock_client | |||
| # Mock list return with special object that has count() method | |||
| mock_list_result = Mock() | |||
| mock_list_result.count.return_value = 1 | |||
| mock_client.storage.from_().list.return_value = mock_list_result | |||
| result = storage.exists("test.txt") | |||
| assert result is True | |||
| # from_ gets called during init too, so just check it was called with the right bucket | |||
| assert "test-bucket" in [call[0][0] for call in mock_client.storage.from_.call_args_list if call[0]] | |||
| mock_client.storage.from_().list.assert_called_with("test.txt") | |||
| def test_exists_with_count_method_greater_than_zero(self, storage_with_mock_client): | |||
| """Test exists returns True when list result has count() > 0.""" | |||
| storage, mock_client = storage_with_mock_client | |||
| # Mock list return with count() method | |||
| mock_list_result = Mock() | |||
| mock_list_result.count.return_value = 1 | |||
| mock_client.storage.from_().list.return_value = mock_list_result | |||
| result = storage.exists("test.txt") | |||
| assert result is True | |||
| # Verify the correct calls were made | |||
| assert "test-bucket" in [call[0][0] for call in mock_client.storage.from_.call_args_list if call[0]] | |||
| mock_client.storage.from_().list.assert_called_with("test.txt") | |||
| mock_list_result.count.assert_called() | |||
| def test_exists_with_count_method_zero(self, storage_with_mock_client): | |||
| """Test exists returns False when list result has count() == 0.""" | |||
| storage, mock_client = storage_with_mock_client | |||
| # Mock list return with count() method returning 0 | |||
| mock_list_result = Mock() | |||
| mock_list_result.count.return_value = 0 | |||
| mock_client.storage.from_().list.return_value = mock_list_result | |||
| result = storage.exists("test.txt") | |||
| assert result is False | |||
| # Verify the correct calls were made | |||
| assert "test-bucket" in [call[0][0] for call in mock_client.storage.from_.call_args_list if call[0]] | |||
| mock_client.storage.from_().list.assert_called_with("test.txt") | |||
| mock_list_result.count.assert_called() | |||
| def test_exists_with_empty_list(self, storage_with_mock_client): | |||
| """Test exists returns False when list() returns empty list.""" | |||
| storage, mock_client = storage_with_mock_client | |||
| # Mock list return with special object that has count() method returning 0 | |||
| mock_list_result = Mock() | |||
| mock_list_result.count.return_value = 0 | |||
| mock_client.storage.from_().list.return_value = mock_list_result | |||
| result = storage.exists("test.txt") | |||
| assert result is False | |||
| # Verify the correct calls were made | |||
| assert "test-bucket" in [call[0][0] for call in mock_client.storage.from_.call_args_list if call[0]] | |||
| mock_client.storage.from_().list.assert_called_with("test.txt") | |||
| def test_delete_calls_remove_with_filename(self, storage_with_mock_client): | |||
| """Test delete calls remove([...]) (some client versions require a list).""" | |||
| storage, mock_client = storage_with_mock_client | |||
| filename = "test.txt" | |||
| storage.delete(filename) | |||
| mock_client.storage.from_.assert_called_once_with("test-bucket") | |||
| mock_client.storage.from_().remove.assert_called_once_with(filename) | |||
| def test_bucket_exists_returns_true_when_bucket_found(self): | |||
| """Test bucket_exists returns True when bucket is found in list.""" | |||
| with patch("extensions.storage.supabase_storage.dify_config") as mock_config: | |||
| mock_config.SUPABASE_URL = "https://test.supabase.co" | |||
| mock_config.SUPABASE_API_KEY = "test-api-key" | |||
| mock_config.SUPABASE_BUCKET_NAME = "test-bucket" | |||
| with patch("extensions.storage.supabase_storage.Client") as mock_client_class: | |||
| mock_client = Mock() | |||
| mock_client_class.return_value = mock_client | |||
| mock_bucket = Mock() | |||
| mock_bucket.name = "test-bucket" | |||
| mock_client.storage.list_buckets.return_value = [mock_bucket] | |||
| storage = SupabaseStorage() | |||
| result = storage.bucket_exists() | |||
| assert result is True | |||
| assert mock_client.storage.list_buckets.call_count >= 1 | |||
| def test_bucket_exists_returns_false_when_bucket_not_found(self): | |||
| """Test bucket_exists returns False when bucket is not found in list.""" | |||
| with patch("extensions.storage.supabase_storage.dify_config") as mock_config: | |||
| mock_config.SUPABASE_URL = "https://test.supabase.co" | |||
| mock_config.SUPABASE_API_KEY = "test-api-key" | |||
| mock_config.SUPABASE_BUCKET_NAME = "test-bucket" | |||
| with patch("extensions.storage.supabase_storage.Client") as mock_client_class: | |||
| mock_client = Mock() | |||
| mock_client_class.return_value = mock_client | |||
| # Mock different bucket | |||
| mock_bucket = Mock() | |||
| mock_bucket.name = "different-bucket" | |||
| mock_client.storage.list_buckets.return_value = [mock_bucket] | |||
| mock_client.storage.create_bucket = Mock() | |||
| storage = SupabaseStorage() | |||
| result = storage.bucket_exists() | |||
| assert result is False | |||
| assert mock_client.storage.list_buckets.call_count >= 1 | |||
| def test_bucket_exists_returns_false_when_no_buckets(self): | |||
| """Test bucket_exists returns False when no buckets exist.""" | |||
| with patch("extensions.storage.supabase_storage.dify_config") as mock_config: | |||
| mock_config.SUPABASE_URL = "https://test.supabase.co" | |||
| mock_config.SUPABASE_API_KEY = "test-api-key" | |||
| mock_config.SUPABASE_BUCKET_NAME = "test-bucket" | |||
| with patch("extensions.storage.supabase_storage.Client") as mock_client_class: | |||
| mock_client = Mock() | |||
| mock_client_class.return_value = mock_client | |||
| mock_client.storage.list_buckets.return_value = [] | |||
| mock_client.storage.create_bucket = Mock() | |||
| storage = SupabaseStorage() | |||
| result = storage.bucket_exists() | |||
| assert result is False | |||
| assert mock_client.storage.list_buckets.call_count >= 1 | |||
| @@ -43,28 +43,28 @@ def _get_test_app(): | |||
| @pytest.fixture | |||
| def mock_request_receiver(monkeypatch) -> mock.Mock: | |||
| def mock_request_receiver(monkeypatch: pytest.MonkeyPatch) -> mock.Mock: | |||
| mock_log_request_started = mock.Mock() | |||
| monkeypatch.setattr(ext_request_logging, "_log_request_started", mock_log_request_started) | |||
| return mock_log_request_started | |||
| @pytest.fixture | |||
| def mock_response_receiver(monkeypatch) -> mock.Mock: | |||
| def mock_response_receiver(monkeypatch: pytest.MonkeyPatch) -> mock.Mock: | |||
| mock_log_request_finished = mock.Mock() | |||
| monkeypatch.setattr(ext_request_logging, "_log_request_finished", mock_log_request_finished) | |||
| return mock_log_request_finished | |||
| @pytest.fixture | |||
| def mock_logger(monkeypatch) -> logging.Logger: | |||
| def mock_logger(monkeypatch: pytest.MonkeyPatch) -> logging.Logger: | |||
| _logger = mock.MagicMock(spec=logging.Logger) | |||
| monkeypatch.setattr(ext_request_logging, "logger", _logger) | |||
| return _logger | |||
| @pytest.fixture | |||
| def enable_request_logging(monkeypatch): | |||
| def enable_request_logging(monkeypatch: pytest.MonkeyPatch): | |||
| monkeypatch.setattr(dify_config, "ENABLE_REQUEST_LOGGING", True) | |||
| @@ -1,9 +1,11 @@ | |||
| import datetime | |||
| import pytest | |||
| from libs.datetime_utils import naive_utc_now | |||
| def test_naive_utc_now(monkeypatch): | |||
| def test_naive_utc_now(monkeypatch: pytest.MonkeyPatch): | |||
| tz_aware_utc_now = datetime.datetime.now(tz=datetime.UTC) | |||
| def _now_func(tz: datetime.timezone | None) -> datetime.datetime: | |||
| @@ -0,0 +1,63 @@ | |||
| """Test PyJWT import paths to catch changes in library structure.""" | |||
| import pytest | |||
| class TestPyJWTImports: | |||
| """Test PyJWT import paths used throughout the codebase.""" | |||
| def test_invalid_token_error_import(self): | |||
| """Test that InvalidTokenError can be imported as used in login controller.""" | |||
| # This test verifies the import path used in controllers/web/login.py:2 | |||
| # If PyJWT changes this import path, this test will fail early | |||
| try: | |||
| from jwt import InvalidTokenError | |||
| # Verify it's the correct exception class | |||
| assert issubclass(InvalidTokenError, Exception) | |||
| # Test that it can be instantiated | |||
| error = InvalidTokenError("test error") | |||
| assert str(error) == "test error" | |||
| except ImportError as e: | |||
| pytest.fail(f"Failed to import InvalidTokenError from jwt: {e}") | |||
| def test_jwt_exceptions_import(self): | |||
| """Test that jwt.exceptions imports work as expected.""" | |||
| # Alternative import path that might be used | |||
| try: | |||
| # Verify it's the same class as the direct import | |||
| from jwt import InvalidTokenError | |||
| from jwt.exceptions import InvalidTokenError as InvalidTokenErrorAlt | |||
| assert InvalidTokenError is InvalidTokenErrorAlt | |||
| except ImportError as e: | |||
| pytest.fail(f"Failed to import InvalidTokenError from jwt.exceptions: {e}") | |||
| def test_other_jwt_exceptions_available(self): | |||
| """Test that other common JWT exceptions are available.""" | |||
| # Test other exceptions that might be used in the codebase | |||
| try: | |||
| from jwt import DecodeError, ExpiredSignatureError, InvalidSignatureError | |||
| # Verify they are exception classes | |||
| assert issubclass(DecodeError, Exception) | |||
| assert issubclass(ExpiredSignatureError, Exception) | |||
| assert issubclass(InvalidSignatureError, Exception) | |||
| except ImportError as e: | |||
| pytest.fail(f"Failed to import JWT exceptions: {e}") | |||
| def test_jwt_main_functions_available(self): | |||
| """Test that main JWT functions are available.""" | |||
| try: | |||
| from jwt import decode, encode | |||
| # Verify they are callable | |||
| assert callable(decode) | |||
| assert callable(encode) | |||
| except ImportError as e: | |||
| pytest.fail(f"Failed to import JWT main functions: {e}") | |||
| @@ -143,7 +143,7 @@ def test_uuidv7_with_custom_timestamp(): | |||
| assert extracted_timestamp == custom_timestamp # Exact match for integer milliseconds | |||
| def test_uuidv7_with_none_timestamp(monkeypatch): | |||
| def test_uuidv7_with_none_timestamp(monkeypatch: pytest.MonkeyPatch): | |||
| """Test UUID generation with None timestamp uses current time.""" | |||
| mock_time = 1609459200 | |||
| mock_time_func = mock.Mock(return_value=mock_time) | |||
| @@ -4,8 +4,8 @@ from unittest.mock import MagicMock | |||
| import pytest | |||
| from _pytest.monkeypatch import MonkeyPatch | |||
| from oss2 import Bucket # type: ignore | |||
| from oss2.models import GetObjectResult, PutObjectResult # type: ignore | |||
| from oss2 import Bucket | |||
| from oss2.models import GetObjectResult, PutObjectResult | |||
| from tests.unit_tests.oss.__mock.base import ( | |||
| get_example_bucket, | |||
| @@ -1,7 +1,7 @@ | |||
| from unittest.mock import patch | |||
| import pytest | |||
| from oss2 import Auth # type: ignore | |||
| from oss2 import Auth | |||
| from extensions.storage.aliyun_oss_storage import AliyunOssStorage | |||
| from tests.unit_tests.oss.__mock.aliyun_oss import setup_aliyun_oss_mock | |||
| @@ -1,7 +1,7 @@ | |||
| from textwrap import dedent | |||
| import pytest | |||
| from yaml import YAMLError # type: ignore | |||
| from yaml import YAMLError | |||
| from core.tools.utils.yaml_utils import load_yaml_file | |||
| @@ -779,6 +779,12 @@ API_SENTRY_PROFILES_SAMPLE_RATE=1.0 | |||
| # If not set, Sentry error reporting will be disabled. | |||
| WEB_SENTRY_DSN= | |||
| # Plugin_daemon Service Sentry DSN address, default is empty, when empty, | |||
| # all monitoring information is not reported to Sentry. | |||
| # If not set, Sentry error reporting will be disabled. | |||
| PLUGIN_SENTRY_ENABLED=false | |||
| PLUGIN_SENTRY_DSN= | |||
| # ------------------------------ | |||
| # Notion Integration Configuration | |||
| # Variables can be obtained by applying for Notion integration: https://www.notion.so/my-integrations | |||
| @@ -212,6 +212,8 @@ services: | |||
| VOLCENGINE_TOS_ACCESS_KEY: ${PLUGIN_VOLCENGINE_TOS_ACCESS_KEY:-} | |||
| VOLCENGINE_TOS_SECRET_KEY: ${PLUGIN_VOLCENGINE_TOS_SECRET_KEY:-} | |||
| VOLCENGINE_TOS_REGION: ${PLUGIN_VOLCENGINE_TOS_REGION:-} | |||
| SENTRY_ENABLED: ${PLUGIN_SENTRY_ENABLED:-false} | |||
| SENTRY_DSN: ${PLUGIN_SENTRY_DSN:-} | |||
| ports: | |||
| - "${EXPOSE_PLUGIN_DEBUGGING_PORT:-5003}:${PLUGIN_DEBUGGING_PORT:-5003}" | |||
| volumes: | |||
| @@ -352,6 +352,8 @@ x-shared-env: &shared-api-worker-env | |||
| API_SENTRY_TRACES_SAMPLE_RATE: ${API_SENTRY_TRACES_SAMPLE_RATE:-1.0} | |||
| API_SENTRY_PROFILES_SAMPLE_RATE: ${API_SENTRY_PROFILES_SAMPLE_RATE:-1.0} | |||
| WEB_SENTRY_DSN: ${WEB_SENTRY_DSN:-} | |||
| PLUGIN_SENTRY_ENABLED: ${PLUGIN_SENTRY_ENABLED:-false} | |||
| PLUGIN_SENTRY_DSN: ${PLUGIN_SENTRY_DSN:-} | |||
| NOTION_INTEGRATION_TYPE: ${NOTION_INTEGRATION_TYPE:-public} | |||
| NOTION_CLIENT_SECRET: ${NOTION_CLIENT_SECRET:-} | |||
| NOTION_CLIENT_ID: ${NOTION_CLIENT_ID:-} | |||
| @@ -794,6 +796,8 @@ services: | |||
| VOLCENGINE_TOS_ACCESS_KEY: ${PLUGIN_VOLCENGINE_TOS_ACCESS_KEY:-} | |||
| VOLCENGINE_TOS_SECRET_KEY: ${PLUGIN_VOLCENGINE_TOS_SECRET_KEY:-} | |||
| VOLCENGINE_TOS_REGION: ${PLUGIN_VOLCENGINE_TOS_REGION:-} | |||
| SENTRY_ENABLED: ${PLUGIN_SENTRY_ENABLED:-false} | |||
| SENTRY_DSN: ${PLUGIN_SENTRY_DSN:-} | |||
| ports: | |||
| - "${EXPOSE_PLUGIN_DEBUGGING_PORT:-5003}:${PLUGIN_DEBUGGING_PORT:-5003}" | |||
| volumes: | |||
| @@ -5,6 +5,9 @@ LABEL maintainer="takatost@gmail.com" | |||
| # if you located in China, you can use aliyun mirror to speed up | |||
| # RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories | |||
| # if you located in China, you can use taobao registry to speed up | |||
| # RUN npm config set registry https://registry.npmmirror.com | |||
| RUN apk add --no-cache tzdata | |||
| RUN corepack enable | |||
| ENV PNPM_HOME="/pnpm" | |||
| @@ -22,9 +25,6 @@ COPY pnpm-lock.yaml . | |||
| # Use packageManager from package.json | |||
| RUN corepack install | |||
| # if you located in China, you can use taobao registry to speed up | |||
| # RUN pnpm install --frozen-lockfile --registry https://registry.npmmirror.com/ | |||
| RUN pnpm install --frozen-lockfile | |||
| # build resources | |||
| @@ -407,9 +407,13 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => { | |||
| currentChatInstanceRef.current.handleStop() | |||
| setShowNewConversationItemInList(true) | |||
| handleChangeConversation('') | |||
| handleNewConversationInputsChange(await getRawInputsFromUrlParams()) | |||
| const conversationInputs: Record<string, any> = {} | |||
| inputsForms.forEach((item: any) => { | |||
| conversationInputs[item.variable] = item.default || null | |||
| }) | |||
| handleNewConversationInputsChange(conversationInputs) | |||
| setClearChatList(true) | |||
| }, [handleChangeConversation, setShowNewConversationItemInList, handleNewConversationInputsChange, setClearChatList]) | |||
| }, [handleChangeConversation, setShowNewConversationItemInList, handleNewConversationInputsChange, setClearChatList, inputsForms]) | |||
| const handleUpdateConversationList = useCallback(() => { | |||
| mutateAppConversationData() | |||
| mutateAppPinnedConversationData() | |||
| @@ -137,24 +137,14 @@ const Answer: FC<AnswerProps> = ({ | |||
| /> | |||
| ) | |||
| } | |||
| {/** Render the normal steps */} | |||
| {/** Render workflow process */} | |||
| { | |||
| workflowProcess && !hideProcessDetail && ( | |||
| workflowProcess && ( | |||
| <WorkflowProcessItem | |||
| data={workflowProcess} | |||
| item={item} | |||
| hideProcessDetail={hideProcessDetail} | |||
| /> | |||
| ) | |||
| } | |||
| {/** Hide workflow steps by it's settings in siteInfo */} | |||
| { | |||
| workflowProcess && hideProcessDetail && appData && ( | |||
| <WorkflowProcessItem | |||
| data={workflowProcess} | |||
| item={item} | |||
| hideProcessDetail={hideProcessDetail} | |||
| readonly={!appData.site.show_workflow_steps} | |||
| readonly={hideProcessDetail && appData ? !appData.site.show_workflow_steps : undefined} | |||
| /> | |||
| ) | |||
| } | |||
| @@ -39,6 +39,8 @@ const WorkflowProcessItem = ({ | |||
| setCollapse(!expand) | |||
| }, [expand]) | |||
| if (readonly) return null | |||
| return ( | |||
| <div | |||
| className={cn( | |||
| @@ -51,8 +53,8 @@ const WorkflowProcessItem = ({ | |||
| )} | |||
| > | |||
| <div | |||
| className={cn('flex cursor-pointer items-center', !collapse && 'px-1.5', readonly && 'cursor-default')} | |||
| onClick={() => !readonly && setCollapse(!collapse)} | |||
| className={cn('flex cursor-pointer items-center', !collapse && 'px-1.5')} | |||
| onClick={() => setCollapse(!collapse)} | |||
| > | |||
| { | |||
| running && ( | |||
| @@ -72,10 +74,10 @@ const WorkflowProcessItem = ({ | |||
| <div className={cn('system-xs-medium text-text-secondary', !collapse && 'grow')}> | |||
| {t('workflow.common.workflowProcess')} | |||
| </div> | |||
| {!readonly && <RiArrowRightSLine className={cn('ml-1 h-4 w-4 text-text-tertiary', !collapse && 'rotate-90')} />} | |||
| <RiArrowRightSLine className={cn('ml-1 h-4 w-4 text-text-tertiary', !collapse && 'rotate-90')} /> | |||
| </div> | |||
| { | |||
| !collapse && !readonly && ( | |||
| !collapse && ( | |||
| <div className='mt-1.5'> | |||
| { | |||
| <TracingPanel | |||
| @@ -53,11 +53,18 @@ The text generation application offers non-session support and is ideal for tran | |||
| The rules are defined by the developer and need to ensure that the user identifier is unique within the application. The Service API does not share conversations created by the WebApp. | |||
| </Property> | |||
| <Property name='files' type='array[object]' key='files'> | |||
| File list, suitable for inputting files (images) combined with text understanding and answering questions, available only when the model supports Vision capability. | |||
| - `type` (string) Supported type: `image` (currently only supports image type) | |||
| - `transfer_method` (string) Transfer method, `remote_url` for image URL / `local_file` for file upload | |||
| - `url` (string) Image URL (when the transfer method is `remote_url`) | |||
| - `upload_file_id` (string) Uploaded file ID, which must be obtained by uploading through the File Upload API in advance (when the transfer method is `local_file`) | |||
| File list, suitable for inputting files combined with text understanding and answering questions, available only when the model supports Vision/Video capability. | |||
| - `type` (string) Supported type: | |||
| - `document` Supported types include: 'TXT', 'MD', 'MARKDOWN', 'MDX', 'PDF', 'HTML', 'XLSX', 'XLS', 'VTT', 'PROPERTIES', 'DOC', 'DOCX', 'CSV', 'EML', 'MSG', 'PPTX', 'PPT', 'XML', 'EPUB' | |||
| - `image` Supported types include: 'JPG', 'JPEG', 'PNG', 'GIF', 'WEBP', 'SVG' | |||
| - `audio` Supported types include: 'MP3', 'M4A', 'WAV', 'WEBM', 'MPGA' | |||
| - `video` Supported types include: 'MP4', 'MOV', 'MPEG', 'WEBM' | |||
| - `custom` Supported types include: other file types | |||
| - `transfer_method` (string) Transfer method: | |||
| - `remote_url`: File URL. | |||
| - `local_file`: Upload file. | |||
| - `url` File URL. (Only when transfer method is `remote_url`). | |||
| - `upload_file_id` Upload file ID. (Only when transfer method is `local_file`). | |||
| </Property> | |||
| </Properties> | |||
| @@ -53,11 +53,18 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from | |||
| アプリケーション内で開発者が一意に定義する必要があります。 | |||
| </Property> | |||
| <Property name='files' type='array[object]' key='files'> | |||
| ファイルリスト、モデルがVision機能をサポートしている場合のみ、テキスト理解と質問応答を組み合わせたファイル(画像)の入力に適しています。 | |||
| - `type` (string) サポートされるタイプ:`image`(現在は画像タイプのみサポート) | |||
| - `transfer_method` (string) 転送方法、画像URLの場合は`remote_url` / ファイルアップロードの場合は`local_file` | |||
| - `url` (string) 画像URL(転送方法が`remote_url`の場合) | |||
| - `upload_file_id` (string) アップロードされたファイルID、事前にファイルアップロードAPIを通じてアップロードする必要があります(転送方法が`local_file`の場合) | |||
| ファイルリスト、モデルが Vision/Video 機能をサポートしている場合に限り、ファイルをテキスト理解および質問応答に組み合わせて入力するのに適しています。 | |||
| - `type` (string) サポートされるタイプ: | |||
| - `document` サポートされるタイプには以下が含まれます:'TXT', 'MD', 'MARKDOWN', 'MDX', 'PDF', 'HTML', 'XLSX', 'XLS', 'VTT', 'PROPERTIES', 'DOC', 'DOCX', 'CSV', 'EML', 'MSG', 'PPTX', 'PPT', 'XML', 'EPUB' | |||
| - `image` サポートされるタイプには以下が含まれます:'JPG', 'JPEG', 'PNG', 'GIF', 'WEBP', 'SVG' | |||
| - `audio` サポートされるタイプには以下が含まれます:'MP3', 'M4A', 'WAV', 'WEBM', 'MPGA' | |||
| - `video` サポートされるタイプには以下が含まれます:'MP4', 'MOV', 'MPEG', 'WEBM' | |||
| - `custom` サポートされるタイプには以下が含まれます:その他のファイルタイプ | |||
| - `transfer_method` (string) 転送方法: | |||
| - `remote_url`: ファイルのURL。 | |||
| - `local_file`: ファイルをアップロード。 | |||
| - `url` ファイルのURL。(転送方法が `remote_url` の場合のみ)。 | |||
| - `upload_file_id` アップロードされたファイルID。(転送方法が `local_file` の場合のみ)。 | |||
| </Property> | |||
| </Properties> | |||
| @@ -51,12 +51,17 @@ import { Row, Col, Properties, Property, Heading, SubProperty } from '../md.tsx' | |||
| 由开发者定义规则,需保证用户标识在应用内唯一。 | |||
| </Property> | |||
| <Property name='files' type='array[object]' key='files'> | |||
| 上传的文件。 | |||
| - `type` (string) 支持类型:图片 `image`(目前仅支持图片格式) 。 | |||
| - `transfer_method` (string) 传递方式: | |||
| - `remote_url`: 图片地址。 | |||
| 文件列表,适用于传入文件结合文本理解并回答问题,仅当模型支持 Vision/Video 能力时可用。 | |||
| - `type` (string) 支持类型: | |||
| - `document` 具体类型包含:'TXT', 'MD', 'MARKDOWN', 'MDX', 'PDF', 'HTML', 'XLSX', 'XLS', 'VTT', 'PROPERTIES', 'DOC', 'DOCX', 'CSV', 'EML', 'MSG', 'PPTX', 'PPT', 'XML', 'EPUB' | |||
| - `image` 具体类型包含:'JPG', 'JPEG', 'PNG', 'GIF', 'WEBP', 'SVG' | |||
| - `audio` 具体类型包含:'MP3', 'M4A', 'WAV', 'WEBM', 'MPGA' | |||
| - `video` 具体类型包含:'MP4', 'MOV', 'MPEG', 'WEBM' | |||
| - `custom` 具体类型包含:其他文件类型 | |||
| - `transfer_method` (string) 传递方式: | |||
| - `remote_url`: 文件地址。 | |||
| - `local_file`: 上传文件。 | |||
| - `url` 图片地址。(仅当传递方式为 `remote_url` 时)。 | |||
| - `url` 文件地址。(仅当传递方式为 `remote_url` 时)。 | |||
| - `upload_file_id` 上传文件 ID。(仅当传递方式为 `local_file `时)。 | |||
| </Property> | |||
| </Properties> | |||
| @@ -57,16 +57,18 @@ Chat applications support session persistence, allowing previous chat history to | |||
| Conversation ID, to continue the conversation based on previous chat records, it is necessary to pass the previous message's conversation_id. | |||
| </Property> | |||
| <Property name='files' type='array[object]' key='files'> | |||
| File list, suitable for inputting files combined with text understanding and answering questions, available only when the model supports Vision capability. | |||
| File list, suitable for inputting files combined with text understanding and answering questions, available only when the model supports Vision/Video capability. | |||
| - `type` (string) Supported type: | |||
| - `document` ('TXT', 'MD', 'MARKDOWN', 'PDF', 'HTML', 'XLSX', 'XLS', 'DOCX', 'CSV', 'EML', 'MSG', 'PPTX', 'PPT', 'XML', 'EPUB') | |||
| - `image` ('JPG', 'JPEG', 'PNG', 'GIF', 'WEBP', 'SVG') | |||
| - `audio` ('MP3', 'M4A', 'WAV', 'WEBM', 'AMR') | |||
| - `video` ('MP4', 'MOV', 'MPEG', 'MPGA') | |||
| - `custom` (Other file types) | |||
| - `transfer_method` (string) Transfer method, `remote_url` for image URL / `local_file` for file upload | |||
| - `url` (string) Image URL (when the transfer method is `remote_url`) | |||
| - `upload_file_id` (string) Uploaded file ID, which must be obtained by uploading through the File Upload API in advance (when the transfer method is `local_file`) | |||
| - `document` Supported types include: 'TXT', 'MD', 'MARKDOWN', 'MDX', 'PDF', 'HTML', 'XLSX', 'XLS', 'VTT', 'PROPERTIES', 'DOC', 'DOCX', 'CSV', 'EML', 'MSG', 'PPTX', 'PPT', 'XML', 'EPUB' | |||
| - `image` Supported types include: 'JPG', 'JPEG', 'PNG', 'GIF', 'WEBP', 'SVG' | |||
| - `audio` Supported types include: 'MP3', 'M4A', 'WAV', 'WEBM', 'MPGA' | |||
| - `video` Supported types include: 'MP4', 'MOV', 'MPEG', 'WEBM' | |||
| - `custom` Supported types include: other file types | |||
| - `transfer_method` (string) Transfer method: | |||
| - `remote_url`: File URL. | |||
| - `local_file`: Upload file. | |||
| - `url` File URL. (Only when transfer method is `remote_url`). | |||
| - `upload_file_id` Upload file ID. (Only when transfer method is `local_file`). | |||
| </Property> | |||
| <Property name='auto_generate_name' type='bool' key='auto_generate_name'> | |||
| Auto-generate title, default is `true`. | |||
| @@ -57,16 +57,18 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from | |||
| 会話ID、以前のチャット記録に基づいて会話を続けるには、以前のメッセージのconversation_idを渡す必要があります。 | |||
| </Property> | |||
| <Property name='files' type='array[object]' key='files'> | |||
| ファイルリスト、テキストの理解と質問への回答を組み合わせたファイルの入力に適しており、モデルがビジョン機能をサポートしている場合にのみ利用可能です。 | |||
| - `type` (string) サポートされているタイプ: | |||
| - `document` ('TXT', 'MD', 'MARKDOWN', 'PDF', 'HTML', 'XLSX', 'XLS', 'DOCX', 'CSV', 'EML', 'MSG', 'PPTX', 'PPT', 'XML', 'EPUB') | |||
| - `image` ('JPG', 'JPEG', 'PNG', 'GIF', 'WEBP', 'SVG') | |||
| - `audio` ('MP3', 'M4A', 'WAV', 'WEBM', 'AMR') | |||
| - `video` ('MP4', 'MOV', 'MPEG', 'MPGA') | |||
| - `custom` (他のファイルタイプ) | |||
| - `transfer_method` (string) 転送方法、画像URLの場合は`remote_url` / ファイルアップロードの場合は`local_file` | |||
| - `url` (string) 画像URL(転送方法が`remote_url`の場合) | |||
| - `upload_file_id` (string) アップロードされたファイルID、事前にファイルアップロードAPIを通じて取得する必要があります(転送方法が`local_file`の場合) | |||
| ファイルリスト、モデルが Vision/Video 機能をサポートしている場合に限り、ファイルをテキスト理解および質問応答に組み合わせて入力するのに適しています。 | |||
| - `type` (string) サポートされるタイプ: | |||
| - `document` サポートされるタイプには以下が含まれます:'TXT', 'MD', 'MARKDOWN', 'MDX', 'PDF', 'HTML', 'XLSX', 'XLS', 'VTT', 'PROPERTIES', 'DOC', 'DOCX', 'CSV', 'EML', 'MSG', 'PPTX', 'PPT', 'XML', 'EPUB' | |||
| - `image` サポートされるタイプには以下が含まれます:'JPG', 'JPEG', 'PNG', 'GIF', 'WEBP', 'SVG' | |||
| - `audio` サポートされるタイプには以下が含まれます:'MP3', 'M4A', 'WAV', 'WEBM', 'MPGA' | |||
| - `video` サポートされるタイプには以下が含まれます:'MP4', 'MOV', 'MPEG', 'WEBM' | |||
| - `custom` サポートされるタイプには以下が含まれます:その他のファイルタイプ | |||
| - `transfer_method` (string) 転送方法: | |||
| - `remote_url`: ファイルのURL。 | |||
| - `local_file`: ファイルをアップロード。 | |||
| - `url` ファイルのURL。(転送方法が `remote_url` の場合のみ)。 | |||
| - `upload_file_id` アップロードされたファイルID。(転送方法が `local_file` の場合のみ)。 | |||
| </Property> | |||
| <Property name='auto_generate_name' type='bool' key='auto_generate_name'> | |||
| タイトルを自動生成、デフォルトは`true`。 | |||
| @@ -55,17 +55,17 @@ import { Row, Col, Properties, Property, Heading, SubProperty } from '../md.tsx' | |||
| (选填)会话 ID,需要基于之前的聊天记录继续对话,必须传之前消息的 conversation_id。 | |||
| </Property> | |||
| <Property name='files' type='array[object]' key='files'> | |||
| 文件列表,适用于传入文件结合文本理解并回答问题,仅当模型支持 Vision 能力时可用。 | |||
| 文件列表,适用于传入文件结合文本理解并回答问题,仅当模型支持 Vision/Video 能力时可用。 | |||
| - `type` (string) 支持类型: | |||
| - `document` 具体类型包含:'TXT', 'MD', 'MARKDOWN', 'PDF', 'HTML', 'XLSX', 'XLS', 'DOCX', 'CSV', 'EML', 'MSG', 'PPTX', 'PPT', 'XML', 'EPUB' | |||
| - `document` 具体类型包含:'TXT', 'MD', 'MARKDOWN', 'MDX', 'PDF', 'HTML', 'XLSX', 'XLS', 'VTT', 'PROPERTIES', 'DOC', 'DOCX', 'CSV', 'EML', 'MSG', 'PPTX', 'PPT', 'XML', 'EPUB' | |||
| - `image` 具体类型包含:'JPG', 'JPEG', 'PNG', 'GIF', 'WEBP', 'SVG' | |||
| - `audio` 具体类型包含:'MP3', 'M4A', 'WAV', 'WEBM', 'AMR' | |||
| - `video` 具体类型包含:'MP4', 'MOV', 'MPEG', 'MPGA' | |||
| - `audio` 具体类型包含:'MP3', 'M4A', 'WAV', 'WEBM', 'MPGA' | |||
| - `video` 具体类型包含:'MP4', 'MOV', 'MPEG', 'WEBM' | |||
| - `custom` 具体类型包含:其他文件类型 | |||
| - `transfer_method` (string) 传递方式: | |||
| - `remote_url`: 图片地址。 | |||
| - `remote_url`: 文件地址。 | |||
| - `local_file`: 上传文件。 | |||
| - `url` 图片地址。(仅当传递方式为 `remote_url` 时)。 | |||
| - `url` 文件地址。(仅当传递方式为 `remote_url` 时)。 | |||
| - `upload_file_id` 上传文件 ID。(仅当传递方式为 `local_file `时)。 | |||
| </Property> | |||
| <Property name='auto_generate_name' type='bool' key='auto_generate_name'> | |||
| @@ -56,11 +56,18 @@ Chat applications support session persistence, allowing previous chat history to | |||
| Conversation ID, to continue the conversation based on previous chat records, it is necessary to pass the previous message's conversation_id. | |||
| </Property> | |||
| <Property name='files' type='array[object]' key='files'> | |||
| File list, suitable for inputting files (images) combined with text understanding and answering questions, available only when the model supports Vision capability. | |||
| - `type` (string) Supported type: `image` (currently only supports image type) | |||
| - `transfer_method` (string) Transfer method, `remote_url` for image URL / `local_file` for file upload | |||
| - `url` (string) Image URL (when the transfer method is `remote_url`) | |||
| - `upload_file_id` (string) Uploaded file ID, which must be obtained by uploading through the File Upload API in advance (when the transfer method is `local_file`) | |||
| File list, suitable for inputting files combined with text understanding and answering questions, available only when the model supports Vision/Video capability. | |||
| - `type` (string) Supported type: | |||
| - `document` Supported types include: 'TXT', 'MD', 'MARKDOWN', 'MDX', 'PDF', 'HTML', 'XLSX', 'XLS', 'VTT', 'PROPERTIES', 'DOC', 'DOCX', 'CSV', 'EML', 'MSG', 'PPTX', 'PPT', 'XML', 'EPUB' | |||
| - `image` Supported types include: 'JPG', 'JPEG', 'PNG', 'GIF', 'WEBP', 'SVG' | |||
| - `audio` Supported types include: 'MP3', 'M4A', 'WAV', 'WEBM', 'MPGA' | |||
| - `video` Supported types include: 'MP4', 'MOV', 'MPEG', 'WEBM' | |||
| - `custom` Supported types include: other file types | |||
| - `transfer_method` (string) Transfer method: | |||
| - `remote_url`: File URL. | |||
| - `local_file`: Upload file. | |||
| - `url` File URL. (Only when transfer method is `remote_url`). | |||
| - `upload_file_id` Upload file ID. (Only when transfer method is `local_file`). | |||
| </Property> | |||
| <Property name='auto_generate_name' type='bool' key='auto_generate_name'> | |||
| Auto-generate title, default is `true`. | |||
| @@ -56,11 +56,18 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from | |||
| 会話ID、以前のチャット記録に基づいて会話を続けるには、前のメッセージのconversation_idを渡す必要があります。 | |||
| </Property> | |||
| <Property name='files' type='array[object]' key='files'> | |||
| ファイルリスト、テキストの理解と質問への回答を組み合わせたファイル(画像)の入力に適しており、モデルがビジョン機能をサポートしている場合にのみ利用可能です。 | |||
| - `type` (string) サポートされているタイプ:`image`(現在は画像タイプのみサポート) | |||
| - `transfer_method` (string) 転送方法、画像URLの場合は`remote_url` / ファイルアップロードの場合は`local_file` | |||
| - `url` (string) 画像URL(転送方法が`remote_url`の場合) | |||
| - `upload_file_id` (string) アップロードされたファイルID、事前にファイルアップロードAPIを通じて取得する必要があります(転送方法が`local_file`の場合) | |||
| ファイルリスト、モデルが Vision/Video 機能をサポートしている場合に限り、ファイルをテキスト理解および質問応答に組み合わせて入力するのに適しています。 | |||
| - `type` (string) サポートされるタイプ: | |||
| - `document` サポートされるタイプには以下が含まれます:'TXT', 'MD', 'MARKDOWN', 'MDX', 'PDF', 'HTML', 'XLSX', 'XLS', 'VTT', 'PROPERTIES', 'DOC', 'DOCX', 'CSV', 'EML', 'MSG', 'PPTX', 'PPT', 'XML', 'EPUB' | |||
| - `image` サポートされるタイプには以下が含まれます:'JPG', 'JPEG', 'PNG', 'GIF', 'WEBP', 'SVG' | |||
| - `audio` サポートされるタイプには以下が含まれます:'MP3', 'M4A', 'WAV', 'WEBM', 'MPGA' | |||
| - `video` サポートされるタイプには以下が含まれます:'MP4', 'MOV', 'MPEG', 'WEBM' | |||
| - `custom` サポートされるタイプには以下が含まれます:その他のファイルタイプ | |||
| - `transfer_method` (string) 転送方法: | |||
| - `remote_url`: ファイルのURL。 | |||
| - `local_file`: ファイルをアップロード。 | |||
| - `url` ファイルのURL。(転送方法が `remote_url` の場合のみ)。 | |||
| - `upload_file_id` アップロードされたファイルID。(転送方法が `local_file` の場合のみ)。 | |||
| </Property> | |||
| <Property name='auto_generate_name' type='bool' key='auto_generate_name'> | |||
| タイトルを自動生成します。デフォルトは`true`です。 | |||
| @@ -55,12 +55,17 @@ import { Row, Col, Properties, Property, Heading, SubProperty } from '../md.tsx' | |||
| (选填)会话 ID,需要基于之前的聊天记录继续对话,必须传之前消息的 conversation_id。 | |||
| </Property> | |||
| <Property name='files' type='array[object]' key='files'> | |||
| 上传的文件。 | |||
| - `type` (string) 支持类型:图片 `image`(目前仅支持图片格式) 。 | |||
| 文件列表,适用于传入文件结合文本理解并回答问题,仅当模型支持 Vision/Video 能力时可用。 | |||
| - `type` (string) 支持类型: | |||
| - `document` 具体类型包含:'TXT', 'MD', 'MARKDOWN', 'MDX', 'PDF', 'HTML', 'XLSX', 'XLS', 'VTT', 'PROPERTIES', 'DOC', 'DOCX', 'CSV', 'EML', 'MSG', 'PPTX', 'PPT', 'XML', 'EPUB' | |||
| - `image` 具体类型包含:'JPG', 'JPEG', 'PNG', 'GIF', 'WEBP', 'SVG' | |||
| - `audio` 具体类型包含:'MP3', 'M4A', 'WAV', 'WEBM', 'MPGA' | |||
| - `video` 具体类型包含:'MP4', 'MOV', 'MPEG', 'WEBM' | |||
| - `custom` 具体类型包含:其他文件类型 | |||
| - `transfer_method` (string) 传递方式: | |||
| - `remote_url`: 图片地址。 | |||
| - `remote_url`: 文件地址。 | |||
| - `local_file`: 上传文件。 | |||
| - `url` 图片地址。(仅当传递方式为 `remote_url` 时)。 | |||
| - `url` 文件地址。(仅当传递方式为 `remote_url` 时)。 | |||
| - `upload_file_id` 上传文件 ID。(仅当传递方式为 `local_file `时)。 | |||
| </Property> | |||
| <Property name='auto_generate_name' type='bool' key='auto_generate_name'> | |||
| @@ -39,14 +39,16 @@ Workflow applications offers non-session support and is ideal for translation, a | |||
| File Array type variable is suitable for inputting files combined with text understanding and answering questions, available only when the model supports file parsing and understanding capability. | |||
| If the variable is of File Array type, the corresponding value should be a list whose elements contain following attributions: | |||
| - `type` (string) Supported type: | |||
| - `document` ('TXT', 'MD', 'MARKDOWN', 'PDF', 'HTML', 'XLSX', 'XLS', 'DOCX', 'CSV', 'EML', 'MSG', 'PPTX', 'PPT', 'XML', 'EPUB') | |||
| - `image` ('JPG', 'JPEG', 'PNG', 'GIF', 'WEBP', 'SVG') | |||
| - `audio` ('MP3', 'M4A', 'WAV', 'WEBM', 'AMR') | |||
| - `video` ('MP4', 'MOV', 'MPEG', 'MPGA') | |||
| - `custom` (Other file types) | |||
| - `transfer_method` (string) Transfer method, `remote_url` for image URL / `local_file` for file upload | |||
| - `url` (string) Image URL (when the transfer method is `remote_url`) | |||
| - `upload_file_id` (string) Uploaded file ID, which must be obtained by uploading through the File Upload API in advance (when the transfer method is `local_file`) | |||
| - `document` Supported types include: 'TXT', 'MD', 'MARKDOWN', 'MDX', 'PDF', 'HTML', 'XLSX', 'XLS', 'VTT', 'PROPERTIES', 'DOC', 'DOCX', 'CSV', 'EML', 'MSG', 'PPTX', 'PPT', 'XML', 'EPUB' | |||
| - `image` Supported types include: 'JPG', 'JPEG', 'PNG', 'GIF', 'WEBP', 'SVG' | |||
| - `audio` Supported types include: 'MP3', 'M4A', 'WAV', 'WEBM', 'MPGA' | |||
| - `video` Supported types include: 'MP4', 'MOV', 'MPEG', 'WEBM' | |||
| - `custom` Supported types include: other file types | |||
| - `transfer_method` (string) Transfer method: | |||
| - `remote_url`: File URL. | |||
| - `local_file`: Upload file. | |||
| - `url` File URL. (Only when transfer method is `remote_url`). | |||
| - `upload_file_id` Upload file ID. (Only when transfer method is `local_file`). | |||
| - `response_mode` (string) Required | |||
| The mode of response return, supporting: | |||
| @@ -39,16 +39,18 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from | |||
| ファイルリストは、テキスト理解と質問への回答を組み合わせたファイルの入力に適しています。モデルがファイルの解析と理解機能をサポートしている場合にのみ使用できます。 | |||
| 変数がファイルリストの場合、リストの各要素は以下の属性を持つ必要があります。 | |||
| - `type` (string) サポートされているタイプ: | |||
| - `document` ('TXT', 'MD', 'MARKDOWN', 'PDF', 'HTML', 'XLSX', 'XLS', 'DOCX', 'CSV', 'EML', 'MSG', 'PPTX', 'PPT', 'XML', 'EPUB') | |||
| - `image` ('JPG', 'JPEG', 'PNG', 'GIF', 'WEBP', 'SVG') | |||
| - `audio` ('MP3', 'M4A', 'WAV', 'WEBM', 'AMR') | |||
| - `video` ('MP4', 'MOV', 'MPEG', 'MPGA') | |||
| - `custom` (他のファイルタイプ) | |||
| - `transfer_method` (string) 転送方法、画像URLの場合は`remote_url` / ファイルアップロードの場合は`local_file` | |||
| - `url` (string) 画像URL(転送方法が`remote_url`の場合) | |||
| - `upload_file_id` (string) アップロードされたファイルID、事前にファイルアップロードAPIを通じて取得する必要があります(転送方法が`local_file`の場合) | |||
| - `type` (string) サポートされるタイプ: | |||
| - `document` サポートされるタイプには以下が含まれます:'TXT', 'MD', 'MARKDOWN', 'MDX', 'PDF', 'HTML', 'XLSX', 'XLS', 'VTT', 'PROPERTIES', 'DOC', 'DOCX', 'CSV', 'EML', 'MSG', 'PPTX', 'PPT', 'XML', 'EPUB' | |||
| - `image` サポートされるタイプには以下が含まれます:'JPG', 'JPEG', 'PNG', 'GIF', 'WEBP', 'SVG' | |||
| - `audio` サポートされるタイプには以下が含まれます:'MP3', 'M4A', 'WAV', 'WEBM', 'MPGA' | |||
| - `video` サポートされるタイプには以下が含まれます:'MP4', 'MOV', 'MPEG', 'WEBM' | |||
| - `custom` サポートされるタイプには以下が含まれます:その他のファイルタイプ | |||
| - `transfer_method` (string) 転送方法: | |||
| - `remote_url`: ファイルのURL。 | |||
| - `local_file`: ファイルをアップロード。 | |||
| - `url` ファイルのURL。(転送方法が `remote_url` の場合のみ)。 | |||
| - `upload_file_id` アップロードされたファイルID。(転送方法が `local_file` の場合のみ)。 | |||
| - `response_mode` (string) 必須 | |||
| 応答の返却モードを指定します。サポートされているモード: | |||
| - `streaming` ストリーミングモード(推奨)、SSE([Server-Sent Events](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events))を通じてタイプライターのような出力を実装します。 | |||
| @@ -342,14 +342,17 @@ Workflow 应用无会话支持,适合用于翻译/文章写作/总结 AI 等 | |||
| inputs 参数包含了多组键值对(Key/Value pairs),每组的键对应一个特定变量,每组的值则是该变量的具体值。变量可以是文件列表类型。 | |||
| 文件列表类型变量适用于传入文件结合文本理解并回答问题,仅当模型支持该类型文件解析能力时可用。如果该变量是文件列表类型,该变量对应的值应是列表格式,其中每个元素应包含以下内容: | |||
| - `type` (string) 支持类型: | |||
| - `document` 具体类型包含:'TXT', 'MD', 'MARKDOWN', 'PDF', 'HTML', 'XLSX', 'XLS', 'DOCX', 'CSV', 'EML', 'MSG', 'PPTX', 'PPT', 'XML', 'EPUB' | |||
| - `document` 具体类型包含:'TXT', 'MD', 'MARKDOWN', 'MDX', 'PDF', 'HTML', 'XLSX', 'XLS', 'VTT', 'PROPERTIES', 'DOC', 'DOCX', 'CSV', 'EML', 'MSG', 'PPTX', 'PPT', 'XML', 'EPUB' | |||
| - `image` 具体类型包含:'JPG', 'JPEG', 'PNG', 'GIF', 'WEBP', 'SVG' | |||
| - `audio` 具体类型包含:'MP3', 'M4A', 'WAV', 'WEBM', 'AMR' | |||
| - `video` 具体类型包含:'MP4', 'MOV', 'MPEG', 'MPGA' | |||
| - `audio` 具体类型包含:'MP3', 'M4A', 'WAV', 'WEBM', 'MPGA' | |||
| - `video` 具体类型包含:'MP4', 'MOV', 'MPEG', 'WEBM' | |||
| - `custom` 具体类型包含:其他文件类型 | |||
| - `transfer_method` (string) 传递方式,`remote_url` 图片地址 / `local_file` 上传文件 | |||
| - `url` (string) 图片地址(仅当传递方式为 `remote_url` 时) | |||
| - `upload_file_id` (string) 上传文件 ID(仅当传递方式为 `local_file` 时) | |||
| - `transfer_method` (string) 传递方式: | |||
| - `remote_url`: 文件地址。 | |||
| - `local_file`: 上传文件。 | |||
| - `url` 文件地址。(仅当传递方式为 `remote_url` 时)。 | |||
| - `upload_file_id` 上传文件 ID。(仅当传递方式为 `local_file `时)。 | |||
| - `response_mode` (string) Required | |||
| 返回响应模式,支持: | |||
| - `streaming` 流式模式(推荐)。基于 SSE(**[Server-Sent Events](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events)**)实现类似打字机输出方式的流式返回。 | |||
| @@ -0,0 +1,44 @@ | |||
| import type { SlashCommandHandler } from './types' | |||
| import React from 'react' | |||
| import { RiUser3Line } from '@remixicon/react' | |||
| import i18n from '@/i18n-config/i18next-config' | |||
| import { registerCommands, unregisterCommands } from './command-bus' | |||
| // Account command dependency types - no external dependencies needed | |||
| type AccountDeps = Record<string, never> | |||
| /** | |||
| * Account command - Navigates to account page | |||
| */ | |||
| export const accountCommand: SlashCommandHandler<AccountDeps> = { | |||
| name: 'account', | |||
| description: 'Navigate to account page', | |||
| async search(args: string, locale: string = 'en') { | |||
| return [{ | |||
| id: 'account', | |||
| title: i18n.t('common.account.account', { lng: locale }), | |||
| description: i18n.t('app.gotoAnything.actions.accountDesc', { lng: locale }), | |||
| type: 'command' as const, | |||
| icon: ( | |||
| <div className='flex h-6 w-6 items-center justify-center rounded-md border-[0.5px] border-divider-regular bg-components-panel-bg'> | |||
| <RiUser3Line className='h-4 w-4 text-text-tertiary' /> | |||
| </div> | |||
| ), | |||
| data: { command: 'navigation.account', args: {} }, | |||
| }] | |||
| }, | |||
| register(_deps: AccountDeps) { | |||
| registerCommands({ | |||
| 'navigation.account': async (_args) => { | |||
| // Navigate to account page | |||
| window.location.href = '/account' | |||
| }, | |||
| }) | |||
| }, | |||
| unregister() { | |||
| unregisterCommands(['navigation.account']) | |||
| }, | |||
| } | |||
| @@ -0,0 +1,43 @@ | |||
| import type { SlashCommandHandler } from './types' | |||
| import React from 'react' | |||
| import { RiDiscordLine } from '@remixicon/react' | |||
| import i18n from '@/i18n-config/i18next-config' | |||
| import { registerCommands, unregisterCommands } from './command-bus' | |||
| // Community command dependency types | |||
| type CommunityDeps = Record<string, never> | |||
| /** | |||
| * Community command - Opens Discord community | |||
| */ | |||
| export const communityCommand: SlashCommandHandler<CommunityDeps> = { | |||
| name: 'community', | |||
| description: 'Open community Discord', | |||
| async search(args: string, locale: string = 'en') { | |||
| return [{ | |||
| id: 'community', | |||
| title: i18n.t('common.userProfile.community', { lng: locale }), | |||
| description: i18n.t('app.gotoAnything.actions.communityDesc', { lng: locale }) || 'Open Discord community', | |||
| type: 'command' as const, | |||
| icon: ( | |||
| <div className='flex h-6 w-6 items-center justify-center rounded-md border-[0.5px] border-divider-regular bg-components-panel-bg'> | |||
| <RiDiscordLine className='h-4 w-4 text-text-tertiary' /> | |||
| </div> | |||
| ), | |||
| data: { command: 'navigation.community', args: { url: 'https://discord.gg/5AEfbxcd9k' } }, | |||
| }] | |||
| }, | |||
| register(_deps: CommunityDeps) { | |||
| registerCommands({ | |||
| 'navigation.community': async (args) => { | |||
| const url = args?.url || 'https://discord.gg/5AEfbxcd9k' | |||
| window.open(url, '_blank', 'noopener,noreferrer') | |||
| }, | |||
| }) | |||
| }, | |||
| unregister() { | |||
| unregisterCommands(['navigation.community']) | |||
| }, | |||
| } | |||
| @@ -0,0 +1,44 @@ | |||
| import type { SlashCommandHandler } from './types' | |||
| import React from 'react' | |||
| import { RiBookOpenLine } from '@remixicon/react' | |||
| import i18n from '@/i18n-config/i18next-config' | |||
| import { registerCommands, unregisterCommands } from './command-bus' | |||
| import { defaultDocBaseUrl } from '@/context/i18n' | |||
| // Documentation command dependency types - no external dependencies needed | |||
| type DocDeps = Record<string, never> | |||
| /** | |||
| * Documentation command - Opens help documentation | |||
| */ | |||
| export const docCommand: SlashCommandHandler<DocDeps> = { | |||
| name: 'doc', | |||
| description: 'Open documentation', | |||
| async search(args: string, locale: string = 'en') { | |||
| return [{ | |||
| id: 'doc', | |||
| title: i18n.t('common.userProfile.helpCenter', { lng: locale }), | |||
| description: i18n.t('app.gotoAnything.actions.docDesc', { lng: locale }) || 'Open help documentation', | |||
| type: 'command' as const, | |||
| icon: ( | |||
| <div className='flex h-6 w-6 items-center justify-center rounded-md border-[0.5px] border-divider-regular bg-components-panel-bg'> | |||
| <RiBookOpenLine className='h-4 w-4 text-text-tertiary' /> | |||
| </div> | |||
| ), | |||
| data: { command: 'navigation.doc', args: {} }, | |||
| }] | |||
| }, | |||
| register(_deps: DocDeps) { | |||
| registerCommands({ | |||
| 'navigation.doc': async (_args) => { | |||
| const url = `${defaultDocBaseUrl}` | |||
| window.open(url, '_blank', 'noopener,noreferrer') | |||
| }, | |||
| }) | |||
| }, | |||
| unregister() { | |||
| unregisterCommands(['navigation.doc']) | |||
| }, | |||
| } | |||
| @@ -0,0 +1,43 @@ | |||
| import type { SlashCommandHandler } from './types' | |||
| import React from 'react' | |||
| import { RiFeedbackLine } from '@remixicon/react' | |||
| import i18n from '@/i18n-config/i18next-config' | |||
| import { registerCommands, unregisterCommands } from './command-bus' | |||
| // Feedback command dependency types | |||
| type FeedbackDeps = Record<string, never> | |||
| /** | |||
| * Feedback command - Opens GitHub feedback discussions | |||
| */ | |||
| export const feedbackCommand: SlashCommandHandler<FeedbackDeps> = { | |||
| name: 'feedback', | |||
| description: 'Open feedback discussions', | |||
| async search(args: string, locale: string = 'en') { | |||
| return [{ | |||
| id: 'feedback', | |||
| title: i18n.t('common.userProfile.communityFeedback', { lng: locale }), | |||
| description: i18n.t('app.gotoAnything.actions.feedbackDesc', { lng: locale }) || 'Open community feedback discussions', | |||
| type: 'command' as const, | |||
| icon: ( | |||
| <div className='flex h-6 w-6 items-center justify-center rounded-md border-[0.5px] border-divider-regular bg-components-panel-bg'> | |||
| <RiFeedbackLine className='h-4 w-4 text-text-tertiary' /> | |||
| </div> | |||
| ), | |||
| data: { command: 'navigation.feedback', args: { url: 'https://github.com/langgenius/dify/discussions/categories/feedbacks' } }, | |||
| }] | |||
| }, | |||
| register(_deps: FeedbackDeps) { | |||
| registerCommands({ | |||
| 'navigation.feedback': async (args) => { | |||
| const url = args?.url || 'https://github.com/langgenius/dify/discussions/categories/feedbacks' | |||
| window.open(url, '_blank', 'noopener,noreferrer') | |||
| }, | |||
| }) | |||
| }, | |||
| unregister() { | |||
| unregisterCommands(['navigation.feedback']) | |||
| }, | |||
| } | |||
| @@ -7,6 +7,10 @@ import { useTheme } from 'next-themes' | |||
| import { setLocaleOnClient } from '@/i18n-config' | |||
| import { themeCommand } from './theme' | |||
| import { languageCommand } from './language' | |||
| import { feedbackCommand } from './feedback' | |||
| import { docCommand } from './doc' | |||
| import { communityCommand } from './community' | |||
| import { accountCommand } from './account' | |||
| import i18n from '@/i18n-config/i18next-config' | |||
| export const slashAction: ActionItem = { | |||
| @@ -30,12 +34,20 @@ export const registerSlashCommands = (deps: Record<string, any>) => { | |||
| // Register command handlers to the registry system with their respective dependencies | |||
| slashCommandRegistry.register(themeCommand, { setTheme: deps.setTheme }) | |||
| slashCommandRegistry.register(languageCommand, { setLocale: deps.setLocale }) | |||
| slashCommandRegistry.register(feedbackCommand, {}) | |||
| slashCommandRegistry.register(docCommand, {}) | |||
| slashCommandRegistry.register(communityCommand, {}) | |||
| slashCommandRegistry.register(accountCommand, {}) | |||
| } | |||
| export const unregisterSlashCommands = () => { | |||
| // Remove command handlers from registry system (automatically calls each command's unregister method) | |||
| slashCommandRegistry.unregister('theme') | |||
| slashCommandRegistry.unregister('language') | |||
| slashCommandRegistry.unregister('feedback') | |||
| slashCommandRegistry.unregister('doc') | |||
| slashCommandRegistry.unregister('community') | |||
| slashCommandRegistry.unregister('account') | |||
| } | |||
| export const SlashCommandProvider = () => { | |||
| @@ -1,8 +1,9 @@ | |||
| import type { FC } from 'react' | |||
| import { useEffect } from 'react' | |||
| import { useEffect, useMemo } from 'react' | |||
| import { Command } from 'cmdk' | |||
| import { useTranslation } from 'react-i18next' | |||
| import type { ActionItem } from './actions/types' | |||
| import { slashCommandRegistry } from './actions/commands/registry' | |||
| type Props = { | |||
| actions: Record<string, ActionItem> | |||
| @@ -10,27 +11,57 @@ type Props = { | |||
| searchFilter?: string | |||
| commandValue?: string | |||
| onCommandValueChange?: (value: string) => void | |||
| originalQuery?: string | |||
| } | |||
| const CommandSelector: FC<Props> = ({ actions, onCommandSelect, searchFilter, commandValue, onCommandValueChange }) => { | |||
| const CommandSelector: FC<Props> = ({ actions, onCommandSelect, searchFilter, commandValue, onCommandValueChange, originalQuery }) => { | |||
| const { t } = useTranslation() | |||
| const filteredActions = Object.values(actions).filter((action) => { | |||
| if (!searchFilter) | |||
| return true | |||
| const filterLower = searchFilter.toLowerCase() | |||
| return action.shortcut.toLowerCase().includes(filterLower) | |||
| }) | |||
| // Check if we're in slash command mode | |||
| const isSlashMode = originalQuery?.trim().startsWith('/') || false | |||
| // Get slash commands from registry | |||
| const slashCommands = useMemo(() => { | |||
| if (!isSlashMode) return [] | |||
| const allCommands = slashCommandRegistry.getAllCommands() | |||
| const filter = searchFilter?.toLowerCase() || '' // searchFilter already has '/' removed | |||
| return allCommands.filter((cmd) => { | |||
| if (!filter) return true | |||
| return cmd.name.toLowerCase().includes(filter) | |||
| }).map(cmd => ({ | |||
| key: `/${cmd.name}`, | |||
| shortcut: `/${cmd.name}`, | |||
| title: cmd.name, | |||
| description: cmd.description, | |||
| })) | |||
| }, [isSlashMode, searchFilter]) | |||
| const filteredActions = useMemo(() => { | |||
| if (isSlashMode) return [] | |||
| return Object.values(actions).filter((action) => { | |||
| // Exclude slash action when in @ mode | |||
| if (action.key === '/') return false | |||
| if (!searchFilter) | |||
| return true | |||
| const filterLower = searchFilter.toLowerCase() | |||
| return action.shortcut.toLowerCase().includes(filterLower) | |||
| }) | |||
| }, [actions, searchFilter, isSlashMode]) | |||
| const allItems = isSlashMode ? slashCommands : filteredActions | |||
| useEffect(() => { | |||
| if (filteredActions.length > 0 && onCommandValueChange) { | |||
| const currentValueExists = filteredActions.some(action => action.shortcut === commandValue) | |||
| if (allItems.length > 0 && onCommandValueChange) { | |||
| const currentValueExists = allItems.some(item => item.shortcut === commandValue) | |||
| if (!currentValueExists) | |||
| onCommandValueChange(filteredActions[0].shortcut) | |||
| onCommandValueChange(allItems[0].shortcut) | |||
| } | |||
| }, [searchFilter, filteredActions.length]) | |||
| }, [searchFilter, allItems.length]) | |||
| if (filteredActions.length === 0) { | |||
| if (allItems.length === 0) { | |||
| return ( | |||
| <div className="p-4"> | |||
| <div className="flex items-center justify-center py-8 text-center text-text-tertiary"> | |||
| @@ -50,33 +81,46 @@ const CommandSelector: FC<Props> = ({ actions, onCommandSelect, searchFilter, co | |||
| return ( | |||
| <div className="p-4"> | |||
| <div className="mb-3 text-left text-sm font-medium text-text-secondary"> | |||
| {t('app.gotoAnything.selectSearchType')} | |||
| {isSlashMode ? t('app.gotoAnything.groups.commands') : t('app.gotoAnything.selectSearchType')} | |||
| </div> | |||
| <Command.Group className="space-y-1"> | |||
| {filteredActions.map(action => ( | |||
| {allItems.map(item => ( | |||
| <Command.Item | |||
| key={action.key} | |||
| value={action.shortcut} | |||
| key={item.key} | |||
| value={item.shortcut} | |||
| className="flex cursor-pointer items-center rounded-md | |||
| p-2.5 | |||
| transition-all | |||
| duration-150 hover:bg-state-base-hover aria-[selected=true]:bg-state-base-hover-alt" | |||
| onSelect={() => onCommandSelect(action.shortcut)} | |||
| onSelect={() => onCommandSelect(item.shortcut)} | |||
| > | |||
| <span className="min-w-[4.5rem] text-left font-mono text-xs text-text-tertiary"> | |||
| {action.shortcut} | |||
| {item.shortcut} | |||
| </span> | |||
| <span className="ml-3 text-sm text-text-secondary"> | |||
| {(() => { | |||
| const keyMap: Record<string, string> = { | |||
| '/': 'app.gotoAnything.actions.slashDesc', | |||
| '@app': 'app.gotoAnything.actions.searchApplicationsDesc', | |||
| '@plugin': 'app.gotoAnything.actions.searchPluginsDesc', | |||
| '@knowledge': 'app.gotoAnything.actions.searchKnowledgeBasesDesc', | |||
| '@node': 'app.gotoAnything.actions.searchWorkflowNodesDesc', | |||
| } | |||
| return t(keyMap[action.key]) | |||
| })()} | |||
| {isSlashMode ? ( | |||
| (() => { | |||
| const slashKeyMap: Record<string, string> = { | |||
| '/theme': 'app.gotoAnything.actions.themeCategoryDesc', | |||
| '/language': 'app.gotoAnything.actions.languageChangeDesc', | |||
| '/account': 'app.gotoAnything.actions.accountDesc', | |||
| '/feedback': 'app.gotoAnything.actions.feedbackDesc', | |||
| '/doc': 'app.gotoAnything.actions.docDesc', | |||
| '/community': 'app.gotoAnything.actions.communityDesc', | |||
| } | |||
| return t(slashKeyMap[item.key] || item.description) | |||
| })() | |||
| ) : ( | |||
| (() => { | |||
| const keyMap: Record<string, string> = { | |||
| '@app': 'app.gotoAnything.actions.searchApplicationsDesc', | |||
| '@plugin': 'app.gotoAnything.actions.searchPluginsDesc', | |||
| '@knowledge': 'app.gotoAnything.actions.searchKnowledgeBasesDesc', | |||
| '@node': 'app.gotoAnything.actions.searchWorkflowNodesDesc', | |||
| } | |||
| return t(keyMap[item.key]) | |||
| })() | |||
| )} | |||
| </span> | |||
| </Command.Item> | |||
| ))} | |||
| @@ -226,6 +226,7 @@ const GotoAnything: FC<Props> = ({ | |||
| <div className='mt-3 space-y-1 text-xs text-text-quaternary'> | |||
| <div>{t('app.gotoAnything.searchHint')}</div> | |||
| <div>{t('app.gotoAnything.commandHint')}</div> | |||
| <div>{t('app.gotoAnything.slashHint')}</div> | |||
| </div> | |||
| </div> | |||
| </div>) | |||
| @@ -321,6 +322,7 @@ const GotoAnything: FC<Props> = ({ | |||
| searchFilter={searchQuery.trim().substring(1)} | |||
| commandValue={cmdVal} | |||
| onCommandValueChange={setCmdVal} | |||
| originalQuery={searchQuery.trim()} | |||
| /> | |||
| ) : ( | |||
| Object.entries(groupedResults).map(([type, results], groupIndex) => ( | |||
| @@ -133,7 +133,7 @@ const RunPanel: FC<RunProps> = ({ | |||
| >{t('runLog.tracing')}</div> | |||
| </div> | |||
| {/* panel detail */} | |||
| <div ref={ref} className={cn('relative h-0 grow overflow-y-auto rounded-b-2xl bg-components-panel-bg')}> | |||
| <div ref={ref} className={cn('relative h-0 grow overflow-y-auto rounded-b-xl bg-components-panel-bg')}> | |||
| {loading && ( | |||
| <div className='flex h-full items-center justify-center bg-components-panel-bg'> | |||
| <Loading /> | |||
| @@ -32,7 +32,7 @@ export const useGetPricingPageLanguage = () => { | |||
| return getPricingPageLanguage(locale) | |||
| } | |||
| const defaultDocBaseUrl = 'https://docs.dify.ai' | |||
| export const defaultDocBaseUrl = 'https://docs.dify.ai' | |||
| export const useDocLink = (baseUrl?: string): ((path?: string, pathMap?: { [index: string]: string }) => string) => { | |||
| let baseDocUrl = baseUrl || defaultDocBaseUrl | |||
| baseDocUrl = (baseDocUrl.endsWith('/')) ? baseDocUrl.slice(0, -1) : baseDocUrl | |||
| @@ -279,6 +279,10 @@ const translation = { | |||
| runDesc: 'Führen Sie schnelle Befehle aus (Thema, Sprache, ...)', | |||
| themeCategoryTitle: 'Thema', | |||
| slashDesc: 'Führen Sie Befehle wie /theme, /lang aus', | |||
| accountDesc: 'Gehe zur Kontoseite', | |||
| feedbackDesc: 'Offene Diskussionen zum Feedback der Gemeinschaft', | |||
| communityDesc: 'Offene Discord-Community', | |||
| docDesc: 'Öffnen Sie die Hilfedokumentation', | |||
| }, | |||
| emptyState: { | |||
| noPluginsFound: 'Keine Plugins gefunden', | |||
| @@ -313,6 +317,7 @@ const translation = { | |||
| inScope: 'in {{scope}}s', | |||
| noMatchingCommands: 'Keine übereinstimmenden Befehle gefunden', | |||
| tryDifferentSearch: 'Versuchen Sie es mit einem anderen Suchbegriff', | |||
| slashHint: 'Geben Sie / ein, um alle verfügbaren Befehle anzuzeigen.', | |||
| }, | |||
| } | |||
| @@ -270,6 +270,7 @@ const translation = { | |||
| selectSearchType: 'Choose what to search for', | |||
| searchHint: 'Start typing to search everything instantly', | |||
| commandHint: 'Type @ to browse by category', | |||
| slashHint: 'Type / to see all available commands', | |||
| actions: { | |||
| searchApplications: 'Search Applications', | |||
| searchApplicationsDesc: 'Search and navigate to your applications', | |||
| @@ -293,7 +294,11 @@ const translation = { | |||
| languageCategoryTitle: 'Language', | |||
| languageCategoryDesc: 'Switch interface language', | |||
| languageChangeDesc: 'Change UI language', | |||
| slashDesc: 'Execute commands like /theme, /lang', | |||
| slashDesc: 'Execute commands (type / to see all available commands)', | |||
| accountDesc: 'Navigate to account page', | |||
| communityDesc: 'Open Discord community', | |||
| docDesc: 'Open help documentation', | |||
| feedbackDesc: 'Open community feedback discussions', | |||
| }, | |||
| emptyState: { | |||
| noAppsFound: 'No apps found', | |||
| @@ -277,6 +277,10 @@ const translation = { | |||
| languageCategoryDesc: 'Cambiar el idioma de la interfaz', | |||
| themeCategoryDesc: 'Cambiar el tema de la aplicación', | |||
| slashDesc: 'Ejecuta comandos como /tema, /idioma', | |||
| accountDesc: 'Navegar a la página de cuenta', | |||
| communityDesc: 'Abrir comunidad de Discord', | |||
| feedbackDesc: 'Discusiones de retroalimentación de la comunidad abierta', | |||
| docDesc: 'Abrir la documentación de ayuda', | |||
| }, | |||
| emptyState: { | |||
| noAppsFound: 'No se encontraron aplicaciones', | |||
| @@ -311,6 +315,7 @@ const translation = { | |||
| inScope: 'en {{scope}}s', | |||
| tryDifferentSearch: 'Prueba con un término de búsqueda diferente', | |||
| noMatchingCommands: 'No se encontraron comandos coincidentes', | |||
| slashHint: 'Escribe / para ver todos los comandos disponibles', | |||
| }, | |||
| } | |||
| @@ -277,6 +277,10 @@ const translation = { | |||
| themeSystemDesc: 'به ظاهر سیستمعامل خود پایبند باشید', | |||
| runDesc: 'دستورات سریع اجرا کنید (موضوع، زبان، ...)', | |||
| slashDesc: 'دستورات را مانند /theme، /lang اجرا کنید', | |||
| feedbackDesc: 'بحثهای باز بازخورد جامعه', | |||
| accountDesc: 'به صفحه حساب کاربری بروید', | |||
| communityDesc: 'جامعه دیسکورد باز', | |||
| docDesc: 'مستندات کمک را باز کنید', | |||
| }, | |||
| emptyState: { | |||
| noKnowledgeBasesFound: 'هیچ پایگاه دانش یافت نشد', | |||
| @@ -311,6 +315,7 @@ const translation = { | |||
| inScope: 'در {{scope}}s', | |||
| noMatchingCommands: 'هیچ دستوری منطبق یافت نشد', | |||
| tryDifferentSearch: 'عبارت جستجوی دیگری را امتحان کنید', | |||
| slashHint: 'برای مشاهده تمام دستورات موجود / را تایپ کنید', | |||
| }, | |||
| } | |||
| @@ -277,6 +277,10 @@ const translation = { | |||
| languageCategoryDesc: 'Changer la langue de l\'interface', | |||
| runDesc: 'Exécuter des commandes rapides (thème, langue, ...)', | |||
| slashDesc: 'Exécutez des commandes telles que /theme, /lang', | |||
| communityDesc: 'Ouvrir la communauté Discord', | |||
| docDesc: 'Ouvrir la documentation d\'aide', | |||
| accountDesc: 'Accédez à la page de compte', | |||
| feedbackDesc: 'Discussions de rétroaction de la communauté ouverte', | |||
| }, | |||
| emptyState: { | |||
| noKnowledgeBasesFound: 'Aucune base de connaissances trouvée', | |||
| @@ -311,6 +315,7 @@ const translation = { | |||
| inScope: 'dans {{scope}}s', | |||
| noMatchingCommands: 'Aucune commande correspondante n’a été trouvée', | |||
| tryDifferentSearch: 'Essayez un autre terme de recherche', | |||
| slashHint: 'Tapez / pour voir toutes les commandes disponibles', | |||
| }, | |||
| } | |||
| @@ -277,6 +277,10 @@ const translation = { | |||
| themeSystemDesc: 'अपने ऑपरेटिंग सिस्टम की उपस्थिति का पालन करें', | |||
| runDesc: 'त्वरित कमांड चलाएँ (थीम, भाषा, ...)', | |||
| slashDesc: 'कमांड्स चलाएं जैसे /theme, /lang', | |||
| accountDesc: 'खाता पृष्ठ पर जाएं', | |||
| docDesc: 'सहायता दस्तावेज़ खोलें', | |||
| communityDesc: 'ओपन डिस्कॉर्ड समुदाय', | |||
| feedbackDesc: 'खुले समुदाय की फीडबैक चर्चाएँ', | |||
| }, | |||
| emptyState: { | |||
| noPluginsFound: 'कोई प्लगइन नहीं मिले', | |||
| @@ -311,6 +315,7 @@ const translation = { | |||
| inScope: '{{scope}}s में', | |||
| tryDifferentSearch: 'एक अलग खोज शब्द आजमाएँ', | |||
| noMatchingCommands: 'कोई मिलती-जुलती कमांड्स नहीं मिलीं', | |||
| slashHint: 'सभी उपलब्ध कमांड देखने के लिए टाइप करें /', | |||
| }, | |||
| } | |||
| @@ -283,6 +283,10 @@ const translation = { | |||
| runDesc: 'Esegui comandi rapidi (tema, lingua, ...)', | |||
| themeSystemDesc: 'Segui l\'aspetto del tuo sistema operativo', | |||
| slashDesc: 'Esegui comandi come /theme, /lang', | |||
| communityDesc: 'Apri la community di Discord', | |||
| accountDesc: 'Vai alla pagina dell\'account', | |||
| feedbackDesc: 'Discussioni di feedback della comunità aperta', | |||
| docDesc: 'Apri la documentazione di aiuto', | |||
| }, | |||
| emptyState: { | |||
| noKnowledgeBasesFound: 'Nessuna base di conoscenza trovata', | |||
| @@ -317,6 +321,7 @@ const translation = { | |||
| inScope: 'in {{scope}}s', | |||
| tryDifferentSearch: 'Prova un termine di ricerca diverso', | |||
| noMatchingCommands: 'Nessun comando corrispondente trovato', | |||
| slashHint: 'Digita / per vedere tutti i comandi disponibili', | |||
| }, | |||
| } | |||
| @@ -292,6 +292,10 @@ const translation = { | |||
| themeCategoryDesc: 'アプリケーションのテーマを切り替える', | |||
| runDesc: 'クイックコマンドを実行する(テーマ、言語、...)', | |||
| slashDesc: 'コマンドを実行します、例えば /theme や /lang のように', | |||
| accountDesc: 'アカウントページに移動する', | |||
| docDesc: 'ヘルプドキュメントを開く', | |||
| communityDesc: 'オープンDiscordコミュニティ', | |||
| feedbackDesc: 'オープンなコミュニティフィードバックディスカッション', | |||
| }, | |||
| emptyState: { | |||
| noAppsFound: 'アプリが見つかりません', | |||
| @@ -310,6 +314,7 @@ const translation = { | |||
| }, | |||
| noMatchingCommands: '一致するコマンドが見つかりません', | |||
| tryDifferentSearch: '別の検索語句をお試しください', | |||
| slashHint: '/を入力して、利用可能なすべてのコマンドを表示します。', | |||
| }, | |||
| } | |||
| @@ -297,6 +297,10 @@ const translation = { | |||
| runDesc: '빠른 명령 실행 (테마, 언어 등...)', | |||
| themeSystemDesc: '운영 체제의 외관을 따르세요', | |||
| slashDesc: '/theme, /lang와 같은 명령어를 실행하십시오.', | |||
| communityDesc: '오픈 디스코드 커뮤니티', | |||
| feedbackDesc: '공개 커뮤니티 피드백 토론', | |||
| docDesc: '도움 문서 열기', | |||
| accountDesc: '계정 페이지로 이동', | |||
| }, | |||
| emptyState: { | |||
| noAppsFound: '앱을 찾을 수 없습니다.', | |||
| @@ -331,6 +335,7 @@ const translation = { | |||
| inScope: '{{scope}}s 내에서', | |||
| tryDifferentSearch: '다른 검색어 사용해 보기', | |||
| noMatchingCommands: '일치하는 명령을 찾을 수 없습니다.', | |||
| slashHint: '모든 사용 가능한 명령을 보려면 /를 입력하세요.', | |||
| }, | |||
| } | |||
| @@ -278,6 +278,10 @@ const translation = { | |||
| themeSystemDesc: 'Podążaj za wyglądem swojego systemu operacyjnego', | |||
| runDesc: 'Uruchom szybkie polecenia (motyw, język, ...)', | |||
| slashDesc: 'Wykonuj polecenia takie jak /theme, /lang', | |||
| communityDesc: 'Otwarta społeczność Discord', | |||
| docDesc: 'Otwórz dokumentację pomocy', | |||
| accountDesc: 'Przejdź do strony konta', | |||
| feedbackDesc: 'Otwarte dyskusje na temat opinii społeczności', | |||
| }, | |||
| emptyState: { | |||
| noAppsFound: 'Nie znaleziono aplikacji', | |||
| @@ -312,6 +316,7 @@ const translation = { | |||
| inScope: 'w {{scope}}s', | |||
| noMatchingCommands: 'Nie znaleziono pasujących poleceń', | |||
| tryDifferentSearch: 'Spróbuj użyć innego hasła', | |||
| slashHint: 'Wpisz / aby zobaczyć wszystkie dostępne polecenia', | |||
| }, | |||
| } | |||
| @@ -277,6 +277,10 @@ const translation = { | |||
| themeSystemDesc: 'Siga a aparência do seu sistema operacional', | |||
| languageCategoryDesc: 'Mudar o idioma da interface', | |||
| slashDesc: 'Execute comandos como /tema, /idioma', | |||
| accountDesc: 'Navegue até a página da conta', | |||
| communityDesc: 'Comunidade do Discord aberta', | |||
| feedbackDesc: 'Discussões de feedback da comunidade aberta', | |||
| docDesc: 'Abra a documentação de ajuda', | |||
| }, | |||
| emptyState: { | |||
| noAppsFound: 'Nenhum aplicativo encontrado', | |||
| @@ -311,6 +315,7 @@ const translation = { | |||
| inScope: 'em {{scope}}s', | |||
| noMatchingCommands: 'Nenhum comando correspondente encontrado', | |||
| tryDifferentSearch: 'Tente um termo de pesquisa diferente', | |||
| slashHint: 'Digite / para ver todos os comandos disponíveis', | |||
| }, | |||
| } | |||
| @@ -277,6 +277,10 @@ const translation = { | |||
| languageCategoryDesc: 'Schimbați limba interfeței', | |||
| themeSystemDesc: 'Urmăriți aspectul sistemului de operare', | |||
| slashDesc: 'Execută comenzi precum /theme, /lang', | |||
| feedbackDesc: 'Discuții de feedback deschis pentru comunitate', | |||
| docDesc: 'Deschide documentația de ajutor', | |||
| communityDesc: 'Deschide comunitatea Discord', | |||
| accountDesc: 'Navigați la pagina de cont', | |||
| }, | |||
| emptyState: { | |||
| noAppsFound: 'Nu s-au găsit aplicații', | |||
| @@ -311,6 +315,7 @@ const translation = { | |||
| inScope: 'în {{scope}}s', | |||
| noMatchingCommands: 'Nu s-au găsit comenzi potrivite', | |||
| tryDifferentSearch: 'Încercați un alt termen de căutare', | |||
| slashHint: 'Tastați / pentru a vedea toate comenzile disponibile', | |||
| }, | |||
| } | |||
| @@ -277,6 +277,10 @@ const translation = { | |||
| themeLightDesc: 'Используйте светлый внешний вид', | |||
| themeSystemDesc: 'Следуйте внешнему виду вашей операционной системы', | |||
| slashDesc: 'Выполняйте команды, такие как /theme, /lang', | |||
| accountDesc: 'Перейдите на страницу учетной записи', | |||
| feedbackDesc: 'Обсуждения обратной связи с открытым сообществом', | |||
| docDesc: 'Откройте справочную документацию', | |||
| communityDesc: 'Открытое сообщество Discord', | |||
| }, | |||
| emptyState: { | |||
| noPluginsFound: 'Плагины не найдены', | |||
| @@ -311,6 +315,7 @@ const translation = { | |||
| inScope: 'в {{scope}}s', | |||
| noMatchingCommands: 'Соответствующие команды не найдены', | |||
| tryDifferentSearch: 'Попробуйте использовать другой поисковый запрос', | |||
| slashHint: 'Введите / чтобы увидеть все доступные команды', | |||
| }, | |||
| } | |||