瀏覽代碼

Merge branch 'main' into feat/rag-2

# Conflicts:
#	api/core/app/entities/queue_entities.py
#	api/core/workflow/graph_engine/entities/event.py
tags/2.0.0-beta.1
jyong 2 月之前
父節點
當前提交
d0dd728e6c
共有 100 個檔案被更改,包括 5141 行新增2433 行删除
  1. 19
    0
      .claude/settings.json.example
  2. 34
    0
      .mcp.json
  3. 1
    0
      CLAUDE.md
  4. 12
    8
      api/controllers/console/app/message.py
  5. 1
    1
      api/controllers/service_api/dataset/metadata.py
  6. 1
    1
      api/controllers/service_api/wraps.py
  7. 55
    8
      api/controllers/web/app.py
  8. 38
    2
      api/controllers/web/audio.py
  9. 80
    0
      api/controllers/web/completion.py
  10. 16
    1
      api/controllers/web/conversation.py
  11. 1
    1
      api/controllers/web/login.py
  12. 10
    0
      api/controllers/web/message.py
  13. 10
    0
      api/controllers/web/saved_message.py
  14. 12
    0
      api/controllers/web/site.py
  15. 35
    0
      api/controllers/web/workflow.py
  16. 1
    1
      api/core/app/entities/queue_entities.py
  17. 2
    2
      api/core/app/task_pipeline/exc.py
  18. 1
    1
      api/core/model_runtime/model_providers/__base/tokenizers/gpt2_tokenizer.py
  19. 1
    1
      api/core/ops/ops_trace_manager.py
  20. 7
    7
      api/core/plugin/backwards_invocation/model.py
  21. 2
    2
      api/core/rag/datasource/vdb/analyticdb/analyticdb_vector_sql.py
  22. 2
    2
      api/core/rag/datasource/vdb/opengauss/opengauss.py
  23. 1
    1
      api/core/rag/datasource/vdb/opensearch/opensearch_vector.py
  24. 2
    2
      api/core/rag/datasource/vdb/pgvector/pgvector.py
  25. 2
    2
      api/core/rag/datasource/vdb/pyvastbase/vastbase_vector.py
  26. 1
    2
      api/core/rag/datasource/vdb/tidb_on_qdrant/tidb_on_qdrant_vector.py
  27. 1
    1
      api/core/rag/extractor/excel_extractor.py
  28. 2
    2
      api/core/rag/extractor/extract_processor.py
  29. 1
    1
      api/core/rag/extractor/html_extractor.py
  30. 1
    1
      api/core/rag/extractor/unstructured/unstructured_eml_extractor.py
  31. 1
    1
      api/core/rag/splitter/text_splitter.py
  32. 1
    1
      api/core/tools/utils/parser.py
  33. 0
    0
      api/core/workflow/graph_engine/entities/event.py
  34. 2
    2
      api/core/workflow/nodes/document_extractor/node.py
  35. 1
    1
      api/extensions/ext_blueprints.py
  36. 1
    1
      api/extensions/ext_otel.py
  37. 3
    3
      api/extensions/storage/aws_s3_storage.py
  38. 1
    1
      api/models/account.py
  39. 2
    2
      api/pyproject.toml
  40. 2
    2
      api/services/webapp_auth_service.py
  41. 1
    1
      api/tasks/delete_conversation_task.py
  42. 2
    5
      api/tests/integration_tests/model_runtime/__mock/plugin_daemon.py
  43. 1
    2
      api/tests/integration_tests/plugin/__mock/http.py
  44. 1
    2
      api/tests/integration_tests/tools/__mock/http.py
  45. 1
    1
      api/tests/integration_tests/vdb/opengauss/test_opengauss.py
  46. 77
    1
      api/tests/test_containers_integration_tests/conftest.py
  47. 1585
    0
      api/tests/test_containers_integration_tests/services/test_workflow_service.py
  48. 11
    5
      api/tests/unit_tests/configs/test_dify_config.py
  49. 0
    0
      api/tests/unit_tests/extensions/storage/__init__.py
  50. 313
    0
      api/tests/unit_tests/extensions/storage/test_supabase_storage.py
  51. 4
    4
      api/tests/unit_tests/extensions/test_ext_request_logging.py
  52. 3
    1
      api/tests/unit_tests/libs/test_datetime_utils.py
  53. 63
    0
      api/tests/unit_tests/libs/test_jwt_imports.py
  54. 1
    1
      api/tests/unit_tests/libs/test_uuid_utils.py
  55. 2
    2
      api/tests/unit_tests/oss/__mock/aliyun_oss.py
  56. 1
    1
      api/tests/unit_tests/oss/aliyun_oss/aliyun_oss/test_aliyun_oss.py
  57. 1
    1
      api/tests/unit_tests/utils/yaml/test_yaml_utils.py
  58. 2222
    2210
      api/uv.lock
  59. 6
    0
      docker/.env.example
  60. 2
    0
      docker/docker-compose-template.yaml
  61. 4
    0
      docker/docker-compose.yaml
  62. 3
    3
      web/Dockerfile
  63. 6
    2
      web/app/components/base/chat/chat-with-history/hooks.tsx
  64. 3
    13
      web/app/components/base/chat/chat/answer/index.tsx
  65. 6
    4
      web/app/components/base/chat/chat/answer/workflow-process.tsx
  66. 12
    5
      web/app/components/develop/template/template.en.mdx
  67. 12
    5
      web/app/components/develop/template/template.ja.mdx
  68. 10
    5
      web/app/components/develop/template/template.zh.mdx
  69. 11
    9
      web/app/components/develop/template/template_advanced_chat.en.mdx
  70. 12
    10
      web/app/components/develop/template/template_advanced_chat.ja.mdx
  71. 6
    6
      web/app/components/develop/template/template_advanced_chat.zh.mdx
  72. 12
    5
      web/app/components/develop/template/template_chat.en.mdx
  73. 12
    5
      web/app/components/develop/template/template_chat.ja.mdx
  74. 9
    4
      web/app/components/develop/template/template_chat.zh.mdx
  75. 10
    8
      web/app/components/develop/template/template_workflow.en.mdx
  76. 12
    10
      web/app/components/develop/template/template_workflow.ja.mdx
  77. 9
    6
      web/app/components/develop/template/template_workflow.zh.mdx
  78. 44
    0
      web/app/components/goto-anything/actions/commands/account.tsx
  79. 43
    0
      web/app/components/goto-anything/actions/commands/community.tsx
  80. 44
    0
      web/app/components/goto-anything/actions/commands/doc.tsx
  81. 43
    0
      web/app/components/goto-anything/actions/commands/feedback.tsx
  82. 12
    0
      web/app/components/goto-anything/actions/commands/slash.tsx
  83. 73
    29
      web/app/components/goto-anything/command-selector.tsx
  84. 2
    0
      web/app/components/goto-anything/index.tsx
  85. 1
    1
      web/app/components/workflow/run/index.tsx
  86. 1
    1
      web/context/i18n.ts
  87. 5
    0
      web/i18n/de-DE/app.ts
  88. 6
    1
      web/i18n/en-US/app.ts
  89. 5
    0
      web/i18n/es-ES/app.ts
  90. 5
    0
      web/i18n/fa-IR/app.ts
  91. 5
    0
      web/i18n/fr-FR/app.ts
  92. 5
    0
      web/i18n/hi-IN/app.ts
  93. 5
    0
      web/i18n/it-IT/app.ts
  94. 5
    0
      web/i18n/ja-JP/app.ts
  95. 5
    0
      web/i18n/ko-KR/app.ts
  96. 5
    0
      web/i18n/pl-PL/app.ts
  97. 5
    0
      web/i18n/pt-BR/app.ts
  98. 5
    0
      web/i18n/ro-RO/app.ts
  99. 5
    0
      web/i18n/ru-RU/app.ts
  100. 0
    0
      web/i18n/sl-SI/app.ts

+ 19
- 0
.claude/settings.json.example 查看文件

@@ -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
}

+ 34
- 0
.mcp.json 查看文件

@@ -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": {}
}
}
}

+ 1
- 0
CLAUDE.md 查看文件

@@ -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.

+ 12
- 8
api/controllers/console/app/message.py 查看文件

@@ -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
- 1
api/controllers/service_api/dataset/metadata.py 查看文件

@@ -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


+ 1
- 1
api/controllers/service_api/wraps.py 查看文件

@@ -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

+ 55
- 8
api/controllers/web/app.py 查看文件

@@ -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")

+ 38
- 2
api/controllers/web/audio.py 查看文件

@@ -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")

+ 80
- 0
api/controllers/web/completion.py 查看文件

@@ -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}:

+ 16
- 1
api/controllers/web/conversation.py 查看文件

@@ -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
- 1
api/controllers/web/login.py 查看文件

@@ -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 (

+ 10
- 0
api/controllers/web/message.py 查看文件

@@ -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}:

+ 10
- 0
api/controllers/web/saved_message.py 查看文件

@@ -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)


+ 12
- 0
api/controllers/web/site.py 查看文件

@@ -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."""

+ 35
- 0
api/controllers/web/workflow.py 查看文件

@@ -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

+ 1
- 1
api/core/app/entities/queue_entities.py 查看文件

@@ -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


+ 2
- 2
api/core/app/task_pipeline/exc.py 查看文件

@@ -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")


+ 1
- 1
api/core/model_runtime/model_providers/__base/tokenizers/gpt2_tokenizer.py 查看文件

@@ -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")

+ 1
- 1
api/core/ops/ops_trace_manager.py 查看文件

@@ -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()

+ 7
- 7
api/core/plugin/backwards_invocation/model.py 查看文件

@@ -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)):

+ 2
- 2
api/core/rag/datasource/vdb/analyticdb/analyticdb_vector_sql.py 查看文件

@@ -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

+ 2
- 2
api/core/rag/datasource/vdb/opengauss/opengauss.py 查看文件

@@ -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

+ 1
- 1
api/core/rag/datasource/vdb/opensearch/opensearch_vector.py 查看文件

@@ -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(),

+ 2
- 2
api/core/rag/datasource/vdb/pgvector/pgvector.py 查看文件

@@ -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

+ 2
- 2
api/core/rag/datasource/vdb/pyvastbase/vastbase_vector.py 查看文件

@@ -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

+ 1
- 2
api/core/rag/datasource/vdb/tidb_on_qdrant/tidb_on_qdrant_vector.py 查看文件

@@ -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

+ 1
- 1
api/core/rag/extractor/excel_extractor.py 查看文件

@@ -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

+ 2
- 2
api/core/rag/extractor/extract_processor.py 查看文件

@@ -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
- 1
api/core/rag/extractor/html_extractor.py 查看文件

@@ -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

+ 1
- 1
api/core/rag/extractor/unstructured/unstructured_eml_extractor.py 查看文件

@@ -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

+ 1
- 1
api/core/rag/splitter/text_splitter.py 查看文件

@@ -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")

+ 1
- 1
api/core/tools/utils/parser.py 查看文件

@@ -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

+ 0
- 0
api/core/workflow/graph_engine/entities/event.py 查看文件


+ 2
- 2
api/core/workflow/nodes/document_extractor/node.py 查看文件

@@ -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:

+ 1
- 1
api/extensions/ext_blueprints.py 查看文件

@@ -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

+ 1
- 1
api/extensions/ext_otel.py 查看文件

@@ -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

+ 3
- 3
api/extensions/storage/aws_s3_storage.py 查看文件

@@ -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

+ 1
- 1
api/models/account.py 查看文件

@@ -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


+ 2
- 2
api/pyproject.toml 查看文件

@@ -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",
]


+ 2
- 2
api/services/webapp_auth_service.py 查看文件

@@ -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:

+ 1
- 1
api/tasks/delete_conversation_task.py 查看文件

@@ -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

+ 2
- 5
api/tests/integration_tests/model_runtime/__mock/plugin_daemon.py 查看文件

@@ -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)


+ 1
- 2
api/tests/integration_tests/plugin/__mock/http.py 查看文件

@@ -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)


+ 1
- 2
api/tests/integration_tests/tools/__mock/http.py 查看文件

@@ -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
- 1
api/tests/integration_tests/vdb/opengauss/test_opengauss.py 查看文件

@@ -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 (

+ 77
- 1
api/tests/test_containers_integration_tests/conftest.py 查看文件

@@ -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:

+ 1585
- 0
api/tests/test_containers_integration_tests/services/test_workflow_service.py
文件差異過大導致無法顯示
查看文件


+ 11
- 5
api/tests/unit_tests/configs/test_dify_config.py 查看文件

@@ -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
api/tests/unit_tests/extensions/storage/__init__.py 查看文件


+ 313
- 0
api/tests/unit_tests/extensions/storage/test_supabase_storage.py 查看文件

@@ -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

+ 4
- 4
api/tests/unit_tests/extensions/test_ext_request_logging.py 查看文件

@@ -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)



+ 3
- 1
api/tests/unit_tests/libs/test_datetime_utils.py 查看文件

@@ -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:

+ 63
- 0
api/tests/unit_tests/libs/test_jwt_imports.py 查看文件

@@ -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}")

+ 1
- 1
api/tests/unit_tests/libs/test_uuid_utils.py 查看文件

@@ -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)

+ 2
- 2
api/tests/unit_tests/oss/__mock/aliyun_oss.py 查看文件

@@ -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
- 1
api/tests/unit_tests/oss/aliyun_oss/aliyun_oss/test_aliyun_oss.py 查看文件

@@ -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
- 1
api/tests/unit_tests/utils/yaml/test_yaml_utils.py 查看文件

@@ -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


+ 2222
- 2210
api/uv.lock
文件差異過大導致無法顯示
查看文件


+ 6
- 0
docker/.env.example 查看文件

@@ -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

+ 2
- 0
docker/docker-compose-template.yaml 查看文件

@@ -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:

+ 4
- 0
docker/docker-compose.yaml 查看文件

@@ -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:

+ 3
- 3
web/Dockerfile 查看文件

@@ -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

+ 6
- 2
web/app/components/base/chat/chat-with-history/hooks.tsx 查看文件

@@ -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()

+ 3
- 13
web/app/components/base/chat/chat/answer/index.tsx 查看文件

@@ -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}
/>
)
}

+ 6
- 4
web/app/components/base/chat/chat/answer/workflow-process.tsx 查看文件

@@ -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

+ 12
- 5
web/app/components/develop/template/template.en.mdx 查看文件

@@ -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>


+ 12
- 5
web/app/components/develop/template/template.ja.mdx 查看文件

@@ -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>


+ 10
- 5
web/app/components/develop/template/template.zh.mdx 查看文件

@@ -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>

+ 11
- 9
web/app/components/develop/template/template_advanced_chat.en.mdx 查看文件

@@ -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`.

+ 12
- 10
web/app/components/develop/template/template_advanced_chat.ja.mdx 查看文件

@@ -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`。

+ 6
- 6
web/app/components/develop/template/template_advanced_chat.zh.mdx 查看文件

@@ -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'>

+ 12
- 5
web/app/components/develop/template/template_chat.en.mdx 查看文件

@@ -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`.

+ 12
- 5
web/app/components/develop/template/template_chat.ja.mdx 查看文件

@@ -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`です。

+ 9
- 4
web/app/components/develop/template/template_chat.zh.mdx 查看文件

@@ -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'>

+ 10
- 8
web/app/components/develop/template/template_workflow.en.mdx 查看文件

@@ -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:

+ 12
- 10
web/app/components/develop/template/template_workflow.ja.mdx 查看文件

@@ -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))を通じてタイプライターのような出力を実装します。

+ 9
- 6
web/app/components/develop/template/template_workflow.zh.mdx 查看文件

@@ -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)**)实现类似打字机输出方式的流式返回。

+ 44
- 0
web/app/components/goto-anything/actions/commands/account.tsx 查看文件

@@ -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'])
},
}

+ 43
- 0
web/app/components/goto-anything/actions/commands/community.tsx 查看文件

@@ -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'])
},
}

+ 44
- 0
web/app/components/goto-anything/actions/commands/doc.tsx 查看文件

@@ -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'])
},
}

+ 43
- 0
web/app/components/goto-anything/actions/commands/feedback.tsx 查看文件

@@ -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'])
},
}

+ 12
- 0
web/app/components/goto-anything/actions/commands/slash.tsx 查看文件

@@ -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 = () => {

+ 73
- 29
web/app/components/goto-anything/command-selector.tsx 查看文件

@@ -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>
))}

+ 2
- 0
web/app/components/goto-anything/index.tsx 查看文件

@@ -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) => (

+ 1
- 1
web/app/components/workflow/run/index.tsx 查看文件

@@ -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 />

+ 1
- 1
web/context/i18n.ts 查看文件

@@ -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

+ 5
- 0
web/i18n/de-DE/app.ts 查看文件

@@ -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.',
},
}


+ 6
- 1
web/i18n/en-US/app.ts 查看文件

@@ -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',

+ 5
- 0
web/i18n/es-ES/app.ts 查看文件

@@ -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',
},
}


+ 5
- 0
web/i18n/fa-IR/app.ts 查看文件

@@ -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: 'برای مشاهده تمام دستورات موجود / را تایپ کنید',
},
}


+ 5
- 0
web/i18n/fr-FR/app.ts 查看文件

@@ -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',
},
}


+ 5
- 0
web/i18n/hi-IN/app.ts 查看文件

@@ -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: 'सभी उपलब्ध कमांड देखने के लिए टाइप करें /',
},
}


+ 5
- 0
web/i18n/it-IT/app.ts 查看文件

@@ -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',
},
}


+ 5
- 0
web/i18n/ja-JP/app.ts 查看文件

@@ -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: '/を入力して、利用可能なすべてのコマンドを表示します。',
},
}


+ 5
- 0
web/i18n/ko-KR/app.ts 查看文件

@@ -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: '모든 사용 가능한 명령을 보려면 /를 입력하세요.',
},
}


+ 5
- 0
web/i18n/pl-PL/app.ts 查看文件

@@ -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',
},
}


+ 5
- 0
web/i18n/pt-BR/app.ts 查看文件

@@ -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',
},
}


+ 5
- 0
web/i18n/ro-RO/app.ts 查看文件

@@ -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',
},
}


+ 5
- 0
web/i18n/ru-RU/app.ts 查看文件

@@ -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: 'Введите / чтобы увидеть все доступные команды',
},
}


+ 0
- 0
web/i18n/sl-SI/app.ts 查看文件


部分文件因文件數量過多而無法顯示

Loading…
取消
儲存