From b2ac11bc47b77e4a2dede3799fe34cc15ce9deda Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9D=9E=E6=B3=95=E6=93=8D=E4=BD=9C?= Date: Thu, 12 Jun 2025 14:18:15 +0800 Subject: [PATCH 001/329] fix: markdown button can't send message (#20933) --- web/app/components/base/markdown-blocks/button.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/app/components/base/markdown-blocks/button.tsx b/web/app/components/base/markdown-blocks/button.tsx index 4646b12921..315653bcd0 100644 --- a/web/app/components/base/markdown-blocks/button.tsx +++ b/web/app/components/base/markdown-blocks/button.tsx @@ -14,7 +14,7 @@ const MarkdownButton = ({ node }: any) => { size={size} className={cn('!h-auto min-h-8 select-none whitespace-normal !px-3')} onClick={() => { - if (isValidUrl(link)) { + if (link && isValidUrl(link)) { window.open(link, '_blank') return } From c05e47ebc0017caba71f1787a10523a96070a53f Mon Sep 17 00:00:00 2001 From: -LAN- Date: Fri, 13 Jun 2025 09:42:02 +0800 Subject: [PATCH 002/329] refactor(sqlalchemy_workflow_execution_repository): Use the max funtion for getting next_sequence_number. (#20966) --- .../sqlalchemy_workflow_execution_repository.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/api/core/repositories/sqlalchemy_workflow_execution_repository.py b/api/core/repositories/sqlalchemy_workflow_execution_repository.py index 19086cffff..e5ead9dc56 100644 --- a/api/core/repositories/sqlalchemy_workflow_execution_repository.py +++ b/api/core/repositories/sqlalchemy_workflow_execution_repository.py @@ -6,7 +6,7 @@ import json import logging from typing import Optional, Union -from sqlalchemy import select +from sqlalchemy import func, select from sqlalchemy.engine import Engine from sqlalchemy.orm import sessionmaker @@ -151,11 +151,11 @@ class SQLAlchemyWorkflowExecutionRepository(WorkflowExecutionRepository): existing = session.scalar(select(WorkflowRun).where(WorkflowRun.id == domain_model.id_)) if not existing: # For new records, get the next sequence number - stmt = select(WorkflowRun.sequence_number).where( + stmt = select(func.max(WorkflowRun.sequence_number)).where( WorkflowRun.app_id == self._app_id, WorkflowRun.tenant_id == self._tenant_id, ) - max_sequence = session.scalar(stmt.order_by(WorkflowRun.sequence_number.desc())) + max_sequence = session.scalar(stmt) db_model.sequence_number = (max_sequence or 0) + 1 else: # For updates, keep the existing sequence number From 8cb3ed5cc2d51ddb391dfc80cdc96d2b71556d3a Mon Sep 17 00:00:00 2001 From: "Byron.wang" Date: Fri, 13 Jun 2025 15:05:55 +0800 Subject: [PATCH 003/329] feat: add S3_USE_AWS env var to explicitly distinguish AWS S3 usage in plugin-daemon (#20923) --- docker/.env.example | 1 + docker/docker-compose-template.yaml | 1 + docker/docker-compose.middleware.yaml | 1 + docker/docker-compose.yaml | 2 ++ docker/middleware.env.example | 1 + 5 files changed, 6 insertions(+) diff --git a/docker/.env.example b/docker/.env.example index 4cf5e202d0..d4d59936eb 100644 --- a/docker/.env.example +++ b/docker/.env.example @@ -1067,6 +1067,7 @@ PLUGIN_MEDIA_CACHE_PATH=assets # Plugin oss bucket PLUGIN_STORAGE_OSS_BUCKET= # Plugin oss s3 credentials +PLUGIN_S3_USE_AWS= PLUGIN_S3_USE_AWS_MANAGED_IAM=false PLUGIN_S3_ENDPOINT= PLUGIN_S3_USE_PATH_STYLE=false diff --git a/docker/docker-compose-template.yaml b/docker/docker-compose-template.yaml index a409a729ce..648b82c918 100644 --- a/docker/docker-compose-template.yaml +++ b/docker/docker-compose-template.yaml @@ -168,6 +168,7 @@ services: PLUGIN_MEDIA_CACHE_PATH: ${PLUGIN_MEDIA_CACHE_PATH:-assets} PLUGIN_STORAGE_OSS_BUCKET: ${PLUGIN_STORAGE_OSS_BUCKET:-} S3_USE_AWS_MANAGED_IAM: ${PLUGIN_S3_USE_AWS_MANAGED_IAM:-false} + S3_USE_AWS: ${PLUGIN_S3_USE_AWS:-} S3_ENDPOINT: ${PLUGIN_S3_ENDPOINT:-} S3_USE_PATH_STYLE: ${PLUGIN_S3_USE_PATH_STYLE:-false} AWS_ACCESS_KEY: ${PLUGIN_AWS_ACCESS_KEY:-} diff --git a/docker/docker-compose.middleware.yaml b/docker/docker-compose.middleware.yaml index dceee484ca..4081bfd818 100644 --- a/docker/docker-compose.middleware.yaml +++ b/docker/docker-compose.middleware.yaml @@ -104,6 +104,7 @@ services: PLUGIN_PACKAGE_CACHE_PATH: ${PLUGIN_PACKAGE_CACHE_PATH:-plugin_packages} PLUGIN_MEDIA_CACHE_PATH: ${PLUGIN_MEDIA_CACHE_PATH:-assets} PLUGIN_STORAGE_OSS_BUCKET: ${PLUGIN_STORAGE_OSS_BUCKET:-} + S3_USE_AWS: ${PLUGIN_S3_USE_AWS:-} S3_USE_AWS_MANAGED_IAM: ${PLUGIN_S3_USE_AWS_MANAGED_IAM:-false} S3_ENDPOINT: ${PLUGIN_S3_ENDPOINT:-} S3_USE_PATH_STYLE: ${PLUGIN_S3_USE_PATH_STYLE:-false} diff --git a/docker/docker-compose.yaml b/docker/docker-compose.yaml index d927334118..ec910f6258 100644 --- a/docker/docker-compose.yaml +++ b/docker/docker-compose.yaml @@ -467,6 +467,7 @@ x-shared-env: &shared-api-worker-env PLUGIN_PACKAGE_CACHE_PATH: ${PLUGIN_PACKAGE_CACHE_PATH:-plugin_packages} PLUGIN_MEDIA_CACHE_PATH: ${PLUGIN_MEDIA_CACHE_PATH:-assets} PLUGIN_STORAGE_OSS_BUCKET: ${PLUGIN_STORAGE_OSS_BUCKET:-} + PLUGIN_S3_USE_AWS: ${PLUGIN_S3_USE_AWS:-} PLUGIN_S3_USE_AWS_MANAGED_IAM: ${PLUGIN_S3_USE_AWS_MANAGED_IAM:-false} PLUGIN_S3_ENDPOINT: ${PLUGIN_S3_ENDPOINT:-} PLUGIN_S3_USE_PATH_STYLE: ${PLUGIN_S3_USE_PATH_STYLE:-false} @@ -674,6 +675,7 @@ services: PLUGIN_MEDIA_CACHE_PATH: ${PLUGIN_MEDIA_CACHE_PATH:-assets} PLUGIN_STORAGE_OSS_BUCKET: ${PLUGIN_STORAGE_OSS_BUCKET:-} S3_USE_AWS_MANAGED_IAM: ${PLUGIN_S3_USE_AWS_MANAGED_IAM:-false} + S3_USE_AWS: ${PLUGIN_S3_USE_AWS:-} S3_ENDPOINT: ${PLUGIN_S3_ENDPOINT:-} S3_USE_PATH_STYLE: ${PLUGIN_S3_USE_PATH_STYLE:-false} AWS_ACCESS_KEY: ${PLUGIN_AWS_ACCESS_KEY:-} diff --git a/docker/middleware.env.example b/docker/middleware.env.example index 66037f281c..338b057ae8 100644 --- a/docker/middleware.env.example +++ b/docker/middleware.env.example @@ -133,6 +133,7 @@ PLUGIN_MEDIA_CACHE_PATH=assets PLUGIN_STORAGE_OSS_BUCKET= # Plugin oss s3 credentials PLUGIN_S3_USE_AWS_MANAGED_IAM=false +PLUGIN_S3_USE_AWS= PLUGIN_S3_ENDPOINT= PLUGIN_S3_USE_PATH_STYLE=false PLUGIN_AWS_ACCESS_KEY= From 175571e74028043bada19135f52fe4a6293ad974 Mon Sep 17 00:00:00 2001 From: sayThQ199 <18852951350@163.com> Date: Fri, 13 Jun 2025 16:17:12 +0800 Subject: [PATCH 004/329] fix(auth): Clear login rate limit after password reset (#20948) --- api/commands.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/api/commands.py b/api/commands.py index 6262bfde6b..0a6cc61a68 100644 --- a/api/commands.py +++ b/api/commands.py @@ -27,7 +27,7 @@ from models.dataset import Dataset, DatasetCollectionBinding, DatasetMetadata, D from models.dataset import Document as DatasetDocument from models.model import Account, App, AppAnnotationSetting, AppMode, Conversation, MessageAnnotation from models.provider import Provider, ProviderModel -from services.account_service import RegisterService, TenantService +from services.account_service import AccountService, RegisterService, TenantService from services.clear_free_plan_tenant_expired_logs import ClearFreePlanTenantExpiredLogs from services.plugin.data_migration import PluginDataMigration from services.plugin.plugin_migration import PluginMigration @@ -68,6 +68,7 @@ def reset_password(email, new_password, password_confirm): account.password = base64_password_hashed account.password_salt = base64_salt db.session.commit() + AccountService.reset_login_error_rate_limit(email) click.echo(click.style("Password reset successfully.", fg="green")) From 3a628bc671b4e0b13648ee575d619633134d9266 Mon Sep 17 00:00:00 2001 From: quicksand Date: Fri, 13 Jun 2025 16:17:35 +0800 Subject: [PATCH 005/329] chore: app info add author_name (#20973) --- api/controllers/service_api/app/app.py | 8 +++++++- web/app/components/develop/template/template.en.mdx | 6 +++++- web/app/components/develop/template/template.ja.mdx | 6 +++++- web/app/components/develop/template/template.zh.mdx | 6 +++++- .../develop/template/template_advanced_chat.en.mdx | 6 +++++- .../develop/template/template_advanced_chat.ja.mdx | 6 +++++- .../develop/template/template_advanced_chat.zh.mdx | 4 +++- web/app/components/develop/template/template_chat.en.mdx | 6 +++++- web/app/components/develop/template/template_chat.ja.mdx | 6 +++++- web/app/components/develop/template/template_chat.zh.mdx | 6 +++++- .../components/develop/template/template_workflow.en.mdx | 6 +++++- .../components/develop/template/template_workflow.ja.mdx | 6 +++++- .../components/develop/template/template_workflow.zh.mdx | 6 +++++- 13 files changed, 65 insertions(+), 13 deletions(-) diff --git a/api/controllers/service_api/app/app.py b/api/controllers/service_api/app/app.py index 2c03aba33d..89222d5e83 100644 --- a/api/controllers/service_api/app/app.py +++ b/api/controllers/service_api/app/app.py @@ -47,7 +47,13 @@ class AppInfoApi(Resource): def get(self, app_model: App): """Get app information""" tags = [tag.name for tag in app_model.tags] - return {"name": app_model.name, "description": app_model.description, "tags": tags, "mode": app_model.mode} + return { + "name": app_model.name, + "description": app_model.description, + "tags": tags, + "mode": app_model.mode, + "author_name": app_model.author_name, + } api.add_resource(AppParameterApi, "/parameters") diff --git a/web/app/components/develop/template/template.en.mdx b/web/app/components/develop/template/template.en.mdx index 9682b51b39..f1b0b60dda 100755 --- a/web/app/components/develop/template/template.en.mdx +++ b/web/app/components/develop/template/template.en.mdx @@ -512,6 +512,8 @@ The text generation application offers non-session support and is ideal for tran - `name` (string) application name - `description` (string) application description - `tags` (array[string]) application tags + - `mode` (string) application mode + - `author_name` (string) author name @@ -528,7 +530,9 @@ The text generation application offers non-session support and is ideal for tran "tags": [ "tag1", "tag2" - ] + ], + "mode": "chat", + "author_name": "Dify" } ``` diff --git a/web/app/components/develop/template/template.ja.mdx b/web/app/components/develop/template/template.ja.mdx index fc6291f522..2cbd87cf36 100755 --- a/web/app/components/develop/template/template.ja.mdx +++ b/web/app/components/develop/template/template.ja.mdx @@ -510,6 +510,8 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from - `name` (string) アプリケーションの名前 - `description` (string) アプリケーションの説明 - `tags` (array[string]) アプリケーションのタグ + - `mode` (string) アプリケーションのモード + - `author_name` (string) 作者の名前 @@ -526,7 +528,9 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from "tags": [ "tag1", "tag2" - ] + ], + "mode": "chat", + "author_name": "Dify" } ``` diff --git a/web/app/components/develop/template/template.zh.mdx b/web/app/components/develop/template/template.zh.mdx index 9e65a4bd9b..64740d8930 100755 --- a/web/app/components/develop/template/template.zh.mdx +++ b/web/app/components/develop/template/template.zh.mdx @@ -485,6 +485,8 @@ import { Row, Col, Properties, Property, Heading, SubProperty } from '../md.tsx' - `name` (string) 应用名称 - `description` (string) 应用描述 - `tags` (array[string]) 应用标签 + - `mode` (string) 应用模式 + - 'author_name' (string) 作者名称 @@ -501,7 +503,9 @@ import { Row, Col, Properties, Property, Heading, SubProperty } from '../md.tsx' "tags": [ "tag1", "tag2" - ] + ], + "mode": "chat", + "author_name": "Dify" } ``` diff --git a/web/app/components/develop/template/template_advanced_chat.en.mdx b/web/app/components/develop/template/template_advanced_chat.en.mdx index 76e336bdf4..3a9ef73b04 100644 --- a/web/app/components/develop/template/template_advanced_chat.en.mdx +++ b/web/app/components/develop/template/template_advanced_chat.en.mdx @@ -1123,6 +1123,8 @@ Chat applications support session persistence, allowing previous chat history to - `name` (string) application name - `description` (string) application description - `tags` (array[string]) application tags + - `mode` (string) application mode + - `author_name` (string) application author name @@ -1139,7 +1141,9 @@ Chat applications support session persistence, allowing previous chat history to "tags": [ "tag1", "tag2" - ] + ], + "mode": "advanced-chat", + "author_name": "Dify" } ``` diff --git a/web/app/components/develop/template/template_advanced_chat.ja.mdx b/web/app/components/develop/template/template_advanced_chat.ja.mdx index 6f94cbb2c7..72c5885045 100644 --- a/web/app/components/develop/template/template_advanced_chat.ja.mdx +++ b/web/app/components/develop/template/template_advanced_chat.ja.mdx @@ -1123,6 +1123,8 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from - `name` (string) アプリケーションの名前 - `description` (string) アプリケーションの説明 - `tags` (array[string]) アプリケーションのタグ + - `mode` (string) アプリケーションのモード + - `author_name` (string) 作者の名前 @@ -1139,7 +1141,9 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from "tags": [ "tag1", "tag2" - ] + ], + "mode": "advanced-chat", + "author_name": "Dify" } ``` diff --git a/web/app/components/develop/template/template_advanced_chat.zh.mdx b/web/app/components/develop/template/template_advanced_chat.zh.mdx index 3e268d6e65..7559dd507c 100755 --- a/web/app/components/develop/template/template_advanced_chat.zh.mdx +++ b/web/app/components/develop/template/template_advanced_chat.zh.mdx @@ -1173,7 +1173,9 @@ import { Row, Col, Properties, Property, Heading, SubProperty } from '../md.tsx' "tags": [ "tag1", "tag2" - ] + ], + "mode": "advanced-chat", + "author_name": "Dify" } ``` diff --git a/web/app/components/develop/template/template_chat.en.mdx b/web/app/components/develop/template/template_chat.en.mdx index f0cb3cd513..e1fdeb690e 100644 --- a/web/app/components/develop/template/template_chat.en.mdx +++ b/web/app/components/develop/template/template_chat.en.mdx @@ -1151,6 +1151,8 @@ Chat applications support session persistence, allowing previous chat history to - `name` (string) application name - `description` (string) application description - `tags` (array[string]) application tags + - `mode` (string) application mode + - `author_name` (string) application author name @@ -1167,7 +1169,9 @@ Chat applications support session persistence, allowing previous chat history to "tags": [ "tag1", "tag2" - ] + ], + "mode": "advanced-chat", + "author_name": "Dify" } ``` diff --git a/web/app/components/develop/template/template_chat.ja.mdx b/web/app/components/develop/template/template_chat.ja.mdx index a1b2ae5f1d..e62ff4ff42 100644 --- a/web/app/components/develop/template/template_chat.ja.mdx +++ b/web/app/components/develop/template/template_chat.ja.mdx @@ -1150,6 +1150,8 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from - `name` (string) アプリケーションの名前 - `description` (string) アプリケーションの説明 - `tags` (array[string]) アプリケーションのタグ + - `mode` (string) アプリケーションのモード + - `author_name` (string) 作者の名前 @@ -1166,7 +1168,9 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from "tags": [ "tag1", "tag2" - ] + ], + "mode": "chat", + "author_name": "Dify" } ``` diff --git a/web/app/components/develop/template/template_chat.zh.mdx b/web/app/components/develop/template/template_chat.zh.mdx index 9c1a168bf5..f6714c95f0 100644 --- a/web/app/components/develop/template/template_chat.zh.mdx +++ b/web/app/components/develop/template/template_chat.zh.mdx @@ -1162,6 +1162,8 @@ import { Row, Col, Properties, Property, Heading, SubProperty } from '../md.tsx' - `name` (string) 应用名称 - `description` (string) 应用描述 - `tags` (array[string]) 应用标签 + - `mode` (string) 应用模式 + - 'author_name' (string) 作者名称 @@ -1178,7 +1180,9 @@ import { Row, Col, Properties, Property, Heading, SubProperty } from '../md.tsx' "tags": [ "tag1", "tag2" - ] + ], + "mode": "chat", + "author_name": "Dify" } ``` diff --git a/web/app/components/develop/template/template_workflow.en.mdx b/web/app/components/develop/template/template_workflow.en.mdx index 95e4971e67..9aa5d60c57 100644 --- a/web/app/components/develop/template/template_workflow.en.mdx +++ b/web/app/components/develop/template/template_workflow.en.mdx @@ -625,6 +625,8 @@ Workflow applications offers non-session support and is ideal for translation, a - `name` (string) application name - `description` (string) application description - `tags` (array[string]) application tags + - `mode` (string) application mode + - `author_name` (string) application author name @@ -641,7 +643,9 @@ Workflow applications offers non-session support and is ideal for translation, a "tags": [ "tag1", "tag2" - ] + ], + "mode": "workflow", + "author_name": "Dify" } ``` diff --git a/web/app/components/develop/template/template_workflow.ja.mdx b/web/app/components/develop/template/template_workflow.ja.mdx index ab53d05e81..e957de2872 100644 --- a/web/app/components/develop/template/template_workflow.ja.mdx +++ b/web/app/components/develop/template/template_workflow.ja.mdx @@ -628,6 +628,8 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from - `name` (string) アプリケーションの名前 - `description` (string) アプリケーションの説明 - `tags` (array[string]) アプリケーションのタグ + - `mode` (string) アプリケーションのモード + - `author_name` (string) 作者の名前 @@ -644,7 +646,9 @@ import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from "tags": [ "tag1", "tag2" - ] + ], + "mode": "workflow", + "author_name": "Dify" } ``` diff --git a/web/app/components/develop/template/template_workflow.zh.mdx b/web/app/components/develop/template/template_workflow.zh.mdx index fe59988eda..f8b05fea19 100644 --- a/web/app/components/develop/template/template_workflow.zh.mdx +++ b/web/app/components/develop/template/template_workflow.zh.mdx @@ -615,6 +615,8 @@ Workflow 应用无会话支持,适合用于翻译/文章写作/总结 AI 等 - `name` (string) 应用名称 - `description` (string) 应用描述 - `tags` (array[string]) 应用标签 + - `mode` (string) 应用模式 + - 'author_name' (string) 作者名称 @@ -631,7 +633,9 @@ Workflow 应用无会话支持,适合用于翻译/文章写作/总结 AI 等 "tags": [ "tag1", "tag2" - ] + ], + "mode": "workflow", + "author_name": "Dify" } ``` From f4df759ba6282b0c0c0f9e03b8dac171cdba8318 Mon Sep 17 00:00:00 2001 From: Bowen Liang Date: Fri, 13 Jun 2025 16:58:43 +0800 Subject: [PATCH 006/329] refactor: generalize method for getting doc link respecting locale and fix error link paths (#20801) --- .../[datasetId]/layout-main.tsx | 11 +++-------- .../conversation-history/history-panel.tsx | 11 ++++------- .../dataset-config/settings-modal/index.tsx | 4 +++- .../prompt-mode/advanced-mode-waring.tsx | 8 +++----- .../components/app/create-app-modal/index.tsx | 13 ++++++++----- .../app/overview/customize/index.tsx | 11 +++-------- .../app/overview/settings/index.tsx | 10 +++++----- .../base/features/new-feature-panel/index.tsx | 8 +++----- .../datasets/create/step-two/index.tsx | 6 +++++- .../create/website/base/url-input.tsx | 4 +++- .../website/jina-reader/base/url-input.tsx | 4 +++- .../components/datasets/documents/index.tsx | 11 +++-------- .../external-api/external-api-modal/Form.tsx | 4 +++- .../external-api/external-api-panel/index.tsx | 5 ++++- .../create/InfoPanel.tsx | 9 +++++++-- .../external-knowledge-base/create/index.tsx | 4 +++- .../hit-testing/modify-retrieval-modal.tsx | 4 +++- .../datasets/settings/form/index.tsx | 4 +++- .../header/account-dropdown/index.tsx | 6 +++--- .../plugin-detail-panel/endpoint-list.tsx | 8 +++----- .../tools/provider/custom-create-card.tsx | 8 +++++--- .../nodes/_base/components/agent-strategy.tsx | 15 ++++++--------- .../components/error-handle/default-value.tsx | 4 +++- .../error-handle/fail-branch-card.tsx | 4 +++- .../variable/var-reference-popup.tsx | 12 ++++++------ .../nodes/_base/hooks/use-node-help-link.ts | 8 +++----- .../json-schema-config.tsx | 6 +++--- .../panel/chat-variable-panel/index.tsx | 13 +++++++------ web/app/components/workflow/run/node.tsx | 4 +++- web/app/components/workflow/run/status.tsx | 4 +++- .../education-apply/education-apply-page.tsx | 16 +++------------- .../education-apply/verify-state-modal.tsx | 19 ++++++------------- web/app/install/installForm.tsx | 4 +++- web/app/signin/invite-settings/page.tsx | 6 ++++-- web/app/signin/oneMoreStep.tsx | 4 +++- web/context/i18n.ts | 13 +++++++++++++ 36 files changed, 149 insertions(+), 136 deletions(-) diff --git a/web/app/(commonLayout)/datasets/(datasetDetailLayout)/[datasetId]/layout-main.tsx b/web/app/(commonLayout)/datasets/(datasetDetailLayout)/[datasetId]/layout-main.tsx index 94cd5ad562..fb3a9087ca 100644 --- a/web/app/(commonLayout)/datasets/(datasetDetailLayout)/[datasetId]/layout-main.tsx +++ b/web/app/(commonLayout)/datasets/(datasetDetailLayout)/[datasetId]/layout-main.tsx @@ -25,9 +25,8 @@ import Loading from '@/app/components/base/loading' import DatasetDetailContext from '@/context/dataset-detail' import { DataSourceType } from '@/models/datasets' import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints' -import { LanguagesSupported } from '@/i18n/language' import { useStore } from '@/app/components/app/store' -import { getLocaleOnClient } from '@/i18n' +import { useDocLink } from '@/context/i18n' import { useAppContext } from '@/context/app-context' import Tooltip from '@/app/components/base/tooltip' import LinkedAppsPanel from '@/app/components/base/linked-apps-panel' @@ -45,9 +44,9 @@ type IExtraInfoProps = { } const ExtraInfo = ({ isMobile, relatedApps, expand }: IExtraInfoProps) => { - const locale = getLocaleOnClient() const [isShowTips, { toggle: toggleTips, set: setShowTips }] = useBoolean(!isMobile) const { t } = useTranslation() + const docLink = useDocLink() const hasRelatedApps = relatedApps?.data && relatedApps?.data?.length > 0 const relatedAppsTotal = relatedApps?.data?.length || 0 @@ -97,11 +96,7 @@ const ExtraInfo = ({ isMobile, relatedApps, expand }: IExtraInfoProps) => {
{t('common.datasetMenus.emptyTip')}
diff --git a/web/app/components/app/configuration/config-prompt/conversation-history/history-panel.tsx b/web/app/components/app/configuration/config-prompt/conversation-history/history-panel.tsx index 592c95261c..94a02945bb 100644 --- a/web/app/components/app/configuration/config-prompt/conversation-history/history-panel.tsx +++ b/web/app/components/app/configuration/config-prompt/conversation-history/history-panel.tsx @@ -1,13 +1,11 @@ 'use client' import type { FC } from 'react' import React from 'react' -import { useContext } from 'use-context-selector' import { useTranslation } from 'react-i18next' import OperationBtn from '@/app/components/app/configuration/base/operation-btn' import Panel from '@/app/components/app/configuration/base/feature-panel' import { MessageClockCircle } from '@/app/components/base/icons/src/vender/solid/general' -import I18n from '@/context/i18n' -import { LanguagesSupported } from '@/i18n/language' +import { useDocLink } from '@/context/i18n' type Props = { showWarning: boolean @@ -19,7 +17,7 @@ const HistoryPanel: FC = ({ onShowEditModal, }) => { const { t } = useTranslation() - const { locale } = useContext(I18n) + const docLink = useDocLink() return ( = ({ {showWarning && (
{t('appDebug.feature.conversationHistory.tip')} - {t('appDebug.feature.conversationHistory.learnMore')} diff --git a/web/app/components/app/configuration/dataset-config/settings-modal/index.tsx b/web/app/components/app/configuration/dataset-config/settings-modal/index.tsx index 3170d33a82..9835481ae0 100644 --- a/web/app/components/app/configuration/dataset-config/settings-modal/index.tsx +++ b/web/app/components/app/configuration/dataset-config/settings-modal/index.tsx @@ -31,6 +31,7 @@ import { import { ModelTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations' import { fetchMembers } from '@/service/common' import type { Member } from '@/models/common' +import { useDocLink } from '@/context/i18n' type SettingsModalProps = { currentDataset: DataSet @@ -58,6 +59,7 @@ const SettingsModal: FC = ({ currentModel: isRerankDefaultModelValid, } = useModelListAndDefaultModelAndCurrentProviderAndModel(ModelTypeEnum.rerank) const { t } = useTranslation() + const docLink = useDocLink() const { notify } = useToastContext() const ref = useRef(null) const isExternal = currentDataset.provider === 'external' @@ -328,7 +330,7 @@ const SettingsModal: FC = ({
{t('datasetSettings.form.retrievalSetting.title')}
diff --git a/web/app/components/app/configuration/prompt-mode/advanced-mode-waring.tsx b/web/app/components/app/configuration/prompt-mode/advanced-mode-waring.tsx index cca775c86e..f207cddd16 100644 --- a/web/app/components/app/configuration/prompt-mode/advanced-mode-waring.tsx +++ b/web/app/components/app/configuration/prompt-mode/advanced-mode-waring.tsx @@ -2,9 +2,7 @@ import type { FC } from 'react' import React from 'react' import { useTranslation } from 'react-i18next' -import { useContext } from 'use-context-selector' -import I18n from '@/context/i18n' -import { LanguagesSupported } from '@/i18n/language' +import { useDocLink } from '@/context/i18n' type Props = { onReturnToSimpleMode: () => void } @@ -13,7 +11,7 @@ const AdvancedModeWarning: FC = ({ onReturnToSimpleMode, }) => { const { t } = useTranslation() - const { locale } = useContext(I18n) + const docLink = useDocLink() const [show, setShow] = React.useState(true) if (!show) return null @@ -25,7 +23,7 @@ const AdvancedModeWarning: FC = ({ {t('appDebug.promptMode.advancedWarning.description')} {t('appDebug.promptMode.advancedWarning.learnMore')} diff --git a/web/app/components/app/create-app-modal/index.tsx b/web/app/components/app/create-app-modal/index.tsx index 6e5547d08a..bfb7c43c0d 100644 --- a/web/app/components/app/create-app-modal/index.tsx +++ b/web/app/components/app/create-app-modal/index.tsx @@ -29,6 +29,7 @@ import { NEED_REFRESH_APP_LIST_KEY } from '@/config' import { getRedirection } from '@/utils/app-redirection' import FullScreenModal from '@/app/components/base/fullscreen-modal' import useTheme from '@/hooks/use-theme' +import { useDocLink } from '@/context/i18n' type CreateAppProps = { onSuccess: () => void @@ -303,31 +304,33 @@ function AppTypeCard({ icon, title, description, active, onClick }: AppTypeCardP function AppPreview({ mode }: { mode: AppMode }) { const { t } = useTranslation() + const docLink = useDocLink() const modeToPreviewInfoMap = { 'chat': { title: t('app.types.chatbot'), description: t('app.newApp.chatbotUserDescription'), - link: 'https://docs.dify.ai/guides/application-orchestrate/readme', + link: docLink('/guides/application-orchestrate/chatbot-application'), }, 'advanced-chat': { title: t('app.types.advanced'), description: t('app.newApp.advancedUserDescription'), - link: 'https://docs.dify.ai/en/guides/workflow/README', + link: docLink('/guides/workflow/readme'), }, 'agent-chat': { title: t('app.types.agent'), description: t('app.newApp.agentUserDescription'), - link: 'https://docs.dify.ai/en/guides/application-orchestrate/agent', + link: docLink('/guides/application-orchestrate/agent'), }, 'completion': { title: t('app.newApp.completeApp'), description: t('app.newApp.completionUserDescription'), - link: null, + link: docLink('/guides/application-orchestrate/text-generator', + { 'zh-Hans': '/guides/application-orchestrate/readme' }), }, 'workflow': { title: t('app.types.workflow'), description: t('app.newApp.workflowUserDescription'), - link: 'https://docs.dify.ai/en/guides/workflow/README', + link: docLink('/guides/workflow/readme'), }, } const previewInfo = modeToPreviewInfoMap[mode] diff --git a/web/app/components/app/overview/customize/index.tsx b/web/app/components/app/overview/customize/index.tsx index 4e84dd8b1f..0fedd76f89 100644 --- a/web/app/components/app/overview/customize/index.tsx +++ b/web/app/components/app/overview/customize/index.tsx @@ -3,13 +3,11 @@ import type { FC } from 'react' import React from 'react' import { ArrowTopRightOnSquareIcon } from '@heroicons/react/24/outline' import { useTranslation } from 'react-i18next' -import { useContext } from 'use-context-selector' +import { useDocLink } from '@/context/i18n' import type { AppMode } from '@/types/app' -import I18n from '@/context/i18n' import Button from '@/app/components/base/button' import Modal from '@/app/components/base/modal' import Tag from '@/app/components/base/tag' -import { LanguagesSupported } from '@/i18n/language' type IShareLinkProps = { isShow: boolean @@ -43,7 +41,7 @@ const CustomizeModal: FC = ({ mode, }) => { const { t } = useTranslation() - const { locale } = useContext(I18n) + const docLink = useDocLink() const isChatApp = mode === 'chat' || mode === 'advanced-chat' return = ({ className='mt-2' onClick={() => window.open( - `https://docs.dify.ai/${locale !== LanguagesSupported[1] - ? 'user-guide/launching-dify-apps/developing-with-apis' - : `${locale.toLowerCase()}/guides/application-publishing/developing-with-apis` - }`, + docLink('/guides/application-publishing/developing-with-apis'), '_blank', ) } diff --git a/web/app/components/app/overview/settings/index.tsx b/web/app/components/app/overview/settings/index.tsx index 98e2cab681..c2d98383c4 100644 --- a/web/app/components/app/overview/settings/index.tsx +++ b/web/app/components/app/overview/settings/index.tsx @@ -4,7 +4,6 @@ import React, { useCallback, useEffect, useState } from 'react' import { RiArrowRightSLine, RiCloseLine } from '@remixicon/react' import Link from 'next/link' import { Trans, useTranslation } from 'react-i18next' -import { useContext } from 'use-context-selector' import { SparklesSoft } from '@/app/components/base/icons/src/public/common' import Modal from '@/app/components/base/modal' import ActionButton from '@/app/components/base/action-button' @@ -19,14 +18,14 @@ import { SimpleSelect } from '@/app/components/base/select' import type { AppDetailResponse } from '@/models/app' import type { AppIconType, AppSSO, Language } from '@/types/app' import { useToastContext } from '@/app/components/base/toast' -import { LanguagesSupported, languages } from '@/i18n/language' +import { languages } from '@/i18n/language' import Tooltip from '@/app/components/base/tooltip' import { useProviderContext } from '@/context/provider-context' import { useModalContext } from '@/context/modal-context' import type { AppIconSelection } from '@/app/components/base/app-icon-picker' import AppIconPicker from '@/app/components/base/app-icon-picker' -import I18n from '@/context/i18n' import cn from '@/utils/classnames' +import { useDocLink } from '@/context/i18n' export type ISettingsModalProps = { isChat: boolean @@ -98,7 +97,7 @@ const SettingsModal: FC = ({ const [language, setLanguage] = useState(default_language) const [saveLoading, setSaveLoading] = useState(false) const { t } = useTranslation() - const { locale } = useContext(I18n) + const docLink = useDocLink() const [showAppIconPicker, setShowAppIconPicker] = useState(false) const [appIcon, setAppIcon] = useState( @@ -238,7 +237,8 @@ const SettingsModal: FC = ({
{t(`${prefixSettings}.modalTip`)} - {t('common.operation.learnMore')} + {t('common.operation.learnMore')}
{/* form body */} diff --git a/web/app/components/base/features/new-feature-panel/index.tsx b/web/app/components/base/features/new-feature-panel/index.tsx index eee680596b..d00cd9038a 100644 --- a/web/app/components/base/features/new-feature-panel/index.tsx +++ b/web/app/components/base/features/new-feature-panel/index.tsx @@ -1,6 +1,5 @@ import React from 'react' import { useTranslation } from 'react-i18next' -import { useContext } from 'use-context-selector' import { RiCloseLine, RiInformation2Fill } from '@remixicon/react' import DialogWrapper from '@/app/components/base/features/new-feature-panel/dialog-wrapper' import { useDefaultModel } from '@/app/components/header/account-setting/model-provider-page/hooks' @@ -19,8 +18,7 @@ import Moderation from '@/app/components/base/features/new-feature-panel/moderat import AnnotationReply from '@/app/components/base/features/new-feature-panel/annotation-reply' import type { PromptVariable } from '@/models/debug' import type { InputVar } from '@/app/components/workflow/types' -import I18n from '@/context/i18n' -import { LanguagesSupported } from '@/i18n/language' +import { useDocLink } from '@/context/i18n' type Props = { show: boolean @@ -48,7 +46,7 @@ const NewFeaturePanel = ({ onAutoAddPromptVariable, }: Props) => { const { t } = useTranslation() - const { locale } = useContext(I18n) + const docLink = useDocLink() const { data: speech2textDefaultModel } = useDefaultModel(ModelTypeEnum.speech2text) const { data: text2speechDefaultModel } = useDefaultModel(ModelTypeEnum.tts) @@ -80,7 +78,7 @@ const NewFeaturePanel = ({ {isChatMode ? t('workflow.common.fileUploadTip') : t('workflow.common.ImageUploadLegacyTip')} {t('workflow.common.featuresDocLink')} diff --git a/web/app/components/datasets/create/step-two/index.tsx b/web/app/components/datasets/create/step-two/index.tsx index 52c5195bbf..c931addd1a 100644 --- a/web/app/components/datasets/create/step-two/index.tsx +++ b/web/app/components/datasets/create/step-two/index.tsx @@ -63,6 +63,7 @@ import CustomDialog from '@/app/components/base/dialog' import { PortalToFollowElem, PortalToFollowElemContent, PortalToFollowElemTrigger } from '@/app/components/base/portal-to-follow-elem' import { AlertTriangle } from '@/app/components/base/icons/src/vender/solid/alertsAndFeedback' import { noop } from 'lodash-es' +import { useDocLink } from '@/context/i18n' const TextLabel: FC = (props) => { return @@ -146,6 +147,7 @@ const StepTwo = ({ updateRetrievalMethodCache, }: StepTwoProps) => { const { t } = useTranslation() + const docLink = useDocLink() const { locale } = useContext(I18n) const media = useBreakpoints() const isMobile = media === MediaType.mobile @@ -962,7 +964,9 @@ const StepTwo = ({
{t('datasetSettings.form.retrievalSetting.title')}
diff --git a/web/app/components/datasets/create/website/base/url-input.tsx b/web/app/components/datasets/create/website/base/url-input.tsx index b7dc9bfca5..ab965bebc3 100644 --- a/web/app/components/datasets/create/website/base/url-input.tsx +++ b/web/app/components/datasets/create/website/base/url-input.tsx @@ -4,6 +4,7 @@ import React, { useCallback, useState } from 'react' import { useTranslation } from 'react-i18next' import Input from './input' import Button from '@/app/components/base/button' +import { useDocLink } from '@/context/i18n' const I18N_PREFIX = 'datasetCreation.stepOne.website' @@ -17,6 +18,7 @@ const UrlInput: FC = ({ onRun, }) => { const { t } = useTranslation() + const docLink = useDocLink() const [url, setUrl] = useState('') const handleUrlChange = useCallback((url: string | number) => { setUrl(url as string) @@ -32,7 +34,7 @@ const UrlInput: FC = ({ + + + + ) +} + +export default MCPServerModal diff --git a/web/app/components/tools/mcp/mcp-server-param-item.tsx b/web/app/components/tools/mcp/mcp-server-param-item.tsx new file mode 100644 index 0000000000..a48d1b92b0 --- /dev/null +++ b/web/app/components/tools/mcp/mcp-server-param-item.tsx @@ -0,0 +1,37 @@ +'use client' +import React from 'react' +import { useTranslation } from 'react-i18next' +import Textarea from '@/app/components/base/textarea' + +type Props = { + data?: any + value: string + onChange: (value: string) => void +} + +const MCPServerParamItem = ({ + data, + value, + onChange, +}: Props) => { + const { t } = useTranslation() + + return ( +
+
+
{data.label}
+
·
+
{data.variable}
+
{data.type}
+
+ +
+ ) +} + +export default MCPServerParamItem diff --git a/web/app/components/tools/mcp/mcp-service-card.tsx b/web/app/components/tools/mcp/mcp-service-card.tsx new file mode 100644 index 0000000000..443d7a1d1f --- /dev/null +++ b/web/app/components/tools/mcp/mcp-service-card.tsx @@ -0,0 +1,244 @@ +'use client' +import React, { useEffect, useMemo, useState } from 'react' +import { useTranslation } from 'react-i18next' +import { + RiLoopLeftLine, +} from '@remixicon/react' +import { + Mcp, +} from '@/app/components/base/icons/src/vender/other' +import Button from '@/app/components/base/button' +import Tooltip from '@/app/components/base/tooltip' +import Switch from '@/app/components/base/switch' +import Divider from '@/app/components/base/divider' +import CopyFeedback from '@/app/components/base/copy-feedback' +import Confirm from '@/app/components/base/confirm' +import type { AppDetailResponse } from '@/models/app' +import { useAppContext } from '@/context/app-context' +import type { AppSSO } from '@/types/app' +import Indicator from '@/app/components/header/indicator' +import MCPServerModal from '@/app/components/tools/mcp/mcp-server-modal' +import { useAppWorkflow } from '@/service/use-workflow' +import { + useInvalidateMCPServerDetail, + useMCPServerDetail, + useRefreshMCPServerCode, + useUpdateMCPServer, +} from '@/service/use-tools' +import { BlockEnum } from '@/app/components/workflow/types' +import cn from '@/utils/classnames' +import { fetchAppDetail } from '@/service/apps' + +export type IAppCardProps = { + appInfo: AppDetailResponse & Partial +} + +function MCPServiceCard({ + appInfo, +}: IAppCardProps) { + const { t } = useTranslation() + const appId = appInfo.id + const { mutateAsync: updateMCPServer } = useUpdateMCPServer() + const { mutateAsync: refreshMCPServerCode, isPending: genLoading } = useRefreshMCPServerCode() + const invalidateMCPServerDetail = useInvalidateMCPServerDetail() + const { isCurrentWorkspaceManager, isCurrentWorkspaceEditor } = useAppContext() + const [showConfirmDelete, setShowConfirmDelete] = useState(false) + const [showMCPServerModal, setShowMCPServerModal] = useState(false) + + const isAdvancedApp = appInfo?.mode === 'advanced-chat' || appInfo?.mode === 'workflow' + const isBasicApp = !isAdvancedApp + const { data: currentWorkflow } = useAppWorkflow(isAdvancedApp ? appId : '') + const [basicAppConfig, setBasicAppConfig] = useState({}) + const basicAppInputForm = useMemo(() => { + if(!isBasicApp || !basicAppConfig?.user_input_form) + return [] + return basicAppConfig.user_input_form.map((item: any) => { + const type = Object.keys(item)[0] + return { + ...item[type], + type: type || 'text-input', + } + }) + }, [basicAppConfig.user_input_form, isBasicApp]) + useEffect(() => { + if(isBasicApp && appId) { + (async () => { + const res = await fetchAppDetail({ url: '/apps', id: appId }) + setBasicAppConfig(res?.model_config || {}) + })() + } + }, [appId, isBasicApp]) + const { data: detail } = useMCPServerDetail(appId) + const { id, status, server_code } = detail ?? {} + + const appUnpublished = isAdvancedApp ? !currentWorkflow?.graph : !basicAppConfig.updated_at + const serverPublished = !!id + const serverActivated = status === 'active' + const serverURL = serverPublished ? `${appInfo.api_base_url.replace('/v1', '')}/mcp/server/${server_code}/mcp` : '***********' + const toggleDisabled = !isCurrentWorkspaceEditor || appUnpublished + + const [activated, setActivated] = useState(serverActivated) + + const latestParams = useMemo(() => { + if(isAdvancedApp) { + if (!currentWorkflow?.graph) + return [] + const startNode = currentWorkflow?.graph.nodes.find(node => node.data.type === BlockEnum.Start) as any + return startNode?.data.variables as any[] || [] + } + return basicAppInputForm + }, [currentWorkflow, basicAppInputForm, isAdvancedApp]) + + const onGenCode = async () => { + await refreshMCPServerCode(detail?.id || '') + invalidateMCPServerDetail(appId) + } + + const onChangeStatus = async (state: boolean) => { + setActivated(state) + if (state) { + if (!serverPublished) { + setShowMCPServerModal(true) + return + } + + await updateMCPServer({ + appID: appId, + id: id || '', + description: detail?.description || '', + parameters: detail?.parameters || {}, + status: 'active', + }) + invalidateMCPServerDetail(appId) + } + else { + await updateMCPServer({ + appID: appId, + id: id || '', + description: detail?.description || '', + parameters: detail?.parameters || {}, + status: 'inactive', + }) + invalidateMCPServerDetail(appId) + } + } + + const handleServerModalHide = () => { + setShowMCPServerModal(false) + if (!serverActivated) + setActivated(false) + } + + useEffect(() => { + setActivated(serverActivated) + }, [serverActivated]) + + if (!currentWorkflow && isAdvancedApp) + return null + + return ( + <> +
+
+
+
+
+
+ +
+
+
+ {t('tools.mcp.server.title')} +
+
+
+
+ +
+ {serverActivated + ? t('appOverview.overview.status.running') + : t('appOverview.overview.status.disable')} +
+
+ +
+ +
+
+
+
+
+ {t('tools.mcp.server.url')} +
+
+
+
+ {serverURL} +
+
+ {serverPublished && ( + <> + + + {isCurrentWorkspaceManager && ( + +
setShowConfirmDelete(true)} + > + +
+
+ )} + + )} +
+
+
+
+ +
+
+
+ {showMCPServerModal && ( + + )} + {/* button copy link/ button regenerate */} + {showConfirmDelete && ( + { + onGenCode() + setShowConfirmDelete(false) + }} + onCancel={() => setShowConfirmDelete(false)} + /> + )} + + ) +} + +export default MCPServiceCard diff --git a/web/app/components/tools/mcp/mock.ts b/web/app/components/tools/mcp/mock.ts new file mode 100644 index 0000000000..f271f67ed3 --- /dev/null +++ b/web/app/components/tools/mcp/mock.ts @@ -0,0 +1,154 @@ +const tools = [ + { + author: 'Novice', + name: 'NOTION_ADD_PAGE_CONTENT', + label: { + en_US: 'NOTION_ADD_PAGE_CONTENT', + zh_Hans: 'NOTION_ADD_PAGE_CONTENT', + pt_BR: 'NOTION_ADD_PAGE_CONTENT', + ja_JP: 'NOTION_ADD_PAGE_CONTENT', + }, + description: { + en_US: 'Adds a single content block to a notion page. multiple calls needed for multiple blocks. note: only supports adding to notion pages. blocks that can contain children: - page (any block type) - toggle (any nested content) - to-do (nested to-dos/blocks) - bulleted list (nested lists/blocks) - numbered list (nested lists/blocks) - callout (child blocks) - quote (nested blocks)', + zh_Hans: 'Adds a single content block to a notion page. multiple calls needed for multiple blocks. note: only supports adding to notion pages. blocks that can contain children: - page (any block type) - toggle (any nested content) - to-do (nested to-dos/blocks) - bulleted list (nested lists/blocks) - numbered list (nested lists/blocks) - callout (child blocks) - quote (nested blocks)', + pt_BR: 'Adds a single content block to a notion page. multiple calls needed for multiple blocks. note: only supports adding to notion pages. blocks that can contain children: - page (any block type) - toggle (any nested content) - to-do (nested to-dos/blocks) - bulleted list (nested lists/blocks) - numbered list (nested lists/blocks) - callout (child blocks) - quote (nested blocks)', + ja_JP: 'Adds a single content block to a notion page. multiple calls needed for multiple blocks. note: only supports adding to notion pages. blocks that can contain children: - page (any block type) - toggle (any nested content) - to-do (nested to-dos/blocks) - bulleted list (nested lists/blocks) - numbered list (nested lists/blocks) - callout (child blocks) - quote (nested blocks)', + }, + parameters: [ + { + name: 'after', + label: { + en_US: 'after', + zh_Hans: 'after', + pt_BR: 'after', + ja_JP: 'after', + }, + placeholder: null, + scope: null, + auto_generate: null, + template: null, + required: false, + default: null, + min: null, + max: null, + precision: null, + options: [], + type: 'string', + human_description: { + en_US: 'The ID of the existing block that the new block should be appended after. If not provided, content will be appended at the end of the page.', + zh_Hans: 'The ID of the existing block that the new block should be appended after. If not provided, content will be appended at the end of the page.', + pt_BR: 'The ID of the existing block that the new block should be appended after. If not provided, content will be appended at the end of the page.', + ja_JP: 'The ID of the existing block that the new block should be appended after. If not provided, content will be appended at the end of the page.', + }, + form: 'llm', + llm_description: 'The ID of the existing block that the new block should be appended after. If not provided, content will be appended at the end of the page.', + }, + { + name: 'content_block', + label: { + en_US: 'content_block', + zh_Hans: 'content_block', + pt_BR: 'content_block', + ja_JP: 'content_block', + }, + placeholder: null, + scope: null, + auto_generate: null, + template: null, + required: false, + default: null, + min: null, + max: null, + precision: null, + options: [], + type: 'string', + human_description: { + en_US: 'Child content to append to a page.', + zh_Hans: 'Child content to append to a page.', + pt_BR: 'Child content to append to a page.', + ja_JP: 'Child content to append to a page.', + }, + form: 'llm', + llm_description: 'Child content to append to a page.', + }, + { + name: 'parent_block_id', + label: { + en_US: 'parent_block_id', + zh_Hans: 'parent_block_id', + pt_BR: 'parent_block_id', + ja_JP: 'parent_block_id', + }, + placeholder: null, + scope: null, + auto_generate: null, + template: null, + required: false, + default: null, + min: null, + max: null, + precision: null, + options: [], + type: 'string', + human_description: { + en_US: 'The ID of the page which the children will be added.', + zh_Hans: 'The ID of the page which the children will be added.', + pt_BR: 'The ID of the page which the children will be added.', + ja_JP: 'The ID of the page which the children will be added.', + }, + form: 'llm', + llm_description: 'The ID of the page which the children will be added.', + }, + ], + labels: [], + output_schema: null, + }, +] + +export const listData = [ + { + id: 'fdjklajfkljadslf111', + author: 'KVOJJJin', + name: 'GOGOGO', + icon: 'https://cloud.dify.dev/console/api/workspaces/694cc430-fa36-4458-86a0-4a98c09c4684/model-providers/langgenius/openai/openai/icon_small/en_US', + server_url: 'https://mcp.composio.dev/notion/****/abc', + type: 'mcp', + is_team_authorization: true, + tools, + update_elapsed_time: 1744793369, + label: { + en_US: 'GOGOGO', + zh_Hans: 'GOGOGO', + }, + }, + { + id: 'fdjklajfkljadslf222', + author: 'KVOJJJin', + name: 'GOGOGO2', + icon: 'https://cloud.dify.dev/console/api/workspaces/694cc430-fa36-4458-86a0-4a98c09c4684/model-providers/langgenius/openai/openai/icon_small/en_US', + server_url: 'https://mcp.composio.dev/notion/****/abc', + type: 'mcp', + is_team_authorization: false, + tools: [], + update_elapsed_time: 1744793369, + label: { + en_US: 'GOGOGO2', + zh_Hans: 'GOGOGO2', + }, + }, + { + id: 'fdjklajfkljadslf333', + author: 'KVOJJJin', + name: 'GOGOGO3', + icon: 'https://cloud.dify.dev/console/api/workspaces/694cc430-fa36-4458-86a0-4a98c09c4684/model-providers/langgenius/openai/openai/icon_small/en_US', + server_url: 'https://mcp.composio.dev/notion/****/abc', + type: 'mcp', + is_team_authorization: true, + tools, + update_elapsed_time: 1744793369, + label: { + en_US: 'GOGOGO3', + zh_Hans: 'GOGOGO3', + }, + }, +] diff --git a/web/app/components/tools/mcp/modal.tsx b/web/app/components/tools/mcp/modal.tsx new file mode 100644 index 0000000000..0e57cb149b --- /dev/null +++ b/web/app/components/tools/mcp/modal.tsx @@ -0,0 +1,221 @@ +'use client' +import React, { useRef, useState } from 'react' +import { useTranslation } from 'react-i18next' +import { getDomain } from 'tldts' +import { RiCloseLine, RiEditLine } from '@remixicon/react' +import AppIconPicker from '@/app/components/base/app-icon-picker' +import type { AppIconSelection } from '@/app/components/base/app-icon-picker' +import AppIcon from '@/app/components/base/app-icon' +import Modal from '@/app/components/base/modal' +import Button from '@/app/components/base/button' +import Input from '@/app/components/base/input' +import type { AppIconType } from '@/types/app' +import type { ToolWithProvider } from '@/app/components/workflow/types' +import { noop } from 'lodash-es' +import Toast from '@/app/components/base/toast' +import { uploadRemoteFileInfo } from '@/service/common' +import cn from '@/utils/classnames' +import { useHover } from 'ahooks' + +export type DuplicateAppModalProps = { + data?: ToolWithProvider + show: boolean + onConfirm: (info: { + name: string + server_url: string + icon_type: AppIconType + icon: string + icon_background?: string | null + server_identifier: string + }) => void + onHide: () => void +} + +const DEFAULT_ICON = { type: 'emoji', icon: '🧿', background: '#EFF1F5' } +const extractFileId = (url: string) => { + const match = url.match(/files\/(.+?)\/file-preview/) + return match ? match[1] : null +} +const getIcon = (data?: ToolWithProvider) => { + if (!data) + return DEFAULT_ICON as AppIconSelection + if (typeof data.icon === 'string') + return { type: 'image', url: data.icon, fileId: extractFileId(data.icon) } as AppIconSelection + return { + ...data.icon, + icon: data.icon.content, + type: 'emoji', + } as unknown as AppIconSelection +} + +const MCPModal = ({ + data, + show, + onConfirm, + onHide, +}: DuplicateAppModalProps) => { + const { t } = useTranslation() + const isCreate = !data + + const originalServerUrl = data?.server_url + const originalServerID = data?.server_identifier + const [url, setUrl] = React.useState(data?.server_url || '') + const [name, setName] = React.useState(data?.name || '') + const [appIcon, setAppIcon] = useState(getIcon(data)) + const [showAppIconPicker, setShowAppIconPicker] = useState(false) + const [serverIdentifier, setServerIdentifier] = React.useState(data?.server_identifier || '') + const [isFetchingIcon, setIsFetchingIcon] = useState(false) + const appIconRef = useRef(null) + const isHovering = useHover(appIconRef) + + const isValidUrl = (string: string) => { + try { + const urlPattern = /^(https?:\/\/)((([a-z\d]([a-z\d-]*[a-z\d])*)\.)+[a-z]{2,}|((\d{1,3}\.){3}\d{1,3}))(\:\d+)?(\/[-a-z\d%_.~+]*)*(\?[;&a-z\d%_.~+=-]*)?/i + return urlPattern.test(string) + } + catch (e) { + return false + } + } + + const isValidServerID = (str: string) => { + return /^[a-z0-9_-]{1,24}$/.test(str) + } + + const handleBlur = async (url: string) => { + if (data) + return + if (!isValidUrl(url)) + return + const domain = getDomain(url) + const remoteIcon = `https://www.google.com/s2/favicons?domain=${domain}&sz=128` + setIsFetchingIcon(true) + try { + const res = await uploadRemoteFileInfo(remoteIcon, undefined, true) + setAppIcon({ type: 'image', url: res.url, fileId: extractFileId(res.url) || '' }) + } + catch (e) { + console.error('Failed to fetch remote icon:', e) + Toast.notify({ type: 'warning', message: 'Failed to fetch remote icon' }) + } + finally { + setIsFetchingIcon(false) + } + } + + const submit = async () => { + if (!isValidUrl(url)) { + Toast.notify({ type: 'error', message: 'invalid server url' }) + return + } + if (!isValidServerID(serverIdentifier.trim())) { + Toast.notify({ type: 'error', message: 'invalid server identifier' }) + return + } + await onConfirm({ + server_url: originalServerUrl === url ? '[__HIDDEN__]' : url.trim(), + name, + icon_type: appIcon.type, + icon: appIcon.type === 'emoji' ? appIcon.icon : appIcon.fileId, + icon_background: appIcon.type === 'emoji' ? appIcon.background : undefined, + server_identifier: serverIdentifier.trim(), + }) + if(isCreate) + onHide() + } + + return ( + <> + +
+ +
+
{!isCreate ? t('tools.mcp.modal.editTitle') : t('tools.mcp.modal.title')}
+
+
+
+ {t('tools.mcp.modal.serverUrl')} +
+ setUrl(e.target.value)} + onBlur={e => handleBlur(e.target.value.trim())} + placeholder={t('tools.mcp.modal.serverUrlPlaceholder')} + /> + {originalServerUrl && originalServerUrl !== url && ( +
+ {t('tools.mcp.modal.serverUrlWarning')} +
+ )} +
+
+
+
+ {t('tools.mcp.modal.name')} +
+ setName(e.target.value)} + placeholder={t('tools.mcp.modal.namePlaceholder')} + /> +
+
+ + +
) : null + } + onClick={() => { setShowAppIconPicker(true) }} + /> +
+
+
+
+ {t('tools.mcp.modal.serverIdentifier')} +
+
{t('tools.mcp.modal.serverIdentifierTip')}
+ setServerIdentifier(e.target.value)} + placeholder={t('tools.mcp.modal.serverIdentifierPlaceholder')} + /> + {originalServerID && originalServerID !== serverIdentifier && ( +
+ {t('tools.mcp.modal.serverIdentifierWarning')} +
+ )} +
+ +
+ + +
+
+ {showAppIconPicker && { + setAppIcon(payload) + setShowAppIconPicker(false) + }} + onClose={() => { + setAppIcon(getIcon(data)) + setShowAppIconPicker(false) + }} + />} + + + ) +} + +export default MCPModal diff --git a/web/app/components/tools/mcp/provider-card.tsx b/web/app/components/tools/mcp/provider-card.tsx new file mode 100644 index 0000000000..677e25c533 --- /dev/null +++ b/web/app/components/tools/mcp/provider-card.tsx @@ -0,0 +1,152 @@ +'use client' +import { useCallback, useState } from 'react' +import { useBoolean } from 'ahooks' +import { useTranslation } from 'react-i18next' +import { useAppContext } from '@/context/app-context' +import { RiHammerFill } from '@remixicon/react' +import Indicator from '@/app/components/header/indicator' +import Icon from '@/app/components/plugins/card/base/card-icon' +import { useFormatTimeFromNow } from './hooks' +import type { ToolWithProvider } from '../../workflow/types' +import Confirm from '@/app/components/base/confirm' +import MCPModal from './modal' +import OperationDropdown from './detail/operation-dropdown' +import { useDeleteMCP, useUpdateMCP } from '@/service/use-tools' +import cn from '@/utils/classnames' + +type Props = { + currentProvider?: ToolWithProvider + data: ToolWithProvider + handleSelect: (providerID: string) => void + onUpdate: (providerID: string) => void + onDeleted: () => void +} + +const MCPCard = ({ + currentProvider, + data, + onUpdate, + handleSelect, + onDeleted, +}: Props) => { + const { t } = useTranslation() + const { formatTimeFromNow } = useFormatTimeFromNow() + const { isCurrentWorkspaceManager } = useAppContext() + + const { mutateAsync: updateMCP } = useUpdateMCP({}) + const { mutateAsync: deleteMCP } = useDeleteMCP({}) + + const [isOperationShow, setIsOperationShow] = useState(false) + + const [isShowUpdateModal, { + setTrue: showUpdateModal, + setFalse: hideUpdateModal, + }] = useBoolean(false) + + const [isShowDeleteConfirm, { + setTrue: showDeleteConfirm, + setFalse: hideDeleteConfirm, + }] = useBoolean(false) + + const [deleting, { + setTrue: showDeleting, + setFalse: hideDeleting, + }] = useBoolean(false) + + const handleUpdate = useCallback(async (form: any) => { + const res = await updateMCP({ + ...form, + provider_id: data.id, + }) + if ((res as any)?.result === 'success') { + hideUpdateModal() + onUpdate(data.id) + } + }, [data, updateMCP, hideUpdateModal, onUpdate]) + + const handleDelete = useCallback(async () => { + showDeleting() + const res = await deleteMCP(data.id) + hideDeleting() + if ((res as any)?.result === 'success') { + hideDeleteConfirm() + onDeleted() + } + }, [showDeleting, deleteMCP, data.id, hideDeleting, hideDeleteConfirm, onDeleted]) + + return ( +
handleSelect(data.id)} + className={cn( + 'group relative flex cursor-pointer flex-col rounded-xl border-[1.5px] border-transparent bg-components-card-bg shadow-xs hover:bg-components-card-bg-alt hover:shadow-md', + currentProvider?.id === data.id && 'border-components-option-card-option-selected-border bg-components-card-bg-alt', + )} + > +
+
+ +
+
+
{data.name}
+
{data.server_identifier}
+
+
+
+
+
+ + {data.tools.length > 0 && ( +
{t('tools.mcp.toolsCount', { count: data.tools.length })}
+ )} + {!data.tools.length && ( +
{t('tools.mcp.noTools')}
+ )} +
+
/
+
{`${t('tools.mcp.updateTime')} ${formatTimeFromNow(data.updated_at! * 1000)}`}
+
+ {data.is_team_authorization && data.tools.length > 0 && } + {(!data.is_team_authorization || !data.tools.length) && ( +
+ {t('tools.mcp.noConfigured')} + +
+ )} +
+ {isCurrentWorkspaceManager && ( + + )} + {isShowUpdateModal && ( + + )} + {isShowDeleteConfirm && ( + + {t('tools.mcp.deleteConfirmTitle', { mcp: data.name })} +
+ } + onCancel={hideDeleteConfirm} + onConfirm={handleDelete} + isLoading={deleting} + isDisabled={deleting} + /> + )} + + ) +} +export default MCPCard diff --git a/web/app/components/tools/provider-list.tsx b/web/app/components/tools/provider-list.tsx index b0b4f8a8bc..ecfa5f6ea2 100644 --- a/web/app/components/tools/provider-list.tsx +++ b/web/app/components/tools/provider-list.tsx @@ -15,11 +15,29 @@ import WorkflowToolEmpty from '@/app/components/tools/add-tool-modal/empty' import Card from '@/app/components/plugins/card' import CardMoreInfo from '@/app/components/plugins/card/card-more-info' import PluginDetailPanel from '@/app/components/plugins/plugin-detail-panel' +import MCPList from './mcp' import { useAllToolProviders } from '@/service/use-tools' import { useInstalledPluginList, useInvalidateInstalledPluginList } from '@/service/use-plugins' import { useGlobalPublicStore } from '@/context/global-public-context' +import { ToolTypeEnum } from '../workflow/block-selector/types' +const getToolType = (type: string) => { + switch (type) { + case 'builtin': + return ToolTypeEnum.BuiltIn + case 'api': + return ToolTypeEnum.Custom + case 'workflow': + return ToolTypeEnum.Workflow + case 'mcp': + return ToolTypeEnum.MCP + default: + return ToolTypeEnum.BuiltIn + } +} const ProviderList = () => { + // const searchParams = useSearchParams() + // searchParams.get('category') === 'workflow' const { t } = useTranslation() const { enable_marketplace } = useGlobalPublicStore(s => s.systemFeatures) const containerRef = useRef(null) @@ -31,6 +49,7 @@ const ProviderList = () => { { value: 'builtin', text: t('tools.type.builtIn') }, { value: 'api', text: t('tools.type.custom') }, { value: 'workflow', text: t('tools.type.workflow') }, + { value: 'mcp', text: 'MCP' }, ] const [tagFilterValue, setTagFilterValue] = useState([]) const handleTagsChange = (value: string[]) => { @@ -85,7 +104,9 @@ const ProviderList = () => { options={options} />
- + {activeTab !== 'mcp' && ( + + )} { />
- {(filteredCollectionList.length > 0 || activeTab !== 'builtin') && ( + {activeTab !== 'mcp' && (
{ />
))} - {!filteredCollectionList.length && activeTab === 'workflow' &&
} + {!filteredCollectionList.length && activeTab === 'workflow' &&
} )} {!filteredCollectionList.length && activeTab === 'builtin' && ( )} - { - enable_marketplace && activeTab === 'builtin' && ( - { - containerRef.current?.scrollTo({ top: containerRef.current.scrollHeight, behavior: 'smooth' }) - }} - searchPluginText={keywords} - filterPluginTags={tagFilterValue} - /> - ) - } - - + {enable_marketplace && activeTab === 'builtin' && ( + { + containerRef.current?.scrollTo({ top: containerRef.current.scrollHeight, behavior: 'smooth' }) + }} + searchPluginText={keywords} + filterPluginTags={tagFilterValue} + /> + )} + {activeTab === 'mcp' && ( + + )} + + {currentProvider && !currentProvider.plugin_id && ( { return ( <> {isCurrentWorkspaceManager && ( -
-
setIsShowEditCustomCollectionModal(true)}> +
+
setIsShowEditCustomCollectionModal(true)}>
-
- +
+
-
{t('tools.createCustomTool')}
+
{t('tools.createCustomTool')}
- diff --git a/web/app/components/tools/provider/tool-item.tsx b/web/app/components/tools/provider/tool-item.tsx index 161b62963b..d79d20cb9c 100644 --- a/web/app/components/tools/provider/tool-item.tsx +++ b/web/app/components/tools/provider/tool-item.tsx @@ -29,7 +29,7 @@ const ToolItem = ({ return ( <>
!disabled && setShowDetail(true)} >
{tool.label[language]}
diff --git a/web/app/components/tools/types.ts b/web/app/components/tools/types.ts index 32c468cde8..d444ee1f38 100644 --- a/web/app/components/tools/types.ts +++ b/web/app/components/tools/types.ts @@ -29,6 +29,7 @@ export enum CollectionType { custom = 'api', model = 'model', workflow = 'workflow', + mcp = 'mcp', } export type Emoji = { @@ -50,6 +51,10 @@ export type Collection = { labels: string[] plugin_id?: string letter?: string + // MCP Server + server_url?: string + updated_at?: number + server_identifier?: string } export type ToolParameter = { @@ -168,3 +173,11 @@ export type WorkflowToolProviderResponse = { } privacy_policy: string } + +export type MCPServerDetail = { + id: string + server_code: string + description: string + status: string + parameters?: Record +} diff --git a/web/app/components/tools/utils/to-form-schema.ts b/web/app/components/tools/utils/to-form-schema.ts index 179f59021e..ee7f3379ad 100644 --- a/web/app/components/tools/utils/to-form-schema.ts +++ b/web/app/components/tools/utils/to-form-schema.ts @@ -1,4 +1,7 @@ import type { ToolCredential, ToolParameter } from '../types' +import { FormTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations' +import { VarType as VarKindType } from '@/app/components/workflow/nodes/tool/types' + export const toType = (type: string) => { switch (type) { case 'string': @@ -54,7 +57,7 @@ export const toolCredentialToFormSchemas = (parameters: ToolCredential[]) => { return formSchemas } -export const addDefaultValue = (value: Record, formSchemas: { variable: string; default?: any }[]) => { +export const addDefaultValue = (value: Record, formSchemas: { variable: string; type: string; default?: any }[]) => { const newValues = { ...value } formSchemas.forEach((formSchema) => { const itemValue = value[formSchema.variable] @@ -64,14 +67,47 @@ export const addDefaultValue = (value: Record, formSchemas: { varia return newValues } -export const generateFormValue = (value: Record, formSchemas: { variable: string; default?: any }[], isReasoning = false) => { +const correctInitialData = (type: string, target: any, defaultValue: any) => { + if (type === 'text-input' || type === 'secret-input') + target.type = 'mixed' + + if (type === 'boolean') { + if (typeof defaultValue === 'string') + target.value = defaultValue === 'true' || defaultValue === '1' + + if (typeof defaultValue === 'boolean') + target.value = defaultValue + + if (typeof defaultValue === 'number') + target.value = defaultValue === 1 + } + + if (type === 'number-input') { + if (typeof defaultValue === 'string' && defaultValue !== '') + target.value = Number.parseFloat(defaultValue) + } + + if (type === 'app-selector' || type === 'model-selector') + target.value = defaultValue + + return target +} + +export const generateFormValue = (value: Record, formSchemas: { variable: string; default?: any; type: string }[], isReasoning = false) => { const newValues = {} as any formSchemas.forEach((formSchema) => { const itemValue = value[formSchema.variable] if ((formSchema.default !== undefined) && (value === undefined || itemValue === null || itemValue === '' || itemValue === undefined)) { + const value = formSchema.default newValues[formSchema.variable] = { - ...(isReasoning ? { value: null, auto: 1 } : { value: formSchema.default }), + value: { + type: 'constant', + value: formSchema.default, + }, + ...(isReasoning ? { auto: 1, value: null } : {}), } + if (!isReasoning) + newValues[formSchema.variable].value = correctInitialData(formSchema.type, newValues[formSchema.variable].value, value) } }) return newValues @@ -80,7 +116,9 @@ export const generateFormValue = (value: Record, formSchemas: { var export const getPlainValue = (value: Record) => { const plainValue = { ...value } Object.keys(plainValue).forEach((key) => { - plainValue[key] = value[key].value + plainValue[key] = { + ...value[key].value, + } }) return plainValue } @@ -94,3 +132,65 @@ export const getStructureValue = (value: Record) => { }) return newValue } + +export const getConfiguredValue = (value: Record, formSchemas: { variable: string; type: string; default?: any }[]) => { + const newValues = { ...value } + formSchemas.forEach((formSchema) => { + const itemValue = value[formSchema.variable] + if ((formSchema.default !== undefined) && (value === undefined || itemValue === null || itemValue === '' || itemValue === undefined)) { + const value = formSchema.default + newValues[formSchema.variable] = { + type: 'constant', + value: formSchema.default, + } + newValues[formSchema.variable] = correctInitialData(formSchema.type, newValues[formSchema.variable], value) + } + }) + return newValues +} + +const getVarKindType = (type: FormTypeEnum) => { + if (type === FormTypeEnum.file || type === FormTypeEnum.files) + return VarKindType.variable + if (type === FormTypeEnum.select || type === FormTypeEnum.boolean || type === FormTypeEnum.textNumber) + return VarKindType.constant + if (type === FormTypeEnum.textInput || type === FormTypeEnum.secretInput) + return VarKindType.mixed + } + +export const generateAgentToolValue = (value: Record, formSchemas: { variable: string; default?: any; type: string }[], isReasoning = false) => { + const newValues = {} as any + if (!isReasoning) { + formSchemas.forEach((formSchema) => { + const itemValue = value[formSchema.variable] + newValues[formSchema.variable] = { + value: { + type: 'constant', + value: itemValue.value, + }, + } + newValues[formSchema.variable].value = correctInitialData(formSchema.type, newValues[formSchema.variable].value, itemValue.value) + }) + } + else { + formSchemas.forEach((formSchema) => { + const itemValue = value[formSchema.variable] + if (itemValue.auto === 1) { + newValues[formSchema.variable] = { + auto: 1, + value: null, + } + } + else { + newValues[formSchema.variable] = { + auto: 0, + value: itemValue.value || { + type: getVarKindType(formSchema.type as FormTypeEnum), + value: null, + }, + } + } + }) + } + return newValues +} diff --git a/web/app/components/workflow-app/components/workflow-header/features-trigger.tsx b/web/app/components/workflow-app/components/workflow-header/features-trigger.tsx index 83f354d7d8..fd48935147 100644 --- a/web/app/components/workflow-app/components/workflow-header/features-trigger.tsx +++ b/web/app/components/workflow-app/components/workflow-header/features-trigger.tsx @@ -23,7 +23,7 @@ import { InputVarType, } from '@/app/components/workflow/types' import { useToastContext } from '@/app/components/base/toast' -import { usePublishWorkflow, useResetWorkflowVersionHistory } from '@/service/use-workflow' +import { useInvalidateAppWorkflow, usePublishWorkflow, useResetWorkflowVersionHistory } from '@/service/use-workflow' import type { PublishWorkflowParams } from '@/types/workflow' import { fetchAppDetail } from '@/service/apps' import { useStore as useAppStore } from '@/app/components/app/store' @@ -89,6 +89,7 @@ const FeaturesTrigger = () => { } }, [appID, setAppDetail]) const { mutateAsync: publishWorkflow } = usePublishWorkflow(appID!) + const updatePublishedWorkflow = useInvalidateAppWorkflow() const onPublish = useCallback(async (params?: PublishWorkflowParams) => { if (await handleCheckBeforePublish()) { const res = await publishWorkflow({ @@ -98,6 +99,7 @@ const FeaturesTrigger = () => { if (res) { notify({ type: 'success', message: t('common.api.actionSuccess') }) + updatePublishedWorkflow(appID!) updateAppDetail() workflowStore.getState().setPublishedAt(res.created_at) resetWorkflowVersionHistory() @@ -106,7 +108,7 @@ const FeaturesTrigger = () => { else { throw new Error('Checklist failed') } - }, [handleCheckBeforePublish, notify, t, workflowStore, publishWorkflow, resetWorkflowVersionHistory, updateAppDetail]) + }, [handleCheckBeforePublish, publishWorkflow, notify, t, updatePublishedWorkflow, appID, updateAppDetail, workflowStore, resetWorkflowVersionHistory]) const onPublisherToggle = useCallback((state: boolean) => { if (state) diff --git a/web/app/components/workflow/block-selector/all-tools.tsx b/web/app/components/workflow/block-selector/all-tools.tsx index e57a6bd3f7..870d791d4f 100644 --- a/web/app/components/workflow/block-selector/all-tools.tsx +++ b/web/app/components/workflow/block-selector/all-tools.tsx @@ -5,10 +5,11 @@ import { useState, } from 'react' import type { + BlockEnum, OnSelectBlock, ToolWithProvider, } from '../types' -import type { ToolValue } from './types' +import type { ToolDefaultValue, ToolValue } from './types' import { ToolTypeEnum } from './types' import Tools from './tools' import { useToolTabs } from './hooks' @@ -17,8 +18,6 @@ import cn from '@/utils/classnames' import { useGetLanguage } from '@/context/i18n' import type { ListRef } from '@/app/components/workflow/block-selector/market-place-plugin/list' import PluginList, { type ListProps } from '@/app/components/workflow/block-selector/market-place-plugin/list' -import ActionButton from '../../base/action-button' -import { RiAddLine } from '@remixicon/react' import { PluginType } from '../../plugins/types' import { useMarketplacePlugins } from '../../plugins/marketplace/hooks' import { useGlobalPublicStore } from '@/context/global-public-context' @@ -31,11 +30,12 @@ type AllToolsProps = { buildInTools: ToolWithProvider[] customTools: ToolWithProvider[] workflowTools: ToolWithProvider[] + mcpTools: ToolWithProvider[] onSelect: OnSelectBlock - supportAddCustomTool?: boolean - onAddedCustomTool?: () => void - onShowAddCustomCollectionModal?: () => void + canNotSelectMultiple?: boolean + onSelectMultiple?: (type: BlockEnum, tools: ToolDefaultValue[]) => void selectedTools?: ToolValue[] + canChooseMCPTool?: boolean } const DEFAULT_TAGS: AllToolsProps['tags'] = [] @@ -46,12 +46,14 @@ const AllTools = ({ searchText, tags = DEFAULT_TAGS, onSelect, + canNotSelectMultiple, + onSelectMultiple, buildInTools, workflowTools, customTools, - supportAddCustomTool, - onShowAddCustomCollectionModal, + mcpTools = [], selectedTools, + canChooseMCPTool, }: AllToolsProps) => { const language = useGetLanguage() const tabs = useToolTabs() @@ -64,13 +66,15 @@ const AllTools = ({ const tools = useMemo(() => { let mergedTools: ToolWithProvider[] = [] if (activeTab === ToolTypeEnum.All) - mergedTools = [...buildInTools, ...customTools, ...workflowTools] + mergedTools = [...buildInTools, ...customTools, ...workflowTools, ...mcpTools] if (activeTab === ToolTypeEnum.BuiltIn) mergedTools = buildInTools if (activeTab === ToolTypeEnum.Custom) mergedTools = customTools if (activeTab === ToolTypeEnum.Workflow) mergedTools = workflowTools + if (activeTab === ToolTypeEnum.MCP) + mergedTools = mcpTools if (!hasFilter) return mergedTools.filter(toolWithProvider => toolWithProvider.tools.length > 0) @@ -80,7 +84,7 @@ const AllTools = ({ return tool.label[language].toLowerCase().includes(searchText.toLowerCase()) || tool.name.toLowerCase().includes(searchText.toLowerCase()) }) }) - }, [activeTab, buildInTools, customTools, workflowTools, searchText, language, hasFilter]) + }, [activeTab, buildInTools, customTools, workflowTools, mcpTools, searchText, language, hasFilter]) const { queryPluginsWithDebounced: fetchPlugins, @@ -88,7 +92,6 @@ const AllTools = ({ } = useMarketplacePlugins() const { enable_marketplace } = useGlobalPublicStore(s => s.systemFeatures) - useEffect(() => { if (!enable_marketplace) return if (searchText || tags.length > 0) { @@ -103,10 +106,11 @@ const AllTools = ({ const pluginRef = useRef(null) const wrapElemRef = useRef(null) + const isSupportGroupView = [ToolTypeEnum.All, ToolTypeEnum.BuiltIn].includes(activeTab) return ( -
-
+
+
{ tabs.map(tab => ( @@ -124,17 +128,8 @@ const AllTools = ({ )) }
- - {supportAddCustomTool && ( -
-
- - - -
+ {isSupportGroupView && ( + )}
{/* Plugins from marketplace */} {enable_marketplace && { ] } -export const useToolTabs = () => { +export const useToolTabs = (isHideMCPTools?: boolean) => { const { t } = useTranslation() - - return [ + const tabs = [ { key: ToolTypeEnum.All, name: t('workflow.tabs.allTool'), @@ -52,4 +51,12 @@ export const useToolTabs = () => { name: t('workflow.tabs.workflowTool'), }, ] + if(!isHideMCPTools) { + tabs.push({ + key: ToolTypeEnum.MCP, + name: 'MCP', + }) + } + + return tabs } diff --git a/web/app/components/workflow/block-selector/index-bar.tsx b/web/app/components/workflow/block-selector/index-bar.tsx index 4d8bedffbe..097a16eb94 100644 --- a/web/app/components/workflow/block-selector/index-bar.tsx +++ b/web/app/components/workflow/block-selector/index-bar.tsx @@ -83,8 +83,8 @@ const IndexBar: FC = ({ letters, itemRefs, className }) => { element.scrollIntoView({ behavior: 'smooth' }) } return ( -
-
+
+
{letters.map(letter => (
handleIndexClick(letter)}> {letter} diff --git a/web/app/components/workflow/block-selector/index.tsx b/web/app/components/workflow/block-selector/index.tsx index 9e55a24d9e..0673ca0c0d 100644 --- a/web/app/components/workflow/block-selector/index.tsx +++ b/web/app/components/workflow/block-selector/index.tsx @@ -129,33 +129,35 @@ const NodeSelector: FC = ({
-
e.stopPropagation()}> - {activeTab === TabsEnum.Blocks && ( - setSearchText(e.target.value)} - onClear={() => setSearchText('')} - /> - )} - {activeTab === TabsEnum.Tools && ( - - )} - -
e.stopPropagation()}> + {activeTab === TabsEnum.Blocks && ( + setSearchText(e.target.value)} + onClear={() => setSearchText('')} + /> + )} + {activeTab === TabsEnum.Tools && ( + + )} +
+ } onSelect={handleSelect} searchText={searchText} tags={tags} diff --git a/web/app/components/workflow/block-selector/market-place-plugin/list.tsx b/web/app/components/workflow/block-selector/market-place-plugin/list.tsx index e2b4a7acc6..dce877ab91 100644 --- a/web/app/components/workflow/block-selector/market-place-plugin/list.tsx +++ b/web/app/components/workflow/block-selector/market-place-plugin/list.tsx @@ -80,7 +80,7 @@ const List = forwardRef(({ ) } - const maxWidthClassName = toolContentClassName || 'max-w-[300px]' + const maxWidthClassName = toolContentClassName || 'max-w-[100%]' return ( <> @@ -109,18 +109,20 @@ const List = forwardRef(({ onAction={noop} /> ))} -
-
- - - {t('plugin.searchInMarketplace')} - -
-
+ {list.length > 0 && ( +
+
+ + + {t('plugin.searchInMarketplace')} + +
+
+ )}
) diff --git a/web/app/components/workflow/block-selector/tabs.tsx b/web/app/components/workflow/block-selector/tabs.tsx index 67aaaba1a5..3f3fed2ca9 100644 --- a/web/app/components/workflow/block-selector/tabs.tsx +++ b/web/app/components/workflow/block-selector/tabs.tsx @@ -1,6 +1,6 @@ import type { FC } from 'react' import { memo } from 'react' -import { useAllBuiltInTools, useAllCustomTools, useAllWorkflowTools } from '@/service/use-tools' +import { useAllBuiltInTools, useAllCustomTools, useAllMCPTools, useAllWorkflowTools } from '@/service/use-tools' import type { BlockEnum } from '../types' import { useTabs } from './hooks' import type { ToolDefaultValue } from './types' @@ -16,6 +16,7 @@ export type TabsProps = { tags: string[] onSelect: (type: BlockEnum, tool?: ToolDefaultValue) => void availableBlocksTypes?: BlockEnum[] + filterElem: React.ReactNode noBlocks?: boolean } const Tabs: FC = ({ @@ -25,26 +26,28 @@ const Tabs: FC = ({ searchText, onSelect, availableBlocksTypes, + filterElem, noBlocks, }) => { const tabs = useTabs() const { data: buildInTools } = useAllBuiltInTools() const { data: customTools } = useAllCustomTools() const { data: workflowTools } = useAllWorkflowTools() + const { data: mcpTools } = useAllMCPTools() return (
e.stopPropagation()}> { !noBlocks && ( -
+
{ tabs.map(tab => (
onActiveTabChange(tab.key)} @@ -56,25 +59,30 @@ const Tabs: FC = ({
) } + {filterElem} { activeTab === TabsEnum.Blocks && !noBlocks && ( - +
+ +
) } { activeTab === TabsEnum.Tools && ( ) } diff --git a/web/app/components/workflow/block-selector/tool-picker.tsx b/web/app/components/workflow/block-selector/tool-picker.tsx index dbb49fde75..d97a4f3a1b 100644 --- a/web/app/components/workflow/block-selector/tool-picker.tsx +++ b/web/app/components/workflow/block-selector/tool-picker.tsx @@ -23,7 +23,7 @@ import { } from '@/service/tools' import type { CustomCollectionBackend } from '@/app/components/tools/types' import Toast from '@/app/components/base/toast' -import { useAllBuiltInTools, useAllCustomTools, useAllWorkflowTools, useInvalidateAllCustomTools } from '@/service/use-tools' +import { useAllBuiltInTools, useAllCustomTools, useAllMCPTools, useAllWorkflowTools, useInvalidateAllCustomTools } from '@/service/use-tools' import cn from '@/utils/classnames' type Props = { @@ -35,9 +35,11 @@ type Props = { isShow: boolean onShowChange: (isShow: boolean) => void onSelect: (tool: ToolDefaultValue) => void + onSelectMultiple: (tools: ToolDefaultValue[]) => void supportAddCustomTool?: boolean scope?: string selectedTools?: ToolValue[] + canChooseMCPTool?: boolean } const ToolPicker: FC = ({ @@ -48,10 +50,12 @@ const ToolPicker: FC = ({ isShow, onShowChange, onSelect, + onSelectMultiple, supportAddCustomTool, scope = 'all', selectedTools, panelClassName, + canChooseMCPTool, }) => { const { t } = useTranslation() const [searchText, setSearchText] = useState('') @@ -61,6 +65,7 @@ const ToolPicker: FC = ({ const { data: customTools } = useAllCustomTools() const invalidateCustomTools = useInvalidateAllCustomTools() const { data: workflowTools } = useAllWorkflowTools() + const { data: mcpTools } = useAllMCPTools() const { builtinToolList, customToolList, workflowToolList } = useMemo(() => { if (scope === 'plugins') { @@ -102,6 +107,10 @@ const ToolPicker: FC = ({ onSelect(tool!) } + const handleSelectMultiple = (_type: BlockEnum, tools: ToolDefaultValue[]) => { + onSelectMultiple(tools) + } + const [isShowEditCollectionToolModal, { setFalse: hideEditCustomCollectionModal, setTrue: showEditCustomCollectionModal, @@ -142,7 +151,7 @@ const ToolPicker: FC = ({ -
+
= ({ onTagsChange={setTags} size='small' placeholder={t('plugin.searchTools')!} + supportAddCustomTool={supportAddCustomTool} + onAddedCustomTool={handleAddedCustomTool} + onShowAddCustomCollectionModal={showEditCustomCollectionModal} + inputClassName='grow' + />
diff --git a/web/app/components/workflow/block-selector/tool/action-item.tsx b/web/app/components/workflow/block-selector/tool/action-item.tsx index dc9b9b9114..e5e33614b0 100644 --- a/web/app/components/workflow/block-selector/tool/action-item.tsx +++ b/web/app/components/workflow/block-selector/tool/action-item.tsx @@ -10,13 +10,12 @@ import { useGetLanguage } from '@/context/i18n' import BlockIcon from '../../block-icon' import cn from '@/utils/classnames' import { useTranslation } from 'react-i18next' -import { RiCheckLine } from '@remixicon/react' -import Badge from '@/app/components/base/badge' type Props = { provider: ToolWithProvider payload: Tool disabled?: boolean + isAdded?: boolean onSelect: (type: BlockEnum, tool?: ToolDefaultValue) => void } @@ -25,6 +24,7 @@ const ToolItem: FC = ({ payload, onSelect, disabled, + isAdded, }) => { const { t } = useTranslation() @@ -71,18 +71,16 @@ const ToolItem: FC = ({ output_schema: payload.output_schema, paramSchemas: payload.parameters, params, + meta: provider.meta, }) }} > -
{payload.label[language]}
- {disabled && - -
{t('tools.addToolModal.added')}
-
- } +
+ {payload.label[language]} +
+ {isAdded && ( +
{t('tools.addToolModal.added')}
+ )}
) diff --git a/web/app/components/workflow/block-selector/tool/tool-list-flat-view/list.tsx b/web/app/components/workflow/block-selector/tool/tool-list-flat-view/list.tsx index ef671ca1f8..ca462c082e 100644 --- a/web/app/components/workflow/block-selector/tool/tool-list-flat-view/list.tsx +++ b/web/app/components/workflow/block-selector/tool/tool-list-flat-view/list.tsx @@ -11,21 +11,29 @@ import { useMemo } from 'react' type Props = { payload: ToolWithProvider[] isShowLetterIndex: boolean + indexBar: React.ReactNode hasSearchText: boolean onSelect: (type: BlockEnum, tool?: ToolDefaultValue) => void + canNotSelectMultiple?: boolean + onSelectMultiple?: (type: BlockEnum, tools: ToolDefaultValue[]) => void letters: string[] toolRefs: any selectedTools?: ToolValue[] + canChooseMCPTool?: boolean } const ToolViewFlatView: FC = ({ letters, payload, isShowLetterIndex, + indexBar, hasSearchText, onSelect, + canNotSelectMultiple, + onSelectMultiple, toolRefs, selectedTools, + canChooseMCPTool, }) => { const firstLetterToolIds = useMemo(() => { const res: Record = {} @@ -37,26 +45,31 @@ const ToolViewFlatView: FC = ({ return res }, [payload, letters]) return ( -
- {payload.map(tool => ( -
{ - const letter = firstLetterToolIds[tool.id] - if (letter) - toolRefs.current[letter] = el - }} - > - -
- ))} +
+
+ {payload.map(tool => ( +
{ + const letter = firstLetterToolIds[tool.id] + if (letter) + toolRefs.current[letter] = el + }} + > + +
+ ))} +
+ {isShowLetterIndex && indexBar}
) } diff --git a/web/app/components/workflow/block-selector/tool/tool-list-tree-view/item.tsx b/web/app/components/workflow/block-selector/tool/tool-list-tree-view/item.tsx index d6c567f8e2..b3f7aab4df 100644 --- a/web/app/components/workflow/block-selector/tool/tool-list-tree-view/item.tsx +++ b/web/app/components/workflow/block-selector/tool/tool-list-tree-view/item.tsx @@ -12,7 +12,10 @@ type Props = { toolList: ToolWithProvider[] hasSearchText: boolean onSelect: (type: BlockEnum, tool?: ToolDefaultValue) => void + canNotSelectMultiple?: boolean + onSelectMultiple?: (type: BlockEnum, tools: ToolDefaultValue[]) => void selectedTools?: ToolValue[] + canChooseMCPTool?: boolean } const Item: FC = ({ @@ -20,7 +23,10 @@ const Item: FC = ({ toolList, hasSearchText, onSelect, + canNotSelectMultiple, + onSelectMultiple, selectedTools, + canChooseMCPTool, }) => { return (
@@ -36,7 +42,10 @@ const Item: FC = ({ isShowLetterIndex={false} hasSearchText={hasSearchText} onSelect={onSelect} + canNotSelectMultiple={canNotSelectMultiple} + onSelectMultiple={onSelectMultiple} selectedTools={selectedTools} + canChooseMCPTool={canChooseMCPTool} /> ))}
diff --git a/web/app/components/workflow/block-selector/tool/tool-list-tree-view/list.tsx b/web/app/components/workflow/block-selector/tool/tool-list-tree-view/list.tsx index f3f98279c8..d85d1ea682 100644 --- a/web/app/components/workflow/block-selector/tool/tool-list-tree-view/list.tsx +++ b/web/app/components/workflow/block-selector/tool/tool-list-tree-view/list.tsx @@ -12,14 +12,20 @@ type Props = { payload: Record hasSearchText: boolean onSelect: (type: BlockEnum, tool?: ToolDefaultValue) => void + canNotSelectMultiple?: boolean + onSelectMultiple?: (type: BlockEnum, tools: ToolDefaultValue[]) => void selectedTools?: ToolValue[] + canChooseMCPTool?: boolean } const ToolListTreeView: FC = ({ payload, hasSearchText, onSelect, + canNotSelectMultiple, + onSelectMultiple, selectedTools, + canChooseMCPTool, }) => { const { t } = useTranslation() const getI18nGroupName = useCallback((name: string) => { @@ -46,7 +52,10 @@ const ToolListTreeView: FC = ({ toolList={payload[groupName]} hasSearchText={hasSearchText} onSelect={onSelect} + canNotSelectMultiple={canNotSelectMultiple} + onSelectMultiple={onSelectMultiple} selectedTools={selectedTools} + canChooseMCPTool={canChooseMCPTool} /> ))}
diff --git a/web/app/components/workflow/block-selector/tool/tool.tsx b/web/app/components/workflow/block-selector/tool/tool.tsx index d48d0bfc90..83ae062737 100644 --- a/web/app/components/workflow/block-selector/tool/tool.tsx +++ b/web/app/components/workflow/block-selector/tool/tool.tsx @@ -1,6 +1,6 @@ 'use client' import type { FC } from 'react' -import React, { useEffect, useMemo } from 'react' +import React, { useCallback, useEffect, useMemo, useRef } from 'react' import cn from '@/utils/classnames' import { RiArrowDownSLine, RiArrowRightSLine } from '@remixicon/react' import { useGetLanguage } from '@/context/i18n' @@ -13,36 +13,108 @@ import { ViewType } from '../view-type-select' import ActionItem from './action-item' import BlockIcon from '../../block-icon' import { useTranslation } from 'react-i18next' +import { useHover } from 'ahooks' +import McpToolNotSupportTooltip from '../../nodes/_base/components/mcp-tool-not-support-tooltip' +import { Mcp } from '@/app/components/base/icons/src/vender/other' type Props = { className?: string payload: ToolWithProvider viewType: ViewType - isShowLetterIndex: boolean hasSearchText: boolean onSelect: (type: BlockEnum, tool?: ToolDefaultValue) => void + canNotSelectMultiple?: boolean + onSelectMultiple?: (type: BlockEnum, tools: ToolDefaultValue[]) => void selectedTools?: ToolValue[] + canChooseMCPTool?: boolean } const Tool: FC = ({ className, payload, viewType, - isShowLetterIndex, hasSearchText, onSelect, + canNotSelectMultiple, + onSelectMultiple, selectedTools, + canChooseMCPTool, }) => { const { t } = useTranslation() const language = useGetLanguage() const isFlatView = viewType === ViewType.flat + const notShowProvider = payload.type === CollectionType.workflow const actions = payload.tools - const hasAction = true // Now always support actions + const hasAction = !notShowProvider const [isFold, setFold] = React.useState(true) - const getIsDisabled = (tool: ToolType) => { + const ref = useRef(null) + const isHovering = useHover(ref) + const isMCPTool = payload.type === CollectionType.mcp + const isShowCanNotChooseMCPTip = !canChooseMCPTool && isMCPTool + const getIsDisabled = useCallback((tool: ToolType) => { if (!selectedTools || !selectedTools.length) return false - return selectedTools.some(selectedTool => selectedTool.provider_name === payload.name && selectedTool.tool_name === tool.name) - } + return selectedTools.some(selectedTool => (selectedTool.provider_name === payload.name || selectedTool.provider_name === payload.id) && selectedTool.tool_name === tool.name) + }, [payload.id, payload.name, selectedTools]) + + const totalToolsNum = actions.length + const selectedToolsNum = actions.filter(action => getIsDisabled(action)).length + const isAllSelected = selectedToolsNum === totalToolsNum + + const notShowProviderSelectInfo = useMemo(() => { + if (isAllSelected) { + return ( + + {t('tools.addToolModal.added')} + + ) + } + }, [isAllSelected, t]) + const selectedInfo = useMemo(() => { + if (isHovering && !isAllSelected) { + return ( + { + onSelectMultiple?.(BlockEnum.Tool, actions.filter(action => !getIsDisabled(action)).map((tool) => { + const params: Record = {} + if (tool.parameters) { + tool.parameters.forEach((item) => { + params[item.name] = '' + }) + } + return { + provider_id: payload.id, + provider_type: payload.type, + provider_name: payload.name, + tool_name: tool.name, + tool_label: tool.label[language], + tool_description: tool.description[language], + title: tool.label[language], + is_team_authorization: payload.is_team_authorization, + output_schema: tool.output_schema, + paramSchemas: tool.parameters, + params, + } + })) + }} + > + {t('workflow.tabs.addAll')} + + ) + } + + if (selectedToolsNum === 0) + return <> + + return ( + + {isAllSelected + ? t('workflow.tabs.allAdded') + : `${selectedToolsNum} / ${totalToolsNum}` + } + + ) + }, [actions, getIsDisabled, isAllSelected, isHovering, language, onSelectMultiple, payload.id, payload.is_team_authorization, payload.name, payload.type, selectedToolsNum, t, totalToolsNum]) + useEffect(() => { if (hasSearchText && isFold) { setFold(false) @@ -71,59 +143,73 @@ const Tool: FC = ({ return (
{ - if (hasAction) + if (hasAction) { setFold(!isFold) + return + } - // Now always support actions - // if (payload.parameters) { - // payload.parameters.forEach((item) => { - // params[item.name] = '' - // }) - // } - // onSelect(BlockEnum.Tool, { - // provider_id: payload.id, - // provider_type: payload.type, - // provider_name: payload.name, - // tool_name: payload.name, - // tool_label: payload.label[language], - // title: payload.label[language], - // params: {}, - // }) + const tool = actions[0] + const params: Record = {} + if (tool.parameters) { + tool.parameters.forEach((item) => { + params[item.name] = '' + }) + } + onSelect(BlockEnum.Tool, { + provider_id: payload.id, + provider_type: payload.type, + provider_name: payload.name, + tool_name: tool.name, + tool_label: tool.label[language], + tool_description: tool.description[language], + title: tool.label[language], + is_team_authorization: payload.is_team_authorization, + output_schema: tool.output_schema, + paramSchemas: tool.parameters, + params, + }) }} > -
+
-
{payload.label[language]}
+
+ {notShowProvider ? actions[0]?.label[language] : payload.label[language]} + {isFlatView && groupName && ( + {groupName} + )} + {isMCPTool && } +
-
- {isFlatView && ( -
{groupName}
- )} +
+ {!isShowCanNotChooseMCPTip && !canNotSelectMultiple && (notShowProvider ? notShowProviderSelectInfo : selectedInfo)} + {isShowCanNotChooseMCPTip && } {hasAction && ( - + )}
- {hasAction && !isFold && ( + {!notShowProvider && hasAction && !isFold && ( actions.map(action => ( )) )} diff --git a/web/app/components/workflow/block-selector/tools.tsx b/web/app/components/workflow/block-selector/tools.tsx index 2562501524..da47432b04 100644 --- a/web/app/components/workflow/block-selector/tools.tsx +++ b/web/app/components/workflow/block-selector/tools.tsx @@ -7,6 +7,7 @@ import { useTranslation } from 'react-i18next' import type { BlockEnum, ToolWithProvider } from '../types' import IndexBar, { groupItems } from './index-bar' import type { ToolDefaultValue, ToolValue } from './types' +import type { ToolTypeEnum } from './types' import { ViewType } from './view-type-select' import Empty from '@/app/components/tools/add-tool-modal/empty' import { useGetLanguage } from '@/context/i18n' @@ -15,25 +16,34 @@ import ToolListFlatView from './tool/tool-list-flat-view/list' import classNames from '@/utils/classnames' type ToolsProps = { - showWorkflowEmpty: boolean onSelect: (type: BlockEnum, tool?: ToolDefaultValue) => void + canNotSelectMultiple?: boolean + onSelectMultiple?: (type: BlockEnum, tools: ToolDefaultValue[]) => void tools: ToolWithProvider[] viewType: ViewType hasSearchText: boolean + toolType?: ToolTypeEnum + isAgent?: boolean className?: string indexBarClassName?: string selectedTools?: ToolValue[] + canChooseMCPTool?: boolean } const Blocks = ({ - showWorkflowEmpty, onSelect, + canNotSelectMultiple, + onSelectMultiple, tools, viewType, hasSearchText, + toolType, + isAgent, className, indexBarClassName, selectedTools, + canChooseMCPTool, }: ToolsProps) => { + // const tools: any = [] const { t } = useTranslation() const language = useGetLanguage() const isFlatView = viewType === ViewType.flat @@ -87,15 +97,15 @@ const Blocks = ({ const toolRefs = useRef({}) return ( -
+
{ - !tools.length && !showWorkflowEmpty && ( -
{t('workflow.tabs.noResult')}
+ !tools.length && hasSearchText && ( +
{t('workflow.tabs.noResult')}
) } - {!tools.length && showWorkflowEmpty && ( + {!tools.length && !hasSearchText && (
- +
)} {!!tools.length && ( @@ -107,19 +117,24 @@ const Blocks = ({ isShowLetterIndex={isShowLetterIndex} hasSearchText={hasSearchText} onSelect={onSelect} + canNotSelectMultiple={canNotSelectMultiple} + onSelectMultiple={onSelectMultiple} selectedTools={selectedTools} + canChooseMCPTool={canChooseMCPTool} + indexBar={} /> ) : ( ) )} - - {isShowLetterIndex && }
) } diff --git a/web/app/components/workflow/block-selector/types.ts b/web/app/components/workflow/block-selector/types.ts index f1bdbbfbd9..c96a60f674 100644 --- a/web/app/components/workflow/block-selector/types.ts +++ b/web/app/components/workflow/block-selector/types.ts @@ -1,3 +1,5 @@ +import type { PluginMeta } from '../../plugins/types' + export enum TabsEnum { Blocks = 'blocks', Tools = 'tools', @@ -8,6 +10,7 @@ export enum ToolTypeEnum { BuiltIn = 'built-in', Custom = 'custom', Workflow = 'workflow', + MCP = 'mcp', } export enum BlockClassificationEnum { @@ -30,10 +33,12 @@ export type ToolDefaultValue = { params: Record paramSchemas: Record[] output_schema: Record + meta?: PluginMeta } export type ToolValue = { provider_name: string + provider_show_name?: string tool_name: string tool_label: string tool_description?: string diff --git a/web/app/components/workflow/block-selector/use-check-vertical-scrollbar.ts b/web/app/components/workflow/block-selector/use-check-vertical-scrollbar.ts new file mode 100644 index 0000000000..98986cf3b6 --- /dev/null +++ b/web/app/components/workflow/block-selector/use-check-vertical-scrollbar.ts @@ -0,0 +1,31 @@ +import { useEffect, useState } from 'react' + +const useCheckVerticalScrollbar = (ref: React.RefObject) => { + const [hasVerticalScrollbar, setHasVerticalScrollbar] = useState(false) + + useEffect(() => { + const elem = ref.current + if (!elem) return + + const checkScrollbar = () => { + setHasVerticalScrollbar(elem.scrollHeight > elem.clientHeight) + } + + checkScrollbar() + + const resizeObserver = new ResizeObserver(checkScrollbar) + resizeObserver.observe(elem) + + const mutationObserver = new MutationObserver(checkScrollbar) + mutationObserver.observe(elem, { childList: true, subtree: true, characterData: true }) + + return () => { + resizeObserver.disconnect() + mutationObserver.disconnect() + } + }, [ref]) + + return hasVerticalScrollbar +} + +export default useCheckVerticalScrollbar diff --git a/web/app/components/workflow/hooks/use-workflow.ts b/web/app/components/workflow/hooks/use-workflow.ts index 1b98178152..8bc9d3436f 100644 --- a/web/app/components/workflow/hooks/use-workflow.ts +++ b/web/app/components/workflow/hooks/use-workflow.ts @@ -40,6 +40,7 @@ import { useStore as useAppStore } from '@/app/components/app/store' import { fetchAllBuiltInTools, fetchAllCustomTools, + fetchAllMCPTools, fetchAllWorkflowTools, } from '@/service/tools' import { CollectionType } from '@/app/components/tools/types' @@ -445,6 +446,13 @@ export const useFetchToolsData = () => { workflowTools: workflowTools || [], }) } + if(type === 'mcp') { + const mcpTools = await fetchAllMCPTools() + + workflowStore.setState({ + mcpTools: mcpTools || [], + }) + } }, [workflowStore]) return { @@ -491,6 +499,8 @@ export const useToolIcon = (data: Node['data']) => { const buildInTools = useStore(s => s.buildInTools) const customTools = useStore(s => s.customTools) const workflowTools = useStore(s => s.workflowTools) + const mcpTools = useStore(s => s.mcpTools) + const toolIcon = useMemo(() => { if(!data) return '' @@ -500,11 +510,13 @@ export const useToolIcon = (data: Node['data']) => { targetTools = buildInTools else if (data.provider_type === CollectionType.custom) targetTools = customTools + else if (data.provider_type === CollectionType.mcp) + targetTools = mcpTools else targetTools = workflowTools return targetTools.find(toolWithProvider => canFindTool(toolWithProvider.id, data.provider_id))?.icon } - }, [data, buildInTools, customTools, workflowTools]) + }, [data, buildInTools, customTools, mcpTools, workflowTools]) return toolIcon } diff --git a/web/app/components/workflow/index.tsx b/web/app/components/workflow/index.tsx index 8631eb58e3..8ea861ebb4 100644 --- a/web/app/components/workflow/index.tsx +++ b/web/app/components/workflow/index.tsx @@ -234,6 +234,7 @@ export const Workflow: FC = memo(({ handleFetchAllTools('builtin') handleFetchAllTools('custom') handleFetchAllTools('workflow') + handleFetchAllTools('mcp') }, [handleFetchAllTools]) const { diff --git a/web/app/components/workflow/nodes/_base/components/agent-strategy-selector.tsx b/web/app/components/workflow/nodes/_base/components/agent-strategy-selector.tsx index f262ae7e34..ba5281870f 100644 --- a/web/app/components/workflow/nodes/_base/components/agent-strategy-selector.tsx +++ b/web/app/components/workflow/nodes/_base/components/agent-strategy-selector.tsx @@ -68,6 +68,7 @@ function formatStrategy(input: StrategyPluginDetail[], getIcon: (i: string) => s icon: getIcon(item.declaration.identity.icon), label: item.declaration.identity.label as any, type: CollectionType.all, + meta: item.meta, tools: item.declaration.strategies.map(strategy => ({ name: strategy.identity.name, author: strategy.identity.author, @@ -89,10 +90,13 @@ function formatStrategy(input: StrategyPluginDetail[], getIcon: (i: string) => s export type AgentStrategySelectorProps = { value?: Strategy, onChange: (value?: Strategy) => void, + canChooseMCPTool: boolean, } export const AgentStrategySelector = memo((props: AgentStrategySelectorProps) => { - const { value, onChange } = props + const { enable_marketplace } = useGlobalPublicStore(s => s.systemFeatures) + + const { value, onChange, canChooseMCPTool } = props const [open, setOpen] = useState(false) const [viewType, setViewType] = useState(ViewType.flat) const [query, setQuery] = useState('') @@ -132,8 +136,6 @@ export const AgentStrategySelector = memo((props: AgentStrategySelectorProps) => plugins: notInstalledPlugins = [], } = useMarketplacePlugins() - const { enable_marketplace } = useGlobalPublicStore(s => s.systemFeatures) - useEffect(() => { if (!enable_marketplace) return if (query) { @@ -214,21 +216,25 @@ export const AgentStrategySelector = memo((props: AgentStrategySelectorProps) => agent_strategy_label: tool!.tool_label, agent_output_schema: tool!.output_schema, plugin_unique_identifier: tool!.provider_id, + meta: tool!.meta, }) setOpen(false) }} className='h-full max-h-full max-w-none overflow-y-auto' - indexBarClassName='top-0 xl:top-36' showWorkflowEmpty={false} hasSearchText={false} /> - {enable_marketplace - && + {enable_marketplace && - } + />}
diff --git a/web/app/components/workflow/nodes/_base/components/agent-strategy.tsx b/web/app/components/workflow/nodes/_base/components/agent-strategy.tsx index 4ca8746137..31aa91cfdb 100644 --- a/web/app/components/workflow/nodes/_base/components/agent-strategy.tsx +++ b/web/app/components/workflow/nodes/_base/components/agent-strategy.tsx @@ -19,6 +19,8 @@ import { useWorkflowStore } from '../../../store' import { useRenderI18nObject } from '@/hooks/use-i18n' import type { NodeOutPutVar } from '../../../types' import type { Node } from 'reactflow' +import type { PluginMeta } from '@/app/components/plugins/types' +import { noop } from 'lodash' import { useDocLink } from '@/context/i18n' export type Strategy = { @@ -27,6 +29,7 @@ export type Strategy = { agent_strategy_label: string agent_output_schema: Record plugin_unique_identifier: string + meta?: PluginMeta } export type AgentStrategyProps = { @@ -38,6 +41,7 @@ export type AgentStrategyProps = { nodeOutputVars?: NodeOutPutVar[], availableNodes?: Node[], nodeId?: string + canChooseMCPTool: boolean } type CustomSchema = Omit & { type: Type } & Field @@ -48,7 +52,7 @@ type MultipleToolSelectorSchema = CustomSchema<'array[tools]'> type CustomField = ToolSelectorSchema | MultipleToolSelectorSchema export const AgentStrategy = memo((props: AgentStrategyProps) => { - const { strategy, onStrategyChange, formSchema, formValue, onFormValueChange, nodeOutputVars, availableNodes, nodeId } = props + const { strategy, onStrategyChange, formSchema, formValue, onFormValueChange, nodeOutputVars, availableNodes, nodeId, canChooseMCPTool } = props const { t } = useTranslation() const docLink = useDocLink() const defaultModel = useDefaultModel(ModelTypeEnum.textGeneration) @@ -57,6 +61,7 @@ export const AgentStrategy = memo((props: AgentStrategyProps) => { const { setControlPromptEditorRerenderKey, } = workflowStore.getState() + const override: ComponentProps>['override'] = [ [FormTypeEnum.textNumber, FormTypeEnum.textInput], (schema, props) => { @@ -168,6 +173,8 @@ export const AgentStrategy = memo((props: AgentStrategyProps) => { value={value} onSelect={item => onChange(item)} onDelete={() => onChange(null)} + canChooseMCPTool={canChooseMCPTool} + onSelectMultiple={noop} /> ) @@ -189,13 +196,14 @@ export const AgentStrategy = memo((props: AgentStrategyProps) => { onChange={onChange} supportCollapse required={schema.required} + canChooseMCPTool={canChooseMCPTool} /> ) } } } return
- + { strategy ?
@@ -215,6 +223,7 @@ export const AgentStrategy = memo((props: AgentStrategyProps) => { nodeId={nodeId} nodeOutputVars={nodeOutputVars || []} availableNodes={availableNodes || []} + canChooseMCPTool={canChooseMCPTool} />
: = ({ return ( -
+
{title}
{ diff --git a/web/app/components/workflow/nodes/_base/components/editor/code-editor/index.tsx b/web/app/components/workflow/nodes/_base/components/editor/code-editor/index.tsx index 3540c60a39..748698747c 100644 --- a/web/app/components/workflow/nodes/_base/components/editor/code-editor/index.tsx +++ b/web/app/components/workflow/nodes/_base/components/editor/code-editor/index.tsx @@ -23,7 +23,7 @@ export type Props = { value?: string | object placeholder?: React.JSX.Element | string onChange?: (value: string) => void - title?: React.JSX.Element + title?: string | React.JSX.Element language: CodeLanguage headerRight?: React.JSX.Element readOnly?: boolean diff --git a/web/app/components/workflow/nodes/_base/components/form-input-boolean.tsx b/web/app/components/workflow/nodes/_base/components/form-input-boolean.tsx new file mode 100644 index 0000000000..07c3a087b9 --- /dev/null +++ b/web/app/components/workflow/nodes/_base/components/form-input-boolean.tsx @@ -0,0 +1,35 @@ +'use client' +import type { FC } from 'react' +import cn from '@/utils/classnames' + +type Props = { + value: boolean + onChange: (value: boolean) => void +} + +const FormInputBoolean: FC = ({ + value, + onChange, +}) => { + return ( +
+
onChange(true)} + >True
+
onChange(false)} + >False
+
+ ) +} +export default FormInputBoolean diff --git a/web/app/components/workflow/nodes/_base/components/form-input-item.tsx b/web/app/components/workflow/nodes/_base/components/form-input-item.tsx new file mode 100644 index 0000000000..6f8bd17a96 --- /dev/null +++ b/web/app/components/workflow/nodes/_base/components/form-input-item.tsx @@ -0,0 +1,279 @@ +'use client' +import type { FC } from 'react' +import type { ToolVarInputs } from '@/app/components/workflow/nodes/tool/types' +import type { CredentialFormSchema } from '@/app/components/header/account-setting/model-provider-page/declarations' +import { useLanguage } from '@/app/components/header/account-setting/model-provider-page/hooks' +import { FormTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations' +import { VarType as VarKindType } from '@/app/components/workflow/nodes/tool/types' +import { VarType } from '@/app/components/workflow/types' + +import type { ToolWithProvider, ValueSelector, Var } from '@/app/components/workflow/types' +import FormInputTypeSwitch from './form-input-type-switch' +import useAvailableVarList from '@/app/components/workflow/nodes/_base/hooks/use-available-var-list' +import Input from '@/app/components/base/input' +import { SimpleSelect } from '@/app/components/base/select' +import MixedVariableTextInput from '@/app/components/workflow/nodes/tool/components/mixed-variable-text-input' +import FormInputBoolean from './form-input-boolean' +import AppSelector from '@/app/components/plugins/plugin-detail-panel/app-selector' +import ModelParameterModal from '@/app/components/plugins/plugin-detail-panel/model-selector' +import VarReferencePicker from '@/app/components/workflow/nodes/_base/components/variable/var-reference-picker' +import CodeEditor from '@/app/components/workflow/nodes/_base/components/editor/code-editor' +import { CodeLanguage } from '@/app/components/workflow/nodes/code/types' +import cn from '@/utils/classnames' +import type { Tool } from '@/app/components/tools/types' + +type Props = { + readOnly: boolean + nodeId: string + schema: CredentialFormSchema + value: ToolVarInputs + onChange: (value: any) => void + inPanel?: boolean + currentTool?: Tool + currentProvider?: ToolWithProvider +} + +const FormInputItem: FC = ({ + readOnly, + nodeId, + schema, + value, + onChange, + inPanel, + currentTool, + currentProvider, +}) => { + const language = useLanguage() + + const { + placeholder, + variable, + type, + default: defaultValue, + options, + scope, + } = schema as any + const varInput = value[variable] + const isString = type === FormTypeEnum.textInput || type === FormTypeEnum.secretInput + const isNumber = type === FormTypeEnum.textNumber + const isObject = type === FormTypeEnum.object + const isArray = type === FormTypeEnum.array + const isShowJSONEditor = isObject || isArray + const isFile = type === FormTypeEnum.file || type === FormTypeEnum.files + const isBoolean = type === FormTypeEnum.boolean + const isSelect = type === FormTypeEnum.select || type === FormTypeEnum.dynamicSelect + const isAppSelector = type === FormTypeEnum.appSelector + const isModelSelector = type === FormTypeEnum.modelSelector + const showTypeSwitch = isNumber || isObject || isArray + const isConstant = varInput?.type === VarKindType.constant || !varInput?.type + const showVariableSelector = isFile || varInput?.type === VarKindType.variable + + const { availableVars, availableNodesWithParent } = useAvailableVarList(nodeId, { + onlyLeafNodeVar: false, + filterVar: (varPayload: Var) => { + return [VarType.string, VarType.number, VarType.secret].includes(varPayload.type) + }, + }) + + const targetVarType = () => { + if (isString) + return VarType.string + else if (isNumber) + return VarType.number + else if (type === FormTypeEnum.files) + return VarType.arrayFile + else if (type === FormTypeEnum.file) + return VarType.file + // else if (isSelect) + // return VarType.select + // else if (isAppSelector) + // return VarType.appSelector + // else if (isModelSelector) + // return VarType.modelSelector + // else if (isBoolean) + // return VarType.boolean + else if (isObject) + return VarType.object + else if (isArray) + return VarType.arrayObject + else + return VarType.string + } + + const getFilterVar = () => { + if (isNumber) + return (varPayload: any) => varPayload.type === VarType.number + else if (isString) + return (varPayload: any) => [VarType.string, VarType.number, VarType.secret].includes(varPayload.type) + else if (isFile) + return (varPayload: any) => [VarType.file, VarType.arrayFile].includes(varPayload.type) + else if (isBoolean) + return (varPayload: any) => varPayload.type === VarType.boolean + else if (isObject) + return (varPayload: any) => varPayload.type === VarType.object + else if (isArray) + return (varPayload: any) => [VarType.array, VarType.arrayString, VarType.arrayNumber, VarType.arrayObject].includes(varPayload.type) + return undefined + } + + const getVarKindType = () => { + if (isFile) + return VarKindType.variable + if (isSelect || isBoolean || isNumber || isArray || isObject) + return VarKindType.constant + if (isString) + return VarKindType.mixed + } + + const handleTypeChange = (newType: string) => { + if (newType === VarKindType.variable) { + onChange({ + ...value, + [variable]: { + ...varInput, + type: VarKindType.variable, + value: '', + }, + }) + } + else { + onChange({ + ...value, + [variable]: { + ...varInput, + type: VarKindType.constant, + value: defaultValue, + }, + }) + } + } + + const handleValueChange = (newValue: any) => { + onChange({ + ...value, + [variable]: { + ...varInput, + type: getVarKindType(), + value: isNumber ? Number.parseFloat(newValue) : newValue, + }, + }) + } + + const handleAppOrModelSelect = (newValue: any) => { + onChange({ + ...value, + [variable]: { + ...varInput, + ...newValue, + }, + }) + } + + const handleVariableSelectorChange = (newValue: ValueSelector | string, variable: string) => { + onChange({ + ...value, + [variable]: { + ...varInput, + type: VarKindType.variable, + value: newValue || '', + }, + }) + } + + return ( +
+ {showTypeSwitch && ( + + )} + {isString && ( + + )} + {isNumber && isConstant && ( + handleValueChange(e.target.value)} + placeholder={placeholder?.[language] || placeholder?.en_US} + /> + )} + {isBoolean && ( + + )} + {isSelect && ( + { + if (option.show_on.length) + return option.show_on.every(showOnItem => value[showOnItem.variable] === showOnItem.value) + + return true + }).map((option: { value: any; label: { [x: string]: any; en_US: any } }) => ({ value: option.value, name: option.label[language] || option.label.en_US }))} + onSelect={item => handleValueChange(item.value as string)} + placeholder={placeholder?.[language] || placeholder?.en_US} + /> + )} + {isShowJSONEditor && isConstant && ( +
+ {placeholder?.[language] || placeholder?.en_US}
} + /> +
+ )} + {isAppSelector && ( + + )} + {isModelSelector && isConstant && ( + + )} + {showVariableSelector && ( + handleVariableSelectorChange(value, variable)} + filterVar={getFilterVar()} + schema={schema} + valueTypePlaceHolder={targetVarType()} + currentTool={currentTool} + currentProvider={currentProvider} + /> + )} +
+ ) +} +export default FormInputItem diff --git a/web/app/components/workflow/nodes/_base/components/form-input-type-switch.tsx b/web/app/components/workflow/nodes/_base/components/form-input-type-switch.tsx new file mode 100644 index 0000000000..391e204844 --- /dev/null +++ b/web/app/components/workflow/nodes/_base/components/form-input-type-switch.tsx @@ -0,0 +1,47 @@ +'use client' +import type { FC } from 'react' +import { useTranslation } from 'react-i18next' +import { + RiEditLine, +} from '@remixicon/react' +import { Variable02 } from '@/app/components/base/icons/src/vender/solid/development' +import Tooltip from '@/app/components/base/tooltip' +import { VarType } from '@/app/components/workflow/nodes/tool/types' +import cn from '@/utils/classnames' + +type Props = { + value: VarType + onChange: (value: VarType) => void +} + +const FormInputTypeSwitch: FC = ({ + value, + onChange, +}) => { + const { t } = useTranslation() + return ( +
+ +
onChange(VarType.variable)} + > + +
+
+ +
onChange(VarType.constant)} + > + +
+
+
+ ) +} +export default FormInputTypeSwitch diff --git a/web/app/components/workflow/nodes/_base/components/mcp-tool-not-support-tooltip.tsx b/web/app/components/workflow/nodes/_base/components/mcp-tool-not-support-tooltip.tsx new file mode 100644 index 0000000000..8117f7502f --- /dev/null +++ b/web/app/components/workflow/nodes/_base/components/mcp-tool-not-support-tooltip.tsx @@ -0,0 +1,22 @@ +'use client' +import Tooltip from '@/app/components/base/tooltip' +import { RiAlertFill } from '@remixicon/react' +import type { FC } from 'react' +import React from 'react' +import { useTranslation } from 'react-i18next' + +const McpToolNotSupportTooltip: FC = () => { + const { t } = useTranslation() + return ( + + {t('plugin.detailPanel.toolSelector.unsupportedMCPTool')} +
+ } + > + + + ) +} +export default React.memo(McpToolNotSupportTooltip) diff --git a/web/app/components/workflow/nodes/_base/components/setting-item.tsx b/web/app/components/workflow/nodes/_base/components/setting-item.tsx index 134bf4a551..abbfaef490 100644 --- a/web/app/components/workflow/nodes/_base/components/setting-item.tsx +++ b/web/app/components/workflow/nodes/_base/components/setting-item.tsx @@ -13,7 +13,7 @@ export const SettingItem = memo(({ label, children, status, tooltip }: SettingIt const indicator: ComponentProps['color'] = status === 'error' ? 'red' : status === 'warning' ? 'yellow' : undefined const needTooltip = ['error', 'warning'].includes(status as any) return
-
+
{label}
diff --git a/web/app/components/workflow/nodes/_base/components/variable/var-reference-picker.tsx b/web/app/components/workflow/nodes/_base/components/variable/var-reference-picker.tsx index 23ccea2572..e6f3ce1fa1 100644 --- a/web/app/components/workflow/nodes/_base/components/variable/var-reference-picker.tsx +++ b/web/app/components/workflow/nodes/_base/components/variable/var-reference-picker.tsx @@ -528,6 +528,7 @@ const VarReferencePicker: FC = ({ onChange={handleVarReferenceChange} itemWidth={isAddBtnTrigger ? 260 : (minWidth || triggerWidth)} isSupportFileVar={isSupportFileVar} + zIndex={zIndex} /> )} diff --git a/web/app/components/workflow/nodes/_base/components/variable/var-reference-popup.tsx b/web/app/components/workflow/nodes/_base/components/variable/var-reference-popup.tsx index 9398ae7361..3746a85441 100644 --- a/web/app/components/workflow/nodes/_base/components/variable/var-reference-popup.tsx +++ b/web/app/components/workflow/nodes/_base/components/variable/var-reference-popup.tsx @@ -13,6 +13,7 @@ type Props = { onChange: (value: ValueSelector, varDetail: Var) => void itemWidth?: number isSupportFileVar?: boolean + zIndex?: number } const VarReferencePopup: FC = ({ vars, @@ -20,6 +21,7 @@ const VarReferencePopup: FC = ({ onChange, itemWidth, isSupportFileVar = true, + zIndex, }) => { const { t } = useTranslation() const docLink = useDocLink() @@ -60,6 +62,7 @@ const VarReferencePopup: FC = ({ onChange={onChange} itemWidth={itemWidth} isSupportFileVar={isSupportFileVar} + zIndex={zIndex} /> }
diff --git a/web/app/components/workflow/nodes/_base/components/variable/var-reference-vars.tsx b/web/app/components/workflow/nodes/_base/components/variable/var-reference-vars.tsx index 27063a2ba3..303840d8e7 100644 --- a/web/app/components/workflow/nodes/_base/components/variable/var-reference-vars.tsx +++ b/web/app/components/workflow/nodes/_base/components/variable/var-reference-vars.tsx @@ -46,6 +46,7 @@ type ItemProps = { isSupportFileVar?: boolean isException?: boolean isLoopVar?: boolean + zIndex?: number } const objVarTypes = [VarType.object, VarType.file] @@ -60,6 +61,7 @@ const Item: FC = ({ isSupportFileVar, isException, isLoopVar, + zIndex, }) => { const isStructureOutput = itemData.type === VarType.object && (itemData.children as StructuredOutput)?.schema?.properties const isFile = itemData.type === VarType.file && !isStructureOutput @@ -171,7 +173,7 @@ const Item: FC = ({
{(isStructureOutput || isObj) && ( void onBlur?: () => void + zIndex?: number autoFocus?: boolean } const VarReferenceVars: FC = ({ @@ -272,6 +275,7 @@ const VarReferenceVars: FC = ({ maxHeightClass, onClose, onBlur, + zIndex, autoFocus = true, }) => { const { t } = useTranslation() @@ -357,6 +361,7 @@ const VarReferenceVars: FC = ({ isSupportFileVar={isSupportFileVar} isException={v.isException} isLoopVar={item.isLoop} + zIndex={zIndex} /> ))}
)) diff --git a/web/app/components/workflow/nodes/_base/node.tsx b/web/app/components/workflow/nodes/_base/node.tsx index 88771d098e..b969702dd7 100644 --- a/web/app/components/workflow/nodes/_base/node.tsx +++ b/web/app/components/workflow/nodes/_base/node.tsx @@ -32,6 +32,7 @@ import { import { useNodeIterationInteractions } from '../iteration/use-interactions' import { useNodeLoopInteractions } from '../loop/use-interactions' import type { IterationNodeType } from '../iteration/types' +import CopyID from '../tool/components/copy-id' import { NodeSourceHandle, NodeTargetHandle, @@ -321,6 +322,11 @@ const BaseNode: FC = ({
) } + {data.type === BlockEnum.Tool && ( +
+ +
+ )}
) diff --git a/web/app/components/workflow/nodes/agent/components/tool-icon.tsx b/web/app/components/workflow/nodes/agent/components/tool-icon.tsx index b94258855a..8616f34200 100644 --- a/web/app/components/workflow/nodes/agent/components/tool-icon.tsx +++ b/web/app/components/workflow/nodes/agent/components/tool-icon.tsx @@ -2,10 +2,11 @@ import Tooltip from '@/app/components/base/tooltip' import Indicator from '@/app/components/header/indicator' import classNames from '@/utils/classnames' import { memo, useMemo, useRef, useState } from 'react' -import { useAllBuiltInTools, useAllCustomTools, useAllWorkflowTools } from '@/service/use-tools' +import { useAllBuiltInTools, useAllCustomTools, useAllMCPTools, useAllWorkflowTools } from '@/service/use-tools' import { getIconFromMarketPlace } from '@/utils/get-icon' import { useTranslation } from 'react-i18next' import { Group } from '@/app/components/base/icons/src/vender/other' +import AppIcon from '@/app/components/base/app-icon' type Status = 'not-installed' | 'not-authorized' | undefined @@ -19,19 +20,21 @@ export const ToolIcon = memo(({ providerName }: ToolIconProps) => { const { data: buildInTools } = useAllBuiltInTools() const { data: customTools } = useAllCustomTools() const { data: workflowTools } = useAllWorkflowTools() - const isDataReady = !!buildInTools && !!customTools && !!workflowTools + const { data: mcpTools } = useAllMCPTools() + const isDataReady = !!buildInTools && !!customTools && !!workflowTools && !!mcpTools const currentProvider = useMemo(() => { - const mergedTools = [...(buildInTools || []), ...(customTools || []), ...(workflowTools || [])] + const mergedTools = [...(buildInTools || []), ...(customTools || []), ...(workflowTools || []), ...(mcpTools || [])] return mergedTools.find((toolWithProvider) => { - return toolWithProvider.name === providerName + return toolWithProvider.name === providerName || toolWithProvider.id === providerName }) - }, [buildInTools, customTools, providerName, workflowTools]) + }, [buildInTools, customTools, providerName, workflowTools, mcpTools]) + const providerNameParts = providerName.split('/') const author = providerNameParts[0] const name = providerNameParts[1] const icon = useMemo(() => { if (!isDataReady) return '' - if (currentProvider) return currentProvider.icon as string + if (currentProvider) return currentProvider.icon const iconFromMarketPlace = getIconFromMarketPlace(`${author}/${name}`) return iconFromMarketPlace }, [author, currentProvider, name, isDataReady]) @@ -62,19 +65,32 @@ export const ToolIcon = memo(({ providerName }: ToolIconProps) => { )} ref={containerRef} > - {(!iconFetchError && isDataReady) - - ? tool icon setIconFetchError(true)} - /> - : - } + {(() => { + if (iconFetchError || !icon) + return + if (typeof icon === 'string') { + return tool icon setIconFetchError(true)} + /> + } + if (typeof icon === 'object') { + return + } + return + })()} {indicator && }
diff --git a/web/app/components/workflow/nodes/agent/default.ts b/web/app/components/workflow/nodes/agent/default.ts index d80def7bd2..4f68cfe87c 100644 --- a/web/app/components/workflow/nodes/agent/default.ts +++ b/web/app/components/workflow/nodes/agent/default.ts @@ -7,6 +7,7 @@ import { renderI18nObject } from '@/i18n' const nodeDefault: NodeDefault = { defaultValue: { + version: '2', }, getAvailablePrevNodes(isChatMode) { return isChatMode @@ -60,15 +61,28 @@ const nodeDefault: NodeDefault = { const schemas = toolValue.schemas || [] const userSettings = toolValue.settings const reasoningConfig = toolValue.parameters + const version = payload.version schemas.forEach((schema: any) => { if (schema?.required) { - if (schema.form === 'form' && !userSettings[schema.name]?.value) { + if (schema.form === 'form' && !version && !userSettings[schema.name]?.value) { return { isValid: false, errorMessage: t('workflow.errorMsg.toolParameterRequired', { field: renderI18nObject(param.label, language), param: renderI18nObject(schema.label, language) }), } } - if (schema.form === 'llm' && reasoningConfig[schema.name].auto === 0 && !userSettings[schema.name]?.value) { + if (schema.form === 'form' && version && !userSettings[schema.name]?.value.value) { + return { + isValid: false, + errorMessage: t('workflow.errorMsg.toolParameterRequired', { field: renderI18nObject(param.label, language), param: renderI18nObject(schema.label, language) }), + } + } + if (schema.form === 'llm' && !version && reasoningConfig[schema.name].auto === 0 && !reasoningConfig[schema.name]?.value) { + return { + isValid: false, + errorMessage: t('workflow.errorMsg.toolParameterRequired', { field: renderI18nObject(param.label, language), param: renderI18nObject(schema.label, language) }), + } + } + if (schema.form === 'llm' && version && reasoningConfig[schema.name].auto === 0 && !reasoningConfig[schema.name]?.value.value) { return { isValid: false, errorMessage: t('workflow.errorMsg.toolParameterRequired', { field: renderI18nObject(param.label, language), param: renderI18nObject(schema.label, language) }), diff --git a/web/app/components/workflow/nodes/agent/node.tsx b/web/app/components/workflow/nodes/agent/node.tsx index d2267fd00f..a2190317af 100644 --- a/web/app/components/workflow/nodes/agent/node.tsx +++ b/web/app/components/workflow/nodes/agent/node.tsx @@ -104,7 +104,7 @@ const AgentNode: FC> = (props) => { {t('workflow.nodes.agent.toolbox')} }>
- {tools.map(tool => )} + {tools.map((tool, i) => )}
}
diff --git a/web/app/components/workflow/nodes/agent/panel.tsx b/web/app/components/workflow/nodes/agent/panel.tsx index 391383031f..6741453944 100644 --- a/web/app/components/workflow/nodes/agent/panel.tsx +++ b/web/app/components/workflow/nodes/agent/panel.tsx @@ -38,11 +38,11 @@ const AgentPanel: FC> = (props) => { readOnly, outputSchema, handleMemoryChange, + canChooseMCPTool, } = useConfig(props.id, props.data) const { t } = useTranslation() const resetEditor = useStore(s => s.setControlPromptEditorRerenderKey) - return
> = (props) => { agent_strategy_label: inputs.agent_strategy_label!, agent_output_schema: inputs.output_schema, plugin_unique_identifier: inputs.plugin_unique_identifier!, + meta: inputs.meta, } : undefined} onStrategyChange={(strategy) => { setInputs({ @@ -65,6 +66,7 @@ const AgentPanel: FC> = (props) => { agent_strategy_label: strategy?.agent_strategy_label, output_schema: strategy!.agent_output_schema, plugin_unique_identifier: strategy!.plugin_unique_identifier, + meta: strategy?.meta, }) resetEditor(Date.now()) }} @@ -74,6 +76,7 @@ const AgentPanel: FC> = (props) => { nodeOutputVars={availableVars} availableNodes={availableNodesWithParent} nodeId={props.id} + canChooseMCPTool={canChooseMCPTool} />
diff --git a/web/app/components/workflow/nodes/agent/types.ts b/web/app/components/workflow/nodes/agent/types.ts index ca8bb5e71d..5a13a4a4f3 100644 --- a/web/app/components/workflow/nodes/agent/types.ts +++ b/web/app/components/workflow/nodes/agent/types.ts @@ -1,14 +1,17 @@ import type { CommonNodeType, Memory } from '@/app/components/workflow/types' import type { ToolVarInputs } from '../tool/types' +import type { PluginMeta } from '@/app/components/plugins/types' export type AgentNodeType = CommonNodeType & { agent_strategy_provider_name?: string agent_strategy_name?: string agent_strategy_label?: string agent_parameters?: ToolVarInputs + meta?: PluginMeta output_schema: Record plugin_unique_identifier?: string memory?: Memory + version?: string } export enum AgentFeature { diff --git a/web/app/components/workflow/nodes/agent/use-config.ts b/web/app/components/workflow/nodes/agent/use-config.ts index c3e07e4e60..50faf03040 100644 --- a/web/app/components/workflow/nodes/agent/use-config.ts +++ b/web/app/components/workflow/nodes/agent/use-config.ts @@ -6,13 +6,16 @@ import { useIsChatMode, useNodesReadOnly, } from '@/app/components/workflow/hooks' -import { useCallback, useMemo } from 'react' +import { useCallback, useEffect, useMemo } from 'react' import { type ToolVarInputs, VarType } from '../tool/types' import { useCheckInstalled, useFetchPluginsInMarketPlaceByIds } from '@/service/use-plugins' import type { Memory, Var } from '../../types' import { VarType as VarKindType } from '../../types' import useAvailableVarList from '../_base/hooks/use-available-var-list' import produce from 'immer' +import { isSupportMCP } from '@/utils/plugin-version-feature' +import { FormTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations' +import { generateAgentToolValue, toolParametersToFormSchemas } from '@/app/components/tools/utils/to-form-schema' export type StrategyStatus = { plugin: { @@ -85,11 +88,12 @@ const useConfig = (id: string, payload: AgentNodeType) => { }) const formData = useMemo(() => { const paramNameList = (currentStrategy?.parameters || []).map(item => item.name) - return Object.fromEntries( + const res = Object.fromEntries( Object.entries(inputs.agent_parameters || {}).filter(([name]) => paramNameList.includes(name)).map(([key, value]) => { return [key, value.value] }), ) + return res }, [inputs.agent_parameters, currentStrategy?.parameters]) const onFormChange = (value: Record) => { const res: ToolVarInputs = {} @@ -105,6 +109,42 @@ const useConfig = (id: string, payload: AgentNodeType) => { }) } + const formattingToolData = (data: any) => { + const settingValues = generateAgentToolValue(data.settings, toolParametersToFormSchemas(data.schemas.filter((param: { form: string }) => param.form !== 'llm') as any)) + const paramValues = generateAgentToolValue(data.parameters, toolParametersToFormSchemas(data.schemas.filter((param: { form: string }) => param.form === 'llm') as any), true) + const res = produce(data, (draft: any) => { + draft.settings = settingValues + draft.parameters = paramValues + }) + return res + } + + const formattingLegacyData = () => { + if (inputs.version) + return inputs + const newData = produce(inputs, (draft) => { + const schemas = currentStrategy?.parameters || [] + Object.keys(draft.agent_parameters || {}).forEach((key) => { + const targetSchema = schemas.find(schema => schema.name === key) + if (targetSchema?.type === FormTypeEnum.toolSelector) + draft.agent_parameters![key].value = formattingToolData(draft.agent_parameters![key].value) + if (targetSchema?.type === FormTypeEnum.multiToolSelector) + draft.agent_parameters![key].value = draft.agent_parameters![key].value.map((tool: any) => formattingToolData(tool)) + }) + draft.version = '2' + }) + return newData + } + + // formatting legacy data + useEffect(() => { + if (!currentStrategy) + return + const newData = formattingLegacyData() + setInputs(newData) + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [currentStrategy]) + // vars const filterMemoryPromptVar = useCallback((varPayload: Var) => { @@ -172,6 +212,7 @@ const useConfig = (id: string, payload: AgentNodeType) => { outputSchema, handleMemoryChange, isChatMode, + canChooseMCPTool: isSupportMCP(inputs.meta?.version), } } diff --git a/web/app/components/workflow/nodes/llm/components/json-schema-config-modal/visual-editor/hooks.ts b/web/app/components/workflow/nodes/llm/components/json-schema-config-modal/visual-editor/hooks.ts index 470a322b13..eb3dff83d8 100644 --- a/web/app/components/workflow/nodes/llm/components/json-schema-config-modal/visual-editor/hooks.ts +++ b/web/app/components/workflow/nodes/llm/components/json-schema-config-modal/visual-editor/hooks.ts @@ -6,6 +6,7 @@ import type { EditData } from './edit-card' import { ArrayType, type Field, Type } from '../../../types' import Toast from '@/app/components/base/toast' import { findPropertyWithPath } from '../../../utils' +import _ from 'lodash' type ChangeEventParams = { path: string[], @@ -19,7 +20,8 @@ type AddEventParams = { } export const useSchemaNodeOperations = (props: VisualEditorProps) => { - const { schema: jsonSchema, onChange } = props + const { schema: jsonSchema, onChange: doOnChange } = props + const onChange = doOnChange || _.noop const backupSchema = useVisualEditorStore(state => state.backupSchema) const setBackupSchema = useVisualEditorStore(state => state.setBackupSchema) const isAddingNewField = useVisualEditorStore(state => state.isAddingNewField) diff --git a/web/app/components/workflow/nodes/llm/components/json-schema-config-modal/visual-editor/index.tsx b/web/app/components/workflow/nodes/llm/components/json-schema-config-modal/visual-editor/index.tsx index 1df42532a6..d96f856bbb 100644 --- a/web/app/components/workflow/nodes/llm/components/json-schema-config-modal/visual-editor/index.tsx +++ b/web/app/components/workflow/nodes/llm/components/json-schema-config-modal/visual-editor/index.tsx @@ -2,24 +2,29 @@ import type { FC } from 'react' import type { SchemaRoot } from '../../../types' import SchemaNode from './schema-node' import { useSchemaNodeOperations } from './hooks' +import cn from '@/utils/classnames' export type VisualEditorProps = { + className?: string schema: SchemaRoot - onChange: (schema: SchemaRoot) => void + rootName?: string + readOnly?: boolean + onChange?: (schema: SchemaRoot) => void } const VisualEditor: FC = (props) => { - const { schema } = props + const { className, schema, readOnly } = props useSchemaNodeOperations(props) return ( -
+
) diff --git a/web/app/components/workflow/nodes/llm/components/json-schema-config-modal/visual-editor/schema-node.tsx b/web/app/components/workflow/nodes/llm/components/json-schema-config-modal/visual-editor/schema-node.tsx index 70a6b861ad..96bbf999db 100644 --- a/web/app/components/workflow/nodes/llm/components/json-schema-config-modal/visual-editor/schema-node.tsx +++ b/web/app/components/workflow/nodes/llm/components/json-schema-config-modal/visual-editor/schema-node.tsx @@ -19,6 +19,7 @@ type SchemaNodeProps = { path: string[] parentPath?: string[] depth: number + readOnly?: boolean } // Support 10 levels of indentation @@ -57,6 +58,7 @@ const SchemaNode: FC = ({ path, parentPath, depth, + readOnly, }) => { const [isExpanded, setIsExpanded] = useState(true) const hoveringProperty = useVisualEditorStore(state => state.hoveringProperty) @@ -77,11 +79,13 @@ const SchemaNode: FC = ({ } const handleMouseEnter = () => { + if(!readOnly) return if (advancedEditing || isAddingNewField) return setHoveringPropertyDebounced(path.join('.')) } const handleMouseLeave = () => { + if(!readOnly) return if (advancedEditing || isAddingNewField) return setHoveringPropertyDebounced(null) } @@ -183,7 +187,7 @@ const SchemaNode: FC = ({ )} { - depth === 0 && !isAddingNewField && ( + !readOnly && depth === 0 && !isAddingNewField && ( ) } diff --git a/web/app/components/workflow/nodes/tool/components/copy-id.tsx b/web/app/components/workflow/nodes/tool/components/copy-id.tsx new file mode 100644 index 0000000000..3a633e1d2e --- /dev/null +++ b/web/app/components/workflow/nodes/tool/components/copy-id.tsx @@ -0,0 +1,51 @@ +'use client' +import React, { useState } from 'react' +import { useTranslation } from 'react-i18next' +import { RiFileCopyLine } from '@remixicon/react' +import copy from 'copy-to-clipboard' +import { debounce } from 'lodash-es' +import Tooltip from '@/app/components/base/tooltip' + +type Props = { + content: string +} + +const prefixEmbedded = 'appOverview.overview.appInfo.embedded' + +const CopyFeedbackNew = ({ content }: Props) => { + const { t } = useTranslation() + const [isCopied, setIsCopied] = useState(false) + + const onClickCopy = debounce(() => { + copy(content) + setIsCopied(true) + }, 100) + + const onMouseLeave = debounce(() => { + setIsCopied(false) + }, 100) + + return ( +
e.stopPropagation()} onMouseLeave={onMouseLeave}> + +
+
{content}
+ +
+
+
+ ) +} + +export default CopyFeedbackNew diff --git a/web/app/components/workflow/nodes/tool/components/mixed-variable-text-input/index.tsx b/web/app/components/workflow/nodes/tool/components/mixed-variable-text-input/index.tsx new file mode 100644 index 0000000000..6680c8ebb6 --- /dev/null +++ b/web/app/components/workflow/nodes/tool/components/mixed-variable-text-input/index.tsx @@ -0,0 +1,62 @@ +import { + memo, +} from 'react' +import { useTranslation } from 'react-i18next' +import PromptEditor from '@/app/components/base/prompt-editor' +import Placeholder from './placeholder' +import type { + Node, + NodeOutPutVar, +} from '@/app/components/workflow/types' +import { BlockEnum } from '@/app/components/workflow/types' +import cn from '@/utils/classnames' + +type MixedVariableTextInputProps = { + readOnly?: boolean + nodesOutputVars?: NodeOutPutVar[] + availableNodes?: Node[] + value?: string + onChange?: (text: string) => void +} +const MixedVariableTextInput = ({ + readOnly = false, + nodesOutputVars, + availableNodes = [], + value = '', + onChange, +}: MixedVariableTextInputProps) => { + const { t } = useTranslation() + return ( + { + acc[node.id] = { + title: node.data.title, + type: node.data.type, + } + if (node.data.type === BlockEnum.Start) { + acc.sys = { + title: t('workflow.blocks.start'), + type: BlockEnum.Start, + } + } + return acc + }, {} as any), + }} + placeholder={} + onChange={onChange} + /> + ) +} + +export default memo(MixedVariableTextInput) diff --git a/web/app/components/workflow/nodes/tool/components/mixed-variable-text-input/placeholder.tsx b/web/app/components/workflow/nodes/tool/components/mixed-variable-text-input/placeholder.tsx new file mode 100644 index 0000000000..3337d6ae66 --- /dev/null +++ b/web/app/components/workflow/nodes/tool/components/mixed-variable-text-input/placeholder.tsx @@ -0,0 +1,51 @@ +import { useCallback } from 'react' +import { useTranslation } from 'react-i18next' +import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext' +import { FOCUS_COMMAND } from 'lexical' +import { $insertNodes } from 'lexical' +import { CustomTextNode } from '@/app/components/base/prompt-editor/plugins/custom-text/node' +import Badge from '@/app/components/base/badge' + +const Placeholder = () => { + const { t } = useTranslation() + const [editor] = useLexicalComposerContext() + + const handleInsert = useCallback((text: string) => { + editor.update(() => { + const textNode = new CustomTextNode(text) + $insertNodes([textNode]) + }) + editor.dispatchCommand(FOCUS_COMMAND, undefined as any) + }, [editor]) + + return ( +
{ + e.stopPropagation() + handleInsert('') + }} + > +
+ {t('workflow.nodes.tool.insertPlaceholder1')} +
/
+
{ + e.stopPropagation() + handleInsert('/') + })} + > + {t('workflow.nodes.tool.insertPlaceholder2')} +
+
+ +
+ ) +} + +export default Placeholder diff --git a/web/app/components/workflow/nodes/tool/components/tool-form/index.tsx b/web/app/components/workflow/nodes/tool/components/tool-form/index.tsx new file mode 100644 index 0000000000..a867797473 --- /dev/null +++ b/web/app/components/workflow/nodes/tool/components/tool-form/index.tsx @@ -0,0 +1,51 @@ +'use client' +import type { FC } from 'react' +import type { ToolVarInputs } from '../../types' +import type { CredentialFormSchema } from '@/app/components/header/account-setting/model-provider-page/declarations' +import ToolFormItem from './item' +import type { ToolWithProvider } from '@/app/components/workflow/types' +import type { Tool } from '@/app/components/tools/types' + +type Props = { + readOnly: boolean + nodeId: string + schema: CredentialFormSchema[] + value: ToolVarInputs + onChange: (value: ToolVarInputs) => void + onOpen?: (index: number) => void + inPanel?: boolean + currentTool?: Tool + currentProvider?: ToolWithProvider +} + +const ToolForm: FC = ({ + readOnly, + nodeId, + schema, + value, + onChange, + inPanel, + currentTool, + currentProvider, +}) => { + return ( +
+ { + schema.map((schema, index) => ( + + )) + } +
+ ) +} +export default ToolForm diff --git a/web/app/components/workflow/nodes/tool/components/tool-form/item.tsx b/web/app/components/workflow/nodes/tool/components/tool-form/item.tsx new file mode 100644 index 0000000000..11de42fe56 --- /dev/null +++ b/web/app/components/workflow/nodes/tool/components/tool-form/item.tsx @@ -0,0 +1,105 @@ +'use client' +import type { FC } from 'react' +import { + RiBracesLine, +} from '@remixicon/react' +import type { ToolVarInputs } from '../../types' +import type { CredentialFormSchema } from '@/app/components/header/account-setting/model-provider-page/declarations' +import { FormTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations' +import { useLanguage } from '@/app/components/header/account-setting/model-provider-page/hooks' +import Button from '@/app/components/base/button' +import Tooltip from '@/app/components/base/tooltip' +import FormInputItem from '@/app/components/workflow/nodes/_base/components/form-input-item' +import { useBoolean } from 'ahooks' +import SchemaModal from '@/app/components/plugins/plugin-detail-panel/tool-selector/schema-modal' +import type { ToolWithProvider } from '@/app/components/workflow/types' +import type { Tool } from '@/app/components/tools/types' + +type Props = { + readOnly: boolean + nodeId: string + schema: CredentialFormSchema + value: ToolVarInputs + onChange: (value: ToolVarInputs) => void + inPanel?: boolean + currentTool?: Tool + currentProvider?: ToolWithProvider +} + +const ToolFormItem: FC = ({ + readOnly, + nodeId, + schema, + value, + onChange, + inPanel, + currentTool, + currentProvider, +}) => { + const language = useLanguage() + const { name, label, type, required, tooltip, input_schema } = schema + const showSchemaButton = type === FormTypeEnum.object || type === FormTypeEnum.array + const showDescription = type === FormTypeEnum.textInput || type === FormTypeEnum.secretInput + const [isShowSchema, { + setTrue: showSchema, + setFalse: hideSchema, + }] = useBoolean(false) + return ( +
+
+
+
{label[language] || label.en_US}
+ {required && ( +
*
+ )} + {!showDescription && tooltip && ( + + {tooltip[language] || tooltip.en_US} +
} + triggerClassName='ml-1 w-4 h-4' + asChild={false} + /> + )} + {showSchemaButton && ( + <> +
·
+ + + )} +
+ {showDescription && tooltip && ( +
{tooltip[language] || tooltip.en_US}
+ )} +
+ + + {isShowSchema && ( + + )} +
+ ) +} +export default ToolFormItem diff --git a/web/app/components/workflow/nodes/tool/default.ts b/web/app/components/workflow/nodes/tool/default.ts index f245929684..1fdb9eed2d 100644 --- a/web/app/components/workflow/nodes/tool/default.ts +++ b/web/app/components/workflow/nodes/tool/default.ts @@ -10,6 +10,7 @@ const nodeDefault: NodeDefault = { defaultValue: { tool_parameters: {}, tool_configurations: {}, + version: '2', }, getAvailablePrevNodes(isChatMode: boolean) { const nodes = isChatMode @@ -55,6 +56,8 @@ const nodeDefault: NodeDefault = { const value = payload.tool_configurations[field.variable] if (!errorMessages && (value === undefined || value === null || value === '')) errorMessages = t(`${i18nPrefix}.fieldRequired`, { field: field.label[language] }) + if (!errorMessages && typeof value === 'object' && !!value.type && (value.value === undefined || value.value === null || value.value === '' || (Array.isArray(value.value) && value.value.length === 0))) + errorMessages = t(`${i18nPrefix}.fieldRequired`, { field: field.label[language] }) }) } diff --git a/web/app/components/workflow/nodes/tool/node.tsx b/web/app/components/workflow/nodes/tool/node.tsx index f3cb4d9fae..e15ddcaaaa 100644 --- a/web/app/components/workflow/nodes/tool/node.tsx +++ b/web/app/components/workflow/nodes/tool/node.tsx @@ -21,14 +21,14 @@ const Node: FC> = ({
{key}
- {typeof tool_configurations[key] === 'string' && ( + {typeof tool_configurations[key].value === 'string' && (
- {paramSchemas?.find(i => i.name === key)?.type === FormTypeEnum.secretInput ? '********' : tool_configurations[key]} + {paramSchemas?.find(i => i.name === key)?.type === FormTypeEnum.secretInput ? '********' : tool_configurations[key].value}
)} - {typeof tool_configurations[key] === 'number' && ( + {typeof tool_configurations[key].value === 'number' && (
- {tool_configurations[key]} + {tool_configurations[key].value}
)} {typeof tool_configurations[key] !== 'string' && tool_configurations[key]?.type === FormTypeEnum.modelSelector && ( @@ -36,11 +36,6 @@ const Node: FC> = ({ {tool_configurations[key].model}
)} - {/* {typeof tool_configurations[key] !== 'string' && tool_configurations[key]?.type === FormTypeEnum.appSelector && ( -
- {tool_configurations[key].app_id} -
- )} */}
))} diff --git a/web/app/components/workflow/nodes/tool/panel.tsx b/web/app/components/workflow/nodes/tool/panel.tsx index 038159870e..936f730a46 100644 --- a/web/app/components/workflow/nodes/tool/panel.tsx +++ b/web/app/components/workflow/nodes/tool/panel.tsx @@ -4,11 +4,10 @@ import { useTranslation } from 'react-i18next' import Split from '../_base/components/split' import type { ToolNodeType } from './types' import useConfig from './use-config' -import InputVarList from './components/input-var-list' +import ToolForm from './components/tool-form' import Button from '@/app/components/base/button' import Field from '@/app/components/workflow/nodes/_base/components/field' import type { NodePanelProps } from '@/app/components/workflow/types' -import Form from '@/app/components/header/account-setting/model-provider-page/model-modal/Form' import ConfigCredential from '@/app/components/tools/setting/build-in/config-credentials' import Loading from '@/app/components/base/loading' import OutputVars, { VarItem } from '@/app/components/workflow/nodes/_base/components/output-vars' @@ -28,8 +27,6 @@ const Panel: FC> = ({ inputs, toolInputVarSchema, setInputVar, - handleOnVarOpen, - filterVar, toolSettingSchema, toolSettingValue, setToolSettingValue, @@ -45,6 +42,8 @@ const Panel: FC> = ({ currTool, } = useConfig(id, data) + const [collapsed, setCollapsed] = React.useState(false) + if (isLoading) { return
@@ -66,21 +65,19 @@ const Panel: FC> = ({
)} - {!isShowAuthBtn && <> -
+ {!isShowAuthBtn && ( +
{toolInputVarSchema.length > 0 && ( - @@ -88,24 +85,29 @@ const Panel: FC> = ({ )} {toolInputVarSchema.length > 0 && toolSettingSchema.length > 0 && ( - + )} -
+ {toolSettingSchema.length > 0 && ( + <> + + + + + + )}
- } + )} {showSetAuth && ( output_schema: Record paramSchemas?: Record[] + version?: string } diff --git a/web/app/components/workflow/nodes/tool/use-config.ts b/web/app/components/workflow/nodes/tool/use-config.ts index b83ae8a07f..ea8d0e21ca 100644 --- a/web/app/components/workflow/nodes/tool/use-config.ts +++ b/web/app/components/workflow/nodes/tool/use-config.ts @@ -8,10 +8,12 @@ import { useLanguage } from '@/app/components/header/account-setting/model-provi import useNodeCrud from '@/app/components/workflow/nodes/_base/hooks/use-node-crud' import { CollectionType } from '@/app/components/tools/types' import { updateBuiltInToolCredential } from '@/service/tools' -import { addDefaultValue, toolParametersToFormSchemas } from '@/app/components/tools/utils/to-form-schema' +import { + getConfiguredValue, + toolParametersToFormSchemas, +} from '@/app/components/tools/utils/to-form-schema' import Toast from '@/app/components/base/toast' -import { VarType as VarVarType } from '@/app/components/workflow/types' -import type { InputVar, Var } from '@/app/components/workflow/types' +import type { InputVar } from '@/app/components/workflow/types' import { useFetchToolsData, useNodesReadOnly, @@ -26,17 +28,18 @@ const useConfig = (id: string, payload: ToolNodeType) => { const language = useLanguage() const { inputs, setInputs: doSetInputs } = useNodeCrud(id, payload) /* - * tool_configurations: tool setting, not dynamic setting - * tool_parameters: tool dynamic setting(by user) + * tool_configurations: tool setting, not dynamic setting (form type = form) + * tool_parameters: tool dynamic setting(form type = llm) * output_schema: tool dynamic output */ - const { provider_id, provider_type, tool_name, tool_configurations, output_schema } = inputs + const { provider_id, provider_type, tool_name, tool_configurations, output_schema, tool_parameters } = inputs const isBuiltIn = provider_type === CollectionType.builtIn const buildInTools = useStore(s => s.buildInTools) const customTools = useStore(s => s.customTools) const workflowTools = useStore(s => s.workflowTools) + const mcpTools = useStore(s => s.mcpTools) - const currentTools = (() => { + const currentTools = useMemo(() => { switch (provider_type) { case CollectionType.builtIn: return buildInTools @@ -44,10 +47,12 @@ const useConfig = (id: string, payload: ToolNodeType) => { return customTools case CollectionType.workflow: return workflowTools + case CollectionType.mcp: + return mcpTools default: return [] } - })() + }, [buildInTools, customTools, mcpTools, provider_type, workflowTools]) const currCollection = currentTools.find(item => canFindTool(item.id, provider_id)) // Auth @@ -91,10 +96,10 @@ const useConfig = (id: string, payload: ToolNodeType) => { const value = newConfig[key] if (schema?.type === 'boolean') { if (typeof value === 'string') - newConfig[key] = Number.parseInt(value, 10) + newConfig[key] = value === 'true' || value === '1' - if (typeof value === 'boolean') - newConfig[key] = value ? 1 : 0 + if (typeof value === 'number') + newConfig[key] = value === 1 } if (schema?.type === 'number-input') { @@ -107,12 +112,11 @@ const useConfig = (id: string, payload: ToolNodeType) => { doSetInputs(newInputs) }, [doSetInputs, formSchemas, hasShouldTransferTypeSettingInput]) const [notSetDefaultValue, setNotSetDefaultValue] = useState(false) - const toolSettingValue = (() => { + const toolSettingValue = useMemo(() => { if (notSetDefaultValue) return tool_configurations - - return addDefaultValue(tool_configurations, toolSettingSchema) - })() + return getConfiguredValue(tool_configurations, toolSettingSchema) + }, [notSetDefaultValue, toolSettingSchema, tool_configurations]) const setToolSettingValue = useCallback((value: Record) => { setNotSetDefaultValue(true) setInputs({ @@ -121,16 +125,20 @@ const useConfig = (id: string, payload: ToolNodeType) => { }) }, [inputs, setInputs]) - useEffect(() => { - if (!currTool) - return + const formattingParameters = () => { const inputsWithDefaultValue = produce(inputs, (draft) => { if (!draft.tool_configurations || Object.keys(draft.tool_configurations).length === 0) - draft.tool_configurations = addDefaultValue(tool_configurations, toolSettingSchema) - - if (!draft.tool_parameters) - draft.tool_parameters = {} + draft.tool_configurations = getConfiguredValue(tool_configurations, toolSettingSchema) + if (!draft.tool_parameters || Object.keys(draft.tool_parameters).length === 0) + draft.tool_parameters = getConfiguredValue(tool_parameters, toolInputVarSchema) }) + return inputsWithDefaultValue + } + + useEffect(() => { + if (!currTool) + return + const inputsWithDefaultValue = formattingParameters() setInputs(inputsWithDefaultValue) // eslint-disable-next-line react-hooks/exhaustive-deps }, [currTool]) @@ -143,19 +151,6 @@ const useConfig = (id: string, payload: ToolNodeType) => { }) }, [inputs, setInputs]) - const [currVarIndex, setCurrVarIndex] = useState(-1) - const currVarType = toolInputVarSchema[currVarIndex]?._type - const handleOnVarOpen = useCallback((index: number) => { - setCurrVarIndex(index) - }, []) - - const filterVar = useCallback((varPayload: Var) => { - if (currVarType) - return varPayload.type === currVarType - - return varPayload.type !== VarVarType.arrayFile - }, [currVarType]) - const isLoading = currTool && (isBuiltIn ? !currCollection : false) const getMoreDataForCheckValid = () => { @@ -220,8 +215,6 @@ const useConfig = (id: string, payload: ToolNodeType) => { setToolSettingValue, toolInputVarSchema, setInputVar, - handleOnVarOpen, - filterVar, currCollection, isShowAuthBtn, showSetAuth, diff --git a/web/app/components/workflow/nodes/tool/use-single-run-form-params.ts b/web/app/components/workflow/nodes/tool/use-single-run-form-params.ts index 295cf02639..6fc79beebe 100644 --- a/web/app/components/workflow/nodes/tool/use-single-run-form-params.ts +++ b/web/app/components/workflow/nodes/tool/use-single-run-form-params.ts @@ -34,7 +34,12 @@ const useSingleRunFormParams = ({ const hadVarParams = Object.keys(inputs.tool_parameters) .filter(key => inputs.tool_parameters[key].type !== VarType.constant) .map(k => inputs.tool_parameters[k]) - const varInputs = getInputVars(hadVarParams.map((p) => { + + const hadVarSettings = Object.keys(inputs.tool_configurations) + .filter(key => typeof inputs.tool_configurations[key] === 'object' && inputs.tool_configurations[key].type && inputs.tool_configurations[key].type !== VarType.constant) + .map(k => inputs.tool_configurations[k]) + + const varInputs = getInputVars([...hadVarParams, ...hadVarSettings].map((p) => { if (p.type === VarType.variable) { // handle the old wrong value not crash the page if (!(p.value as any).join) @@ -55,8 +60,11 @@ const useSingleRunFormParams = ({ const res = produce(inputVarValues, (draft) => { Object.keys(inputs.tool_parameters).forEach((key: string) => { const { type, value } = inputs.tool_parameters[key] - if (type === VarType.constant && (value === undefined || value === null)) + if (type === VarType.constant && (value === undefined || value === null)) { + if(!draft.tool_parameters || !draft.tool_parameters[key]) + return draft[key] = value + } }) }) return res diff --git a/web/app/components/workflow/store/workflow/tool-slice.ts b/web/app/components/workflow/store/workflow/tool-slice.ts index 2d54bbd925..d6d89abcf0 100644 --- a/web/app/components/workflow/store/workflow/tool-slice.ts +++ b/web/app/components/workflow/store/workflow/tool-slice.ts @@ -10,6 +10,8 @@ export type ToolSliceShape = { setCustomTools: (tools: ToolWithProvider[]) => void workflowTools: ToolWithProvider[] setWorkflowTools: (tools: ToolWithProvider[]) => void + mcpTools: ToolWithProvider[] + setMcpTools: (tools: ToolWithProvider[]) => void toolPublished: boolean setToolPublished: (toolPublished: boolean) => void } @@ -21,6 +23,8 @@ export const createToolSlice: StateCreator = set => ({ setCustomTools: customTools => set(() => ({ customTools })), workflowTools: [], setWorkflowTools: workflowTools => set(() => ({ workflowTools })), + mcpTools: [], + setMcpTools: mcpTools => set(() => ({ mcpTools })), toolPublished: false, setToolPublished: toolPublished => set(() => ({ toolPublished })), }) diff --git a/web/app/components/workflow/types.ts b/web/app/components/workflow/types.ts index 3248ce798d..507d494c74 100644 --- a/web/app/components/workflow/types.ts +++ b/web/app/components/workflow/types.ts @@ -16,6 +16,7 @@ import type { } from '@/app/components/workflow/nodes/_base/components/error-handle/types' import type { WorkflowRetryConfig } from '@/app/components/workflow/nodes/_base/components/retry/types' import type { StructuredOutput } from '@/app/components/workflow/nodes/llm/types' +import type { PluginMeta } from '../plugins/types' export enum BlockEnum { Start = 'start', @@ -410,6 +411,7 @@ export type MoreInfo = { export type ToolWithProvider = Collection & { tools: Tool[] + meta: PluginMeta } export enum SupportUploadFileTypes { diff --git a/web/app/components/workflow/utils/workflow-init.ts b/web/app/components/workflow/utils/workflow-init.ts index 93a61230ba..dc22d61ca5 100644 --- a/web/app/components/workflow/utils/workflow-init.ts +++ b/web/app/components/workflow/utils/workflow-init.ts @@ -28,6 +28,7 @@ import type { IfElseNodeType } from '../nodes/if-else/types' import { branchNameCorrect } from '../nodes/if-else/utils' import type { IterationNodeType } from '../nodes/iteration/types' import type { LoopNodeType } from '../nodes/loop/types' +import type { ToolNodeType } from '../nodes/tool/types' import { getIterationStartNode, getLoopStartNode, @@ -276,6 +277,7 @@ export const initialNodes = (originNodes: Node[], originEdges: Edge[]) => { if (node.data.type === BlockEnum.ParameterExtractor) (node as any).data.model.provider = correctModelProvider((node as any).data.model.provider) + if (node.data.type === BlockEnum.HttpRequest && !node.data.retry_config) { node.data.retry_config = { retry_enabled: true, @@ -284,6 +286,24 @@ export const initialNodes = (originNodes: Node[], originEdges: Edge[]) => { } } + if (node.data.type === BlockEnum.Tool && !(node as Node).data.version) { + (node as Node).data.version = '2' + + const toolConfigurations = (node as Node).data.tool_configurations + if (toolConfigurations && Object.keys(toolConfigurations).length > 0) { + const newValues = { ...toolConfigurations } + Object.keys(toolConfigurations).forEach((key) => { + if (typeof toolConfigurations[key] !== 'object' || toolConfigurations[key] === null) { + newValues[key] = { + type: 'constant', + value: toolConfigurations[key], + } + } + }); + (node as Node).data.tool_configurations = newValues + } + } + return node }) } diff --git a/web/app/oauth-callback/page.tsx b/web/app/oauth-callback/page.tsx new file mode 100644 index 0000000000..38355f435e --- /dev/null +++ b/web/app/oauth-callback/page.tsx @@ -0,0 +1,10 @@ +'use client' +import { useOAuthCallback } from '@/hooks/use-oauth' + +const OAuthCallback = () => { + useOAuthCallback() + + return
+} + +export default OAuthCallback diff --git a/web/hooks/use-oauth.ts b/web/hooks/use-oauth.ts new file mode 100644 index 0000000000..ae9c1cda66 --- /dev/null +++ b/web/hooks/use-oauth.ts @@ -0,0 +1,36 @@ +'use client' +import { useEffect } from 'react' + +export const useOAuthCallback = () => { + useEffect(() => { + if (window.opener) { + window.opener.postMessage({ + type: 'oauth_callback', + }, '*') + window.close() + } + }, []) +} + +export const openOAuthPopup = (url: string, callback: () => void) => { + const width = 600 + const height = 600 + const left = window.screenX + (window.outerWidth - width) / 2 + const top = window.screenY + (window.outerHeight - height) / 2 + + const popup = window.open( + url, + 'OAuth', + `width=${width},height=${height},left=${left},top=${top},scrollbars=yes`, + ) + + const handleMessage = (event: MessageEvent) => { + if (event.data?.type === 'oauth_callback') { + window.removeEventListener('message', handleMessage) + callback() + } + } + + window.addEventListener('message', handleMessage) + return popup +} diff --git a/web/i18n/de-DE/plugin.ts b/web/i18n/de-DE/plugin.ts index 05988dedf1..87f222be94 100644 --- a/web/i18n/de-DE/plugin.ts +++ b/web/i18n/de-DE/plugin.ts @@ -55,7 +55,7 @@ const translation = { unsupportedContent: 'Die installierte Plug-in-Version bietet diese Aktion nicht.', unsupportedTitle: 'Nicht unterstützte Aktion', descriptionPlaceholder: 'Kurze Beschreibung des Zwecks des Werkzeugs, z. B. um die Temperatur für einen bestimmten Ort zu ermitteln.', - auto: 'Automatisch', + auto: 'Auto', params: 'KONFIGURATION DER ARGUMENTATION', unsupportedContent2: 'Klicken Sie hier, um die Version zu wechseln.', placeholder: 'Wählen Sie ein Werkzeug aus...', diff --git a/web/i18n/de-DE/tools.ts b/web/i18n/de-DE/tools.ts index 2f3c24b9da..6e6eda85e0 100644 --- a/web/i18n/de-DE/tools.ts +++ b/web/i18n/de-DE/tools.ts @@ -137,21 +137,97 @@ const translation = { notAuthorized: 'Werkzeug nicht autorisiert', howToGet: 'Wie erhält man', addToolModal: { + type: 'Art', + category: 'Kategorie', + add: 'hinzufügen', added: 'zugefügt', manageInTools: 'Verwalten in Tools', - add: 'hinzufügen', - category: 'Kategorie', - emptyTitle: 'Kein Workflow-Tool verfügbar', - type: 'Art', - emptyTip: 'Gehen Sie zu "Workflow -> Als Tool veröffentlichen"', - emptyTitleCustom: 'Kein benutzerdefiniertes Tool verfügbar', - emptyTipCustom: 'Erstellen eines benutzerdefinierten Werkzeugs', + custom: { + title: 'Kein benutzerdefiniertes Werkzeug verfügbar', + tip: 'Benutzerdefiniertes Werkzeug erstellen', + }, + workflow: { + title: 'Kein Workflow-Werkzeug verfügbar', + tip: 'Veröffentlichen Sie Workflows als Werkzeuge im Studio', + }, + mcp: { + title: 'Kein MCP-Werkzeug verfügbar', + tip: 'Einen MCP-Server hinzufügen', + }, + agent: { + title: 'Keine Agentenstrategie verfügbar', + }, }, toolNameUsageTip: 'Name des Tool-Aufrufs für die Argumentation und Aufforderung des Agenten', customToolTip: 'Erfahren Sie mehr über benutzerdefinierte Dify-Tools', openInStudio: 'In Studio öffnen', noTools: 'Keine Werkzeuge gefunden', copyToolName: 'Name kopieren', + mcp: { + create: { + cardTitle: 'MCP-Server hinzufügen (HTTP)', + cardLink: 'Mehr über MCP-Server-Integration erfahren', + }, + noConfigured: 'Nicht konfigurierter Server', + updateTime: 'Aktualisiert', + toolsCount: '{{count}} Tools', + noTools: 'Keine Tools verfügbar', + modal: { + title: 'MCP-Server hinzufügen (HTTP)', + editTitle: 'MCP-Server bearbeiten (HTTP)', + name: 'Name & Symbol', + namePlaceholder: 'Benennen Sie Ihren MCP-Server', + serverUrl: 'Server-URL', + serverUrlPlaceholder: 'URL zum Server-Endpunkt', + serverUrlWarning: 'Das Ändern der Serveradresse kann Anwendungen unterbrechen, die von diesem Server abhängen', + serverIdentifier: 'Serverkennung', + serverIdentifierTip: 'Eindeutige Kennung für den MCP-Server im Arbeitsbereich. Nur Kleinbuchstaben, Zahlen, Unterstriche und Bindestriche. Maximal 24 Zeichen.', + serverIdentifierPlaceholder: 'Eindeutige Kennung, z.B. mein-mcp-server', + serverIdentifierWarning: 'Nach einer ID-Änderung wird der Server von vorhandenen Apps nicht erkannt', + cancel: 'Abbrechen', + save: 'Speichern', + confirm: 'Hinzufügen & Autorisieren', + }, + delete: 'MCP-Server entfernen', + deleteConfirmTitle: 'Möchten Sie {{mcp}} entfernen?', + operation: { + edit: 'Bearbeiten', + remove: 'Entfernen', + }, + authorize: 'Autorisieren', + authorizing: 'Wird autorisiert...', + authorizingRequired: 'Autorisierung erforderlich', + authorizeTip: 'Nach der Autorisierung werden Tools hier angezeigt.', + update: 'Aktualisieren', + updating: 'Wird aktualisiert', + gettingTools: 'Tools werden abgerufen...', + updateTools: 'Tools werden aktualisiert...', + toolsEmpty: 'Tools nicht geladen', + getTools: 'Tools abrufen', + toolUpdateConfirmTitle: 'Tool-Liste aktualisieren', + toolUpdateConfirmContent: 'Das Aktualisieren der Tool-Liste kann bestehende Apps beeinflussen. Fortfahren?', + toolsNum: '{{count}} Tools enthalten', + onlyTool: '1 Tool enthalten', + identifier: 'Serverkennung (Zum Kopieren klicken)', + server: { + title: 'MCP-Server', + url: 'Server-URL', + reGen: 'Server-URL neu generieren?', + addDescription: 'Beschreibung hinzufügen', + edit: 'Beschreibung bearbeiten', + modal: { + addTitle: 'Beschreibung hinzufügen, um MCP-Server zu aktivieren', + editTitle: 'Beschreibung bearbeiten', + description: 'Beschreibung', + descriptionPlaceholder: 'Erklären Sie, was dieses Tool tut und wie es vom LLM verwendet werden soll', + parameters: 'Parameter', + parametersTip: 'Fügen Sie Beschreibungen für jeden Parameter hinzu, um dem LLM Zweck und Einschränkungen zu verdeutlichen.', + parametersPlaceholder: 'Zweck und Einschränkungen des Parameters', + confirm: 'MCP-Server aktivieren', + }, + publishTip: 'App nicht veröffentlicht. Bitte zuerst die App veröffentlichen.', + }, + }, } export default translation diff --git a/web/i18n/en-US/plugin.ts b/web/i18n/en-US/plugin.ts index 9fba3a4714..cf9df89a6b 100644 --- a/web/i18n/en-US/plugin.ts +++ b/web/i18n/en-US/plugin.ts @@ -85,8 +85,8 @@ const translation = { settings: 'USER SETTINGS', params: 'REASONING CONFIG', paramsTip1: 'Controls LLM inference parameters.', - paramsTip2: 'When \'Automatic\' is off, the default value is used.', - auto: 'Automatic', + paramsTip2: 'When \'Auto\' is off, the default value is used.', + auto: 'Auto', empty: 'Click the \'+\' button to add tools. You can add multiple tools.', uninstalledTitle: 'Tool not installed', uninstalledContent: 'This plugin is installed from the local/GitHub repository. Please use after installation.', @@ -94,6 +94,7 @@ const translation = { unsupportedTitle: 'Unsupported Action', unsupportedContent: 'The installed plugin version does not provide this action.', unsupportedContent2: 'Click to switch version.', + unsupportedMCPTool: 'Currently selected agent strategy plugin version does not support MCP tools.', }, configureApp: 'Configure App', configureModel: 'Configure model', diff --git a/web/i18n/en-US/tools.ts b/web/i18n/en-US/tools.ts index 433e98720a..418d1cb076 100644 --- a/web/i18n/en-US/tools.ts +++ b/web/i18n/en-US/tools.ts @@ -28,10 +28,21 @@ const translation = { add: 'add', added: 'added', manageInTools: 'Manage in Tools', - emptyTitle: 'No workflow tool available', - emptyTip: 'Go to "Workflow -> Publish as Tool"', - emptyTitleCustom: 'No custom tool available', - emptyTipCustom: 'Create a custom tool', + custom: { + title: 'No custom tool available', + tip: 'Create a custom tool', + }, + workflow: { + title: 'No workflow tool available', + tip: 'Publish workflows as tools in Studio', + }, + mcp: { + title: 'No MCP tool available', + tip: 'Add an MCP server', + }, + agent: { + title: 'No agent strategy available', + }, }, createTool: { title: 'Create Custom Tool', @@ -152,6 +163,71 @@ const translation = { toolNameUsageTip: 'Tool call name for agent reasoning and prompting', copyToolName: 'Copy Name', noTools: 'No tools found', + mcp: { + create: { + cardTitle: 'Add MCP Server (HTTP)', + cardLink: 'Learn more about MCP server integration', + }, + noConfigured: 'Unconfigured', + updateTime: 'Updated', + toolsCount: '{{count}} tools', + noTools: 'No tools available', + modal: { + title: 'Add MCP Server (HTTP)', + editTitle: 'Edit MCP Server (HTTP)', + name: 'Name & Icon', + namePlaceholder: 'Name your MCP server', + serverUrl: 'Server URL', + serverUrlPlaceholder: 'URL to server endpoint', + serverUrlWarning: 'Updating the server address may disrupt applications that depend on this server', + serverIdentifier: 'Server Identifier', + serverIdentifierTip: 'Unique identifier for the MCP server within the workspace. Lowercase letters, numbers, underscores, and hyphens only. Up to 24 characters.', + serverIdentifierPlaceholder: 'Unique identifier, e.g., my-mcp-server', + serverIdentifierWarning: 'The server won’t be recognized by existing apps after an ID change', + cancel: 'Cancel', + save: 'Save', + confirm: 'Add & Authorize', + }, + delete: 'Remove MCP Server', + deleteConfirmTitle: 'Would you like to remove {{mcp}}?', + operation: { + edit: 'Edit', + remove: 'Remove', + }, + authorize: 'Authorize', + authorizing: 'Authorizing...', + authorizingRequired: 'Authorization is required', + authorizeTip: 'After authorization, tools will be displayed here.', + update: 'Update', + updating: 'Updating', + gettingTools: 'Getting Tools...', + updateTools: 'Updating Tools...', + toolsEmpty: 'Tools not loaded', + getTools: 'Get tools', + toolUpdateConfirmTitle: 'Update Tool List', + toolUpdateConfirmContent: 'Updating the tool list may affect existing apps. Do you wish to proceed?', + toolsNum: '{{count}} tools included', + onlyTool: '1 tool included', + identifier: 'Server Identifier (Click to Copy)', + server: { + title: 'MCP Server', + url: 'Server URL', + reGen: 'Do you want to regenerator server URL?', + addDescription: 'Add description', + edit: 'Edit description', + modal: { + addTitle: 'Add description to enable MCP server', + editTitle: 'Edit description', + description: 'Description', + descriptionPlaceholder: 'Explain what this tool does and how it should be used by the LLM', + parameters: 'Parameters', + parametersTip: 'Add descriptions for each parameter to help the LLM understand their purpose and constraints.', + parametersPlaceholder: 'Parameter purpose and constraints', + confirm: 'Enable MCP Server', + }, + publishTip: 'App not published. Please publish the app first.', + }, + }, } export default translation diff --git a/web/i18n/en-US/workflow.ts b/web/i18n/en-US/workflow.ts index 88873fb27e..c56b497ac2 100644 --- a/web/i18n/en-US/workflow.ts +++ b/web/i18n/en-US/workflow.ts @@ -231,6 +231,8 @@ const translation = { 'utilities': 'Utilities', 'noResult': 'No match found', 'agent': 'Agent Strategy', + 'allAdded': 'All added', + 'addAll': 'Add all', }, blocks: { 'start': 'Start', @@ -368,6 +370,10 @@ const translation = { ms: 'ms', retries: '{{num}} Retries', }, + typeSwitch: { + input: 'Input value', + variable: 'Use variable', + }, }, start: { required: 'required', @@ -662,6 +668,9 @@ const translation = { tool: { authorize: 'Authorize', inputVars: 'Input Variables', + settings: 'Settings', + insertPlaceholder1: 'Type or press', + insertPlaceholder2: 'insert variable', outputVars: { text: 'tool generated content', files: { @@ -889,6 +898,8 @@ const translation = { install: 'Install', cancel: 'Cancel', }, + clickToViewParameterSchema: 'Click to view parameter schema', + parameterSchema: 'Parameter Schema', }, }, tracing: { diff --git a/web/i18n/es-ES/plugin.ts b/web/i18n/es-ES/plugin.ts index d8250c6b41..84e317add6 100644 --- a/web/i18n/es-ES/plugin.ts +++ b/web/i18n/es-ES/plugin.ts @@ -51,11 +51,11 @@ const translation = { unsupportedContent2: 'Haga clic para cambiar de versión.', descriptionPlaceholder: 'Breve descripción del propósito de la herramienta, por ejemplo, obtener la temperatura para una ubicación específica.', empty: 'Haga clic en el botón \'+\' para agregar herramientas. Puede agregar varias herramientas.', - paramsTip2: 'Cuando \'Automático\' está desactivado, se utiliza el valor predeterminado.', + paramsTip2: 'Cuando \'Auto\' está desactivado, se utiliza el valor predeterminado.', uninstalledTitle: 'Herramienta no instalada', descriptionLabel: 'Descripción de la herramienta', unsupportedContent: 'La versión del plugin instalado no proporciona esta acción.', - auto: 'Automático', + auto: 'Auto', title: 'Agregar herramienta', placeholder: 'Seleccione una herramienta...', uninstalledContent: 'Este plugin se instala desde el repositorio local/GitHub. Úselo después de la instalación.', diff --git a/web/i18n/es-ES/tools.ts b/web/i18n/es-ES/tools.ts index fd37eef5b1..b503f9c41b 100644 --- a/web/i18n/es-ES/tools.ts +++ b/web/i18n/es-ES/tools.ts @@ -28,10 +28,21 @@ const translation = { add: 'agregar', added: 'agregada', manageInTools: 'Administrar en Herramientas', - emptyTitle: 'No hay herramientas de flujo de trabajo disponibles', - emptyTip: 'Ir a "Flujo de Trabajo -> Publicar como Herramienta"', - emptyTitleCustom: 'No hay herramienta personalizada disponible', - emptyTipCustom: 'Crear una herramienta personalizada', + custom: { + title: 'No hay herramienta personalizada disponible', + tip: 'Crear una herramienta personalizada', + }, + workflow: { + title: 'No hay herramienta de flujo de trabajo disponible', + tip: 'Publicar flujos de trabajo como herramientas en el Estudio', + }, + mcp: { + title: 'No hay herramienta MCP disponible', + tip: 'Añadir un servidor MCP', + }, + agent: { + title: 'No hay estrategia de agente disponible', + }, }, createTool: { title: 'Crear Herramienta Personalizada', @@ -152,6 +163,71 @@ const translation = { toolNameUsageTip: 'Nombre de llamada de la herramienta para razonamiento y promoción de agentes', copyToolName: 'Nombre de la copia', noTools: 'No se han encontrado herramientas', + mcp: { + create: { + cardTitle: 'Añadir servidor MCP (HTTP)', + cardLink: 'Más información sobre integración de servidores MCP', + }, + noConfigured: 'Servidor no configurado', + updateTime: 'Actualizado', + toolsCount: '{{count}} herramientas', + noTools: 'No hay herramientas disponibles', + modal: { + title: 'Añadir servidor MCP (HTTP)', + editTitle: 'Editar servidor MCP (HTTP)', + name: 'Nombre e Icono', + namePlaceholder: 'Nombre de su servidor MCP', + serverUrl: 'URL del servidor', + serverUrlPlaceholder: 'URL del endpoint del servidor', + serverUrlWarning: 'Actualizar la dirección del servidor puede interrumpir aplicaciones que dependan de él', + serverIdentifier: 'Identificador del servidor', + serverIdentifierTip: 'Identificador único del servidor MCP en el espacio de trabajo. Solo letras minúsculas, números, guiones bajos y guiones. Máximo 24 caracteres.', + serverIdentifierPlaceholder: 'Identificador único, ej. mi-servidor-mcp', + serverIdentifierWarning: 'El servidor no será reconocido por aplicaciones existentes tras cambiar la ID', + cancel: 'Cancelar', + save: 'Guardar', + confirm: 'Añadir y Autorizar', + }, + delete: 'Eliminar servidor MCP', + deleteConfirmTitle: '¿Eliminar {{mcp}}?', + operation: { + edit: 'Editar', + remove: 'Eliminar', + }, + authorize: 'Autorizar', + authorizing: 'Autorizando...', + authorizingRequired: 'Se requiere autorización', + authorizeTip: 'Tras la autorización, las herramientas se mostrarán aquí.', + update: 'Actualizar', + updating: 'Actualizando', + gettingTools: 'Obteniendo herramientas...', + updateTools: 'Actualizando herramientas...', + toolsEmpty: 'Herramientas no cargadas', + getTools: 'Obtener herramientas', + toolUpdateConfirmTitle: 'Actualizar lista de herramientas', + toolUpdateConfirmContent: 'Actualizar la lista puede afectar a aplicaciones existentes. ¿Continuar?', + toolsNum: '{{count}} herramientas incluidas', + onlyTool: '1 herramienta incluida', + identifier: 'Identificador del servidor (Haz clic para copiar)', + server: { + title: 'Servidor MCP', + url: 'URL del servidor', + reGen: '¿Regenerar URL del servidor?', + addDescription: 'Añadir descripción', + edit: 'Editar descripción', + modal: { + addTitle: 'Añade descripción para habilitar el servidor MCP', + editTitle: 'Editar descripción', + description: 'Descripción', + descriptionPlaceholder: 'Explica qué hace esta herramienta y cómo debe usarla el LLM', + parameters: 'Parámetros', + parametersTip: 'Añade descripciones de cada parámetro para ayudar al LLM a entender su propósito y restricciones.', + parametersPlaceholder: 'Propósito y restricciones del parámetro', + confirm: 'Habilitar servidor MCP', + }, + publishTip: 'App no publicada. Publícala primero.', + }, + }, } export default translation diff --git a/web/i18n/fa-IR/tools.ts b/web/i18n/fa-IR/tools.ts index fddfd2d826..942bde7932 100644 --- a/web/i18n/fa-IR/tools.ts +++ b/web/i18n/fa-IR/tools.ts @@ -28,10 +28,21 @@ const translation = { add: 'افزودن', added: 'افزوده شد', manageInTools: 'مدیریت در ابزارها', - emptyTitle: 'هیچ ابزار جریان کاری در دسترس نیست', - emptyTip: 'به "جریان کاری -> انتشار به عنوان ابزار" بروید', - emptyTipCustom: 'ایجاد یک ابزار سفارشی', - emptyTitleCustom: 'هیچ ابزار سفارشی در دسترس نیست', + custom: { + title: 'هیچ ابزار سفارشی موجود نیست', + tip: 'یک ابزار سفارشی ایجاد کنید', + }, + workflow: { + title: 'هیچ ابزار جریان کاری موجود نیست', + tip: 'جریان‌های کاری را به عنوان ابزار در استودیو منتشر کنید', + }, + mcp: { + title: 'هیچ ابزار MCP موجود نیست', + tip: 'یک سرور MCP اضافه کنید', + }, + agent: { + title: 'هیچ استراتژی عاملی موجود نیست', + }, }, createTool: { title: 'ایجاد ابزار سفارشی', @@ -152,6 +163,71 @@ const translation = { toolNameUsageTip: 'نام فراخوانی ابزار برای استدلال و پرامپت‌های عامل', copyToolName: 'کپی نام', noTools: 'هیچ ابزاری یافت نشد', + mcp: { + create: { + cardTitle: 'افزودن سرور MCP (HTTP)', + cardLink: 'اطلاعات بیشتر درباره یکپارچه‌سازی سرور MCP', + }, + noConfigured: 'سرور پیکربندی نشده', + updateTime: 'آخرین بروزرسانی', + toolsCount: '{count} ابزار', + noTools: 'ابزاری موجود نیست', + modal: { + title: 'افزودن سرور MCP (HTTP)', + editTitle: 'ویرایش سرور MCP (HTTP)', + name: 'نام و آیکون', + namePlaceholder: 'برای سرور MCP خود نام انتخاب کنید', + serverUrl: 'آدرس سرور', + serverUrlPlaceholder: 'URL نقطه پایانی سرور', + serverUrlWarning: 'به‌روزرسانی آدرس سرور ممکن است برنامه‌های وابسته به آن را مختل کند', + serverIdentifier: 'شناسه سرور', + serverIdentifierTip: 'شناسه منحصر به فرد برای سرور MCP در فضای کاری. فقط حروف کوچک، اعداد، زیرخط و خط تیره. حداکثر 24 کاراکتر.', + serverIdentifierPlaceholder: 'شناسه منحصر به فرد، مثال: my-mcp-server', + serverIdentifierWarning: 'پس از تغییر شناسه، سرور توسط برنامه‌های موجود شناسایی نخواهد شد', + cancel: 'لغو', + save: 'ذخیره', + confirm: 'افزودن و مجوزدهی', + }, + delete: 'حذف سرور MCP', + deleteConfirmTitle: 'آیا مایل به حذف {mcp} هستید؟', + operation: { + edit: 'ویرایش', + remove: 'حذف', + }, + authorize: 'مجوزدهی', + authorizing: 'در حال مجوزدهی...', + authorizingRequired: 'مجوز مورد نیاز است', + authorizeTip: 'پس از مجوزدهی، ابزارها در اینجا نمایش داده می‌شوند.', + update: 'به‌روزرسانی', + updating: 'در حال به‌روزرسانی...', + gettingTools: 'دریافت ابزارها...', + updateTools: 'به‌روزرسانی ابزارها...', + toolsEmpty: 'ابزارها بارگیری نشدند', + getTools: 'دریافت ابزارها', + toolUpdateConfirmTitle: 'به‌روزرسانی فهرست ابزارها', + toolUpdateConfirmContent: 'به‌روزرسانی فهرست ابزارها ممکن است بر برنامه‌های موجود تأثیر بگذارد. آیا ادامه می‌دهید؟', + toolsNum: '{count} ابزار موجود است', + onlyTool: '1 ابزار موجود است', + identifier: 'شناسه سرور (کلیک برای کپی)', + server: { + title: 'سرور MCP', + url: 'آدرس سرور', + reGen: 'تولید مجدد آدرس سرور؟', + addDescription: 'افزودن توضیحات', + edit: 'ویرایش توضیحات', + modal: { + addTitle: 'برای فعال‌سازی سرور MCP توضیحات اضافه کنید', + editTitle: 'ویرایش توضیحات', + description: 'توضیحات', + descriptionPlaceholder: 'عملکرد این ابزار و نحوه استفاده LLM از آن را توضیح دهید', + parameters: 'پارامترها', + parametersTip: 'برای کمک به LLM در درک هدف و محدودیت‌ها، برای هر پارامتر توضیحات اضافه کنید.', + parametersPlaceholder: 'هدف و محدودیت‌های پارامتر', + confirm: 'فعال‌سازی سرور MCP', + }, + publishTip: 'برنامه منتشر نشده است. لطفاً ابتدا برنامه را منتشر کنید.', + }, + }, } export default translation diff --git a/web/i18n/fr-FR/plugin.ts b/web/i18n/fr-FR/plugin.ts index 35d36b425a..60366e28cf 100644 --- a/web/i18n/fr-FR/plugin.ts +++ b/web/i18n/fr-FR/plugin.ts @@ -53,14 +53,14 @@ const translation = { placeholder: 'Sélectionnez un outil...', params: 'CONFIGURATION DE RAISONNEMENT', unsupportedContent: 'La version du plugin installée ne fournit pas cette action.', - auto: 'Automatique', + auto: 'Auto', descriptionPlaceholder: 'Brève description de l’objectif de l’outil, par exemple, obtenir la température d’un endroit spécifique.', unsupportedContent2: 'Cliquez pour changer de version.', uninstalledTitle: 'Outil non installé', empty: 'Cliquez sur le bouton « + » pour ajouter des outils. Vous pouvez ajouter plusieurs outils.', toolLabel: 'Outil', settings: 'PARAMÈTRES UTILISATEUR', - paramsTip2: 'Lorsque « Automatique » est désactivé, la valeur par défaut est utilisée.', + paramsTip2: 'Lorsque « Auto » est désactivé, la valeur par défaut est utilisée.', paramsTip1: 'Contrôle les paramètres d’inférence LLM.', toolSetting: 'Paramètres de l\'outil', }, diff --git a/web/i18n/fr-FR/tools.ts b/web/i18n/fr-FR/tools.ts index 8f2362daf1..fdbe213df8 100644 --- a/web/i18n/fr-FR/tools.ts +++ b/web/i18n/fr-FR/tools.ts @@ -138,20 +138,96 @@ const translation = { howToGet: 'Comment obtenir', addToolModal: { type: 'type', - emptyTitle: 'Aucun outil de flux de travail disponible', added: 'supplémentaire', add: 'ajouter', category: 'catégorie', manageInTools: 'Gérer dans Outils', - emptyTip: 'Allez dans « Flux de travail -> Publier en tant qu’outil »', - emptyTitleCustom: 'Aucun outil personnalisé disponible', - emptyTipCustom: 'Créer un outil personnalisé', + custom: { + title: 'Aucun outil personnalisé disponible', + tip: 'Créer un outil personnalisé', + }, + workflow: { + title: 'Aucun outil de workflow disponible', + tip: 'Publier des workflows en tant qu\'outils dans le Studio', + }, + mcp: { + title: 'Aucun outil MCP disponible', + tip: 'Ajouter un serveur MCP', + }, + agent: { + title: 'Aucune stratégie d\'agent disponible', + }, }, openInStudio: 'Ouvrir dans Studio', customToolTip: 'En savoir plus sur les outils personnalisés Dify', toolNameUsageTip: 'Nom de l’appel de l’outil pour le raisonnement et l’invite de l’agent', copyToolName: 'Copier le nom', noTools: 'Aucun outil trouvé', + mcp: { + create: { + cardTitle: 'Ajouter un Serveur MCP (HTTP)', + cardLink: 'En savoir plus sur l\'intégration du serveur MCP', + }, + noConfigured: 'Serveur Non Configuré', + updateTime: 'Mis à jour', + toolsCount: '{count} outils', + noTools: 'Aucun outil disponible', + modal: { + title: 'Ajouter un Serveur MCP (HTTP)', + editTitle: 'Modifier le Serveur MCP (HTTP)', + name: 'Nom & Icône', + namePlaceholder: 'Nommez votre serveur MCP', + serverUrl: 'URL du Serveur', + serverUrlPlaceholder: 'URL de l\'endpoint du serveur', + serverUrlWarning: 'Mettre à jour l\'adresse du serveur peut perturber les applications qui dépendent de ce serveur', + serverIdentifier: 'Identifiant du Serveur', + serverIdentifierTip: 'Identifiant unique pour le serveur MCP au sein de l\'espace de travail. Seulement des lettres minuscules, des chiffres, des traits de soulignement et des tirets. Jusqu\'à 24 caractères.', + serverIdentifierPlaceholder: 'Identifiant unique, par ex. mon-serveur-mcp', + serverIdentifierWarning: 'Le serveur ne sera pas reconnu par les applications existantes après un changement d\'ID', + cancel: 'Annuler', + save: 'Enregistrer', + confirm: 'Ajouter & Authoriser', + }, + delete: 'Supprimer le Serveur MCP', + deleteConfirmTitle: 'Souhaitez-vous supprimer {mcp}?', + operation: { + edit: 'Modifier', + remove: 'Supprimer', + }, + authorize: 'Autoriser', + authorizing: 'Autorisation en cours...', + authorizingRequired: 'L\'autorisation est requise', + authorizeTip: 'Après autorisation, les outils seront affichés ici.', + update: 'Mettre à jour', + updating: 'Mise à jour en cours', + gettingTools: 'Obtention des Outils...', + updateTools: 'Mise à jour des Outils...', + toolsEmpty: 'Outils non chargés', + getTools: 'Obtenir des outils', + toolUpdateConfirmTitle: 'Mettre à jour la Liste des Outils', + toolUpdateConfirmContent: 'La mise à jour de la liste des outils peut affecter les applications existantes. Souhaitez-vous continuer?', + toolsNum: '{count} outils inclus', + onlyTool: '1 outil inclus', + identifier: 'Identifiant du Serveur (Cliquez pour Copier)', + server: { + title: 'Serveur MCP', + url: 'URL du Serveur', + reGen: 'Voulez-vous régénérer l\'URL du serveur?', + addDescription: 'Ajouter une description', + edit: 'Modifier la description', + modal: { + addTitle: 'Ajouter une description pour activer le serveur MCP', + editTitle: 'Modifier la description', + description: 'Description', + descriptionPlaceholder: 'Expliquez ce que fait cet outil et comment il doit être utilisé par le LLM', + parameters: 'Paramètres', + parametersTip: 'Ajoutez des descriptions pour chaque paramètre afin d\'aider le LLM à comprendre leur objectif et leurs contraintes.', + parametersPlaceholder: 'Objectif et contraintes du paramètre', + confirm: 'Activer le Serveur MCP', + }, + publishTip: 'Application non publiée. Merci de publier l\'application en premier.', + }, + }, } export default translation diff --git a/web/i18n/hi-IN/tools.ts b/web/i18n/hi-IN/tools.ts index 105e7e5fa6..8f721da44e 100644 --- a/web/i18n/hi-IN/tools.ts +++ b/web/i18n/hi-IN/tools.ts @@ -29,10 +29,21 @@ const translation = { add: 'जोड़ें', added: 'जोड़ा गया', manageInTools: 'उपकरणों में प्रबंधित करें', - emptyTitle: 'कोई कार्यप्रवाह उपकरण उपलब्ध नहीं', - emptyTip: 'कार्यप्रवाह -> उपकरण के रूप में प्रकाशित पर जाएं', - emptyTipCustom: 'एक कस्टम टूल बनाएं', - emptyTitleCustom: 'कोई कस्टम टूल उपलब्ध नहीं है', + custom: { + title: 'कोई कस्टम टूल उपलब्ध नहीं है', + tip: 'एक कस्टम टूल बनाएं', + }, + workflow: { + title: 'कोई वर्कफ़्लो टूल उपलब्ध नहीं है', + tip: 'स्टूडियो में टूल के रूप में वर्कफ़्लो प्रकाशित करें', + }, + mcp: { + title: 'कोई MCP टूल उपलब्ध नहीं है', + tip: 'एक MCP सर्वर जोड़ें', + }, + agent: { + title: 'कोई एजेंट रणनीति उपलब्ध नहीं है', + }, }, createTool: { title: 'कस्टम उपकरण बनाएं', @@ -157,6 +168,71 @@ const translation = { toolNameUsageTip: 'एजेंट तर्क और प्रेरण के लिए उपकरण कॉल नाम', noTools: 'कोई उपकरण नहीं मिला', copyToolName: 'नाम कॉपी करें', + mcp: { + create: { + cardTitle: 'MCP सर्वर जोड़ें (HTTP)', + cardLink: 'MCP सर्वर एकीकरण के बारे में अधिक जानें', + }, + noConfigured: 'कॉन्फ़िगर न किया गया सर्वर', + updateTime: 'अपडेट किया गया', + toolsCount: '{count} टूल्स', + noTools: 'कोई टूल उपलब्ध नहीं', + modal: { + title: 'MCP सर्वर जोड़ें (HTTP)', + editTitle: 'MCP सर्वर संपादित करें (HTTP)', + name: 'नाम और आइकन', + namePlaceholder: 'अपने MCP सर्वर को नाम दें', + serverUrl: 'सर्वर URL', + serverUrlPlaceholder: 'सर्वर एंडपॉइंट का URL', + serverUrlWarning: 'सर्वर पता अपडेट करने से इस सर्वर पर निर्भर एप्लिकेशन बाधित हो सकते हैं', + serverIdentifier: 'सर्वर आईडेंटिफ़ायर', + serverIdentifierTip: 'वर्कस्पेस में MCP सर्वर के लिए अद्वितीय आईडेंटिफ़ायर। केवल लोअरकेस अक्षर, संख्याएँ, अंडरस्कोर और हाइफ़न। अधिकतम 24 वर्ण।', + serverIdentifierPlaceholder: 'अद्वितीय आईडेंटिफ़ायर, उदा. my-mcp-server', + serverIdentifierWarning: 'आईडी बदलने के बाद सर्वर को मौजूदा ऐप्स द्वारा पहचाना नहीं जाएगा', + cancel: 'रद्द करें', + save: 'सहेजें', + confirm: 'जोड़ें और अधिकृत करें', + }, + delete: 'MCP सर्वर हटाएँ', + deleteConfirmTitle: '{mcp} हटाना चाहते हैं?', + operation: { + edit: 'संपादित करें', + remove: 'हटाएँ', + }, + authorize: 'अधिकृत करें', + authorizing: 'अधिकृत किया जा रहा है...', + authorizingRequired: 'प्राधिकरण आवश्यक है', + authorizeTip: 'अधिकृत होने के बाद, टूल यहाँ प्रदर्शित होंगे।', + update: 'अपडेट करें', + updating: 'अपडेट हो रहा है...', + gettingTools: 'टूल्स प्राप्त किए जा रहे हैं...', + updateTools: 'टूल्स अपडेट किए जा रहे हैं...', + toolsEmpty: 'टूल्स लोड नहीं हुए', + getTools: 'टूल्स प्राप्त करें', + toolUpdateConfirmTitle: 'टूल सूची अपडेट करें', + toolUpdateConfirmContent: 'टूल सूची अपडेट करने से मौजूदा ऐप्स प्रभावित हो सकते हैं। आगे बढ़ना चाहते हैं?', + toolsNum: '{count} टूल्स शामिल', + onlyTool: '1 टूल शामिल', + identifier: 'सर्वर आईडेंटिफ़ायर (कॉपी करने के लिए क्लिक करें)', + server: { + title: 'MCP सर्वर', + url: 'सर्वर URL', + reGen: 'सर्वर URL पुनः उत्पन्न करना चाहते हैं?', + addDescription: 'विवरण जोड़ें', + edit: 'विवरण संपादित करें', + modal: { + addTitle: 'MCP सर्वर सक्षम करने के लिए विवरण जोड़ें', + editTitle: 'विवरण संपादित करें', + description: 'विवरण', + descriptionPlaceholder: 'समझाएं कि यह टूल क्या करता है और LLM द्वारा इसका उपयोग कैसे किया जाना चाहिए', + parameters: 'पैरामीटर्स', + parametersTip: 'प्रत्येक पैरामीटर के लिए विवरण जोड़ें ताकि LLM को उनके उद्देश्य और बाधाओं को समझने में मदद मिले।', + parametersPlaceholder: 'पैरामीटर उद्देश्य और बाधाएँ', + confirm: 'MCP सर्वर सक्षम करें', + }, + publishTip: 'ऐप प्रकाशित नहीं हुआ। कृपया पहले ऐप प्रकाशित करें।', + }, + }, } export default translation diff --git a/web/i18n/it-IT/tools.ts b/web/i18n/it-IT/tools.ts index 3c89d3a749..8aa119b45a 100644 --- a/web/i18n/it-IT/tools.ts +++ b/web/i18n/it-IT/tools.ts @@ -29,10 +29,21 @@ const translation = { add: 'aggiungi', added: 'aggiunto', manageInTools: 'Gestisci in Strumenti', - emptyTitle: 'Nessun strumento di flusso di lavoro disponibile', - emptyTip: 'Vai a `Flusso di lavoro -> Pubblica come Strumento`', - emptyTitleCustom: 'Nessun attrezzo personalizzato disponibile', - emptyTipCustom: 'Creare uno strumento personalizzato', + custom: { + title: 'Nessuno strumento personalizzato disponibile', + tip: 'Crea uno strumento personalizzato', + }, + workflow: { + title: 'Nessuno strumento workflow disponibile', + tip: 'Pubblica i workflow come strumenti nello Studio', + }, + mcp: { + title: 'Nessuno strumento MCP disponibile', + tip: 'Aggiungi un server MCP', + }, + agent: { + title: 'Nessuna strategia agente disponibile', + }, }, createTool: { title: 'Crea Strumento Personalizzato', @@ -162,6 +173,71 @@ const translation = { 'Nome chiamata strumento per il ragionamento e il prompting dell\'agente', noTools: 'Nessun utensile trovato', copyToolName: 'Copia nome', + mcp: { + create: { + cardTitle: 'Aggiungi Server MCP (HTTP)', + cardLink: 'Scopri di più sull\'integrazione del server MCP', + }, + noConfigured: 'Server Non Configurato', + updateTime: 'Aggiornato', + toolsCount: '{count} strumenti', + noTools: 'Nessuno strumento disponibile', + modal: { + title: 'Aggiungi Server MCP (HTTP)', + editTitle: 'Modifica Server MCP (HTTP)', + name: 'Nome & Icona', + namePlaceholder: 'Dai un nome al tuo server MCP', + serverUrl: 'URL del Server', + serverUrlPlaceholder: 'URL dell\'endpoint del server', + serverUrlWarning: 'L\'aggiornamento dell\'indirizzo del server può interrompere le applicazioni che dipendono da questo server', + serverIdentifier: 'Identificatore del Server', + serverIdentifierTip: 'Identificatore unico per il server MCP all\'interno dello spazio di lavoro. Solo lettere minuscole, numeri, underscore e trattini. Fino a 24 caratteri.', + serverIdentifierPlaceholder: 'Identificatore unico, es. mio-server-mcp', + serverIdentifierWarning: 'Il server non sarà riconosciuto dalle app esistenti dopo una modifica dell\'ID', + cancel: 'Annulla', + save: 'Salva', + confirm: 'Aggiungi & Autorizza', + }, + delete: 'Rimuovi Server MCP', + deleteConfirmTitle: 'Vuoi rimuovere {mcp}?', + operation: { + edit: 'Modifica', + remove: 'Rimuovi', + }, + authorize: 'Autorizza', + authorizing: 'Autorizzando...', + authorizingRequired: 'Autorizzazione richiesta', + authorizeTip: 'Dopo l\'autorizzazione, gli strumenti verranno visualizzati qui.', + update: 'Aggiorna', + updating: 'Aggiornamento in corso', + gettingTools: 'Ottimizzando Strumenti...', + updateTools: 'Aggiornando Strumenti...', + toolsEmpty: 'Strumenti non caricati', + getTools: 'Ottieni strumenti', + toolUpdateConfirmTitle: 'Aggiorna Lista Strumenti', + toolUpdateConfirmContent: 'L\'aggiornamento della lista degli strumenti può influire sulle app esistenti. Vuoi procedere?', + toolsNum: '{count} strumenti inclusi', + onlyTool: '1 strumento incluso', + identifier: 'Identificatore del Server (Fai clic per Copiare)', + server: { + title: 'Server MCP', + url: 'URL del Server', + reGen: 'Vuoi rigenerare l\'URL del server?', + addDescription: 'Aggiungi descrizione', + edit: 'Modifica descrizione', + modal: { + addTitle: 'Aggiungi descrizione per abilitare il server MCP', + editTitle: 'Modifica descrizione', + description: 'Descrizione', + descriptionPlaceholder: 'Spiega cosa fa questo strumento e come dovrebbe essere utilizzato dal LLM', + parameters: 'Parametri', + parametersTip: 'Aggiungi descrizioni per ogni parametro per aiutare il LLM a comprendere il loro scopo e le loro restrizioni.', + parametersPlaceholder: 'Scopo e restrizioni del parametro', + confirm: 'Abilitare Server MCP', + }, + publishTip: 'App non pubblicata. Pubblica l\'app prima.', + }, + }, } export default translation diff --git a/web/i18n/ja-JP/tools.ts b/web/i18n/ja-JP/tools.ts index cf9dad95b3..d69cd4a6f5 100644 --- a/web/i18n/ja-JP/tools.ts +++ b/web/i18n/ja-JP/tools.ts @@ -28,10 +28,21 @@ const translation = { add: '追加', added: '追加済', manageInTools: 'ツールリストに移動して管理する', - emptyTitle: '利用可能なワークフローツールはありません', - emptyTip: '追加するには、「ワークフロー -> ツールとして公開」に移動する', - emptyTitleCustom: 'カスタムツールはありません', - emptyTipCustom: 'カスタムツールの作成', + custom: { + title: 'カスタムツールはありません', + tip: 'カスタムツールを作成する', + }, + workflow: { + title: '利用可能なワークフローツールはありません', + tip: 'スタジオでワークフローをツールに公開する', + }, + mcp: { + title: '利用可能なMCPツールはありません', + tip: 'MCPサーバーを追加する', + }, + agent: { + title: 'Agent strategy は利用できません', + }, }, createTool: { title: 'カスタムツールを作成する', @@ -152,6 +163,72 @@ const translation = { toolNameUsageTip: 'ツール呼び出し名、エージェントの推論とプロンプトの単語に使用されます', copyToolName: '名前をコピー', noTools: 'ツールが見つかりませんでした', + mcp: { + create: { + cardTitle: 'MCPサーバー(HTTP)を追加', + cardLink: 'MCPサーバー統合について詳しく知る', + }, + noConfigured: '未設定', + updateTime: '更新日時', + toolsCount: '{{count}} 個のツール', + noTools: '利用可能なツールはありません', + modal: { + title: 'MCPサーバー(HTTP)を追加', + editTitle: 'MCPサーバー(HTTP)を編集', + name: '名前とアイコン', + namePlaceholder: 'MCPサーバーの名前を入力', + serverUrl: 'サーバーURL', + serverUrlPlaceholder: 'サーバーエンドポイントのURLを入力', + serverUrlWarning: 'サーバーアドレスを更新すると、このサーバーに依存するアプリケーションに影響を与える可能性があります。', + serverIdentifier: 'サーバー識別子', + serverIdentifierTip: 'ワークスペース内でのMCPサーバーのユニーク識別子です。使用可能な文字は小文字、数字、アンダースコア、ハイフンで、最大24文字です。', + serverIdentifierPlaceholder: 'ユニーク識別子(例:my-mcp-server)', + serverIdentifierWarning: 'IDを変更すると、既存のアプリケーションではサーバーが認識できなくなります。', + cancel: 'キャンセル', + save: '保存', + confirm: '追加して承認', + }, + delete: 'MCPサーバーを削除', + deleteConfirmTitle: '{{mcp}} を削除しますか?', + operation: { + edit: '編集', + remove: '削除', + }, + authorize: '承認', + authorizing: '承認中...', + authorizingRequired: '承認が必要です。', + authorizeTip: '承認後、このページにツールが表示されるようになります。', + update: '更新', + updating: '更新中...', + gettingTools: 'ツール取得中...', + updateTools: 'ツール更新中...', + toolsEmpty: 'ツールが読み込まれていません', + getTools: 'ツールを取得', + toolUpdateConfirmTitle: 'ツールリストの更新', + toolUpdateConfirmContent: 'ツールリストを更新すると、既存のアプリケーションに重大な影響を与える可能性があります。続行しますか?', + toolsNum: '{{count}} 個のツールが含まれています', + onlyTool: '1つのツールが含まれています', + identifier: 'サーバー識別子(クリックしてコピー)', + server: { + title: 'MCPサーバー', + url: 'サーバーURL', + reGen: 'サーバーURLを再生成しますか?', + addDescription: '説明を追加', + edit: '説明を編集', + modal: { + addTitle: 'MCPサーバーを有効化するための説明を追加', + editTitle: '説明を編集', + description: '説明', + descriptionPlaceholder: 'このツールの機能とLLM(大規模言語モデル)での使用方法を説明してください。', + parameters: 'パラメータ', + parametersTip: '各パラメータの説明を追加して、LLMがその目的と制約を理解できるようにします。', + parametersPlaceholder: 'パラメータの目的と制約', + confirm: 'MCPサーバーを有効にする', + }, + publishTip: 'アプリが公開されていません。まずアプリを公開してください。', + }, + }, + } export default translation diff --git a/web/i18n/ko-KR/tools.ts b/web/i18n/ko-KR/tools.ts index 45c63b5f80..f660790265 100644 --- a/web/i18n/ko-KR/tools.ts +++ b/web/i18n/ko-KR/tools.ts @@ -28,10 +28,21 @@ const translation = { add: '추가', added: '추가됨', manageInTools: '도구에서 관리', - emptyTitle: '사용 가능한 워크플로우 도구 없음', - emptyTip: '"워크플로우 -> 도구로 등록하기"로 이동', - emptyTipCustom: '사용자 지정 도구 만들기', - emptyTitleCustom: '사용 가능한 사용자 지정 도구가 없습니다.', + custom: { + title: '사용자 정의 도구 없음', + tip: '사용자 정의 도구 생성', + }, + workflow: { + title: '워크플로우 도구 없음', + tip: '스튜디오에서 워크플로우를 도구로 게시', + }, + mcp: { + title: 'MCP 도구 없음', + tip: 'MCP 서버 추가', + }, + agent: { + title: '에이전트 전략 없음', + }, }, createTool: { title: '커스텀 도구 만들기', @@ -152,6 +163,71 @@ const translation = { toolNameUsageTip: 'Agent 추리와 프롬프트를 위한 도구 호출 이름', noTools: '도구를 찾을 수 없습니다.', copyToolName: '이름 복사', + mcp: { + create: { + cardTitle: 'MCP 서버 추가 (HTTP)', + cardLink: 'MCP 서버 통합에 대해 자세히 알아보기', + }, + noConfigured: '구성되지 않은 서버', + updateTime: '업데이트됨', + toolsCount: '{count} 도구', + noTools: '사용 가능한 도구 없음', + modal: { + title: 'MCP 서버 추가 (HTTP)', + editTitle: 'MCP 서버 수정 (HTTP)', + name: '이름 및 아이콘', + namePlaceholder: 'MCP 서버 이름 지정', + serverUrl: '서버 URL', + serverUrlPlaceholder: '서버 엔드포인트 URL', + serverUrlWarning: '서버 주소를 업데이트하면 이 서버에 의존하는 응용 프로그램에 지장이 발생할 수 있습니다', + serverIdentifier: '서버 식별자', + serverIdentifierTip: '작업 공간 내에서 MCP 서버의 고유 식별자. 소문자, 숫자, 밑줄 및 하이픈만 사용 가능. 최대 24자.', + serverIdentifierPlaceholder: '고유 식별자, 예: my-mcp-server', + serverIdentifierWarning: 'ID 변경 후 기존 앱에서 서버를 인식하지 못합니다', + cancel: '취소', + save: '저장', + confirm: '추가 및 승인', + }, + delete: 'MCP 서버 제거', + deleteConfirmTitle: '{mcp}를 제거하시겠습니까?', + operation: { + edit: '편집', + remove: '제거', + }, + authorize: '권한 부여', + authorizing: '권한 부여 중...', + authorizingRequired: '권한이 필요합니다', + authorizeTip: '권한 부여 후 도구가 여기에 표시됩니다.', + update: '업데이트', + updating: '업데이트 중', + gettingTools: '도구 가져오는 중...', + updateTools: '도구 업데이트 중...', + toolsEmpty: '도구가 로드되지 않음', + getTools: '도구 가져오기', + toolUpdateConfirmTitle: '도구 목록 업데이트', + toolUpdateConfirmContent: '도구 목록을 업데이트하면 기존 앱에 영향을 줄 수 있습니다. 계속하시겠습니까?', + toolsNum: '{count} 도구가 포함됨', + onlyTool: '1개 도구 포함', + identifier: '서버 식별자 (클릭하여 복사)', + server: { + title: 'MCP 서버', + url: '서버 URL', + reGen: '서버 URL을 다시 생성하시겠습니까?', + addDescription: '설명 추가', + edit: '설명 수정', + modal: { + addTitle: 'MCP 서버를 활성화하기 위한 설명 추가', + editTitle: '설명 수정', + description: '설명', + descriptionPlaceholder: '이 도구가 수행하는 작업과 LLM이 사용하는 방법을 설명하세요.', + parameters: '매개변수', + parametersTip: '각 매개변수의 설명을 추가하여 LLM이 목적과 제한 사항을 이해할 수 있도록 도와주세요.', + parametersPlaceholder: '매개변수의 목적 및 제한 사항', + confirm: 'MCP 서버 활성화', + }, + publishTip: '앱이 게시되지 않았습니다. 먼저 앱을 게시하십시오.', + }, + }, } export default translation diff --git a/web/i18n/pl-PL/plugin.ts b/web/i18n/pl-PL/plugin.ts index 948bf6e8fb..d5c05d0df8 100644 --- a/web/i18n/pl-PL/plugin.ts +++ b/web/i18n/pl-PL/plugin.ts @@ -51,7 +51,7 @@ const translation = { paramsTip1: 'Steruje parametrami wnioskowania LLM.', unsupportedContent: 'Zainstalowana wersja wtyczki nie zapewnia tej akcji.', params: 'KONFIGURACJA ROZUMOWANIA', - auto: 'Automatyczne', + auto: 'Auto', empty: 'Kliknij przycisk "+", aby dodać narzędzia. Możesz dodać wiele narzędzi.', descriptionLabel: 'Opis narzędzia', title: 'Dodaj narzędzie', @@ -60,7 +60,7 @@ const translation = { uninstalledContent: 'Ta wtyczka jest instalowana z repozytorium lokalnego/GitHub. Proszę użyć po instalacji.', unsupportedTitle: 'Nieobsługiwana akcja', uninstalledTitle: 'Narzędzie nie jest zainstalowane', - paramsTip2: 'Gdy opcja "Automatycznie" jest wyłączona, używana jest wartość domyślna.', + paramsTip2: 'Gdy opcja "Auto" jest wyłączona, używana jest wartość domyślna.', toolLabel: 'Narzędzie', toolSetting: 'Ustawienia narzędzi', }, diff --git a/web/i18n/pl-PL/tools.ts b/web/i18n/pl-PL/tools.ts index e9d92d150e..183abc3f31 100644 --- a/web/i18n/pl-PL/tools.ts +++ b/web/i18n/pl-PL/tools.ts @@ -146,16 +146,92 @@ const translation = { type: 'typ', category: 'kategoria', add: 'dodawać', - emptyTitle: 'Brak dostępnego narzędzia do przepływu pracy', - emptyTip: 'Przejdź do "Przepływ pracy -> Opublikuj jako narzędzie"', - emptyTitleCustom: 'Brak dostępnego narzędzia niestandardowego', - emptyTipCustom: 'Tworzenie narzędzia niestandardowego', + custom: { + title: 'Brak dostępnego narzędzia niestandardowego', + tip: 'Utwórz narzędzie niestandardowe', + }, + workflow: { + title: 'Brak dostępnego narzędzia workflow', + tip: 'Publikuj przepływy pracy jako narzędzia w Studio', + }, + mcp: { + title: 'Brak dostępnego narzędzia MCP', + tip: 'Dodaj serwer MCP', + }, + agent: { + title: 'Brak dostępnej strategii agenta', + }, }, openInStudio: 'Otwieranie w Studio', customToolTip: 'Dowiedz się więcej o niestandardowych narzędziach Dify', toolNameUsageTip: 'Nazwa wywołania narzędzia do wnioskowania i podpowiadania agentowi', noTools: 'Nie znaleziono narzędzi', copyToolName: 'Kopiuj nazwę', + mcp: { + create: { + cardTitle: 'Dodaj serwer MCP (HTTP)', + cardLink: 'Dowiedz się więcej o integracji serwera MCP', + }, + noConfigured: 'Serwer nieskonfigurowany', + updateTime: 'Zaktualizowano', + toolsCount: '{count} narzędzi', + noTools: 'Brak dostępnych narzędzi', + modal: { + title: 'Dodaj serwer MCP (HTTP)', + editTitle: 'Edytuj serwer MCP (HTTP)', + name: 'Nazwa i ikona', + namePlaceholder: 'Nazwij swój serwer MCP', + serverUrl: 'URL serwera', + serverUrlPlaceholder: 'URL do punktu końcowego serwera', + serverUrlWarning: 'Aktualizacja adresu serwera może zakłócić działanie aplikacji od niego zależnych', + serverIdentifier: 'Identyfikator serwera', + serverIdentifierTip: 'Unikalny identyfikator serwera MCP w obszarze roboczym. Tylko małe litery, cyfry, podkreślenia i myślniki. Maks. 24 znaki.', + serverIdentifierPlaceholder: 'Unikalny identyfikator, np. my-mcp-server', + serverIdentifierWarning: 'Po zmianie ID serwer nie będzie rozpoznawany przez istniejące aplikacje', + cancel: 'Anuluj', + save: 'Zapisz', + confirm: 'Dodaj i autoryzuj', + }, + delete: 'Usuń serwer MCP', + deleteConfirmTitle: 'Usunąć {mcp}?', + operation: { + edit: 'Edytuj', + remove: 'Usuń', + }, + authorize: 'Autoryzuj', + authorizing: 'Autoryzowanie...', + authorizingRequired: 'Wymagana autoryzacja', + authorizeTip: 'Po autoryzacji narzędzia będą wyświetlane tutaj.', + update: 'Aktualizuj', + updating: 'Aktualizowanie...', + gettingTools: 'Pobieranie narzędzi...', + updateTools: 'Aktualizowanie narzędzi...', + toolsEmpty: 'Narzędzia niezaładowane', + getTools: 'Pobierz narzędzia', + toolUpdateConfirmTitle: 'Aktualizuj listę narzędzi', + toolUpdateConfirmContent: 'Aktualizacja listy narzędzi może wpłynąć na istniejące aplikacje. Kontynuować?', + toolsNum: '{count} narzędzi zawartych', + onlyTool: '1 narzędzie zawarte', + identifier: 'Identyfikator serwera (Kliknij, aby skopiować)', + server: { + title: 'Serwer MCP', + url: 'URL serwera', + reGen: 'Wygenerować ponownie URL serwera?', + addDescription: 'Dodaj opis', + edit: 'Edytuj opis', + modal: { + addTitle: 'Dodaj opis, aby aktywować serwer MCP', + editTitle: 'Edytuj opis', + description: 'Opis', + descriptionPlaceholder: 'Wyjaśnij funkcjonalność tego narzędzia i sposób użycia przez LLM', + parameters: 'Parametry', + parametersTip: 'Dodaj opisy każdego parametru, aby pomóc LLM zrozumieć ich cel i ograniczenia.', + parametersPlaceholder: 'Cel i ograniczenia parametru', + confirm: 'Aktywuj serwer MCP', + }, + publishTip: 'Aplikacja nieopublikowana. Najpierw opublikuj aplikację.', + }, + }, } export default translation diff --git a/web/i18n/pt-BR/plugin.ts b/web/i18n/pt-BR/plugin.ts index 8f6501ec93..be8e7e7f97 100644 --- a/web/i18n/pt-BR/plugin.ts +++ b/web/i18n/pt-BR/plugin.ts @@ -47,14 +47,14 @@ const translation = { toolSelector: { uninstalledLink: 'Gerenciar em plug-ins', unsupportedContent2: 'Clique para mudar de versão.', - auto: 'Automático', + auto: 'Auto', title: 'Adicionar ferramenta', params: 'CONFIGURAÇÃO DE RACIOCÍNIO', toolLabel: 'Ferramenta', paramsTip1: 'Controla os parâmetros de inferência do LLM.', descriptionLabel: 'Descrição da ferramenta', uninstalledContent: 'Este plug-in é instalado a partir do repositório local/GitHub. Por favor, use após a instalação.', - paramsTip2: 'Quando \'Automático\' está desativado, o valor padrão é usado.', + paramsTip2: 'Quando \'Auto\' está desativado, o valor padrão é usado.', placeholder: 'Selecione uma ferramenta...', empty: 'Clique no botão \'+\' para adicionar ferramentas. Você pode adicionar várias ferramentas.', settings: 'CONFIGURAÇÕES DO USUÁRIO', diff --git a/web/i18n/pt-BR/tools.ts b/web/i18n/pt-BR/tools.ts index dde7add80a..bd57de362f 100644 --- a/web/i18n/pt-BR/tools.ts +++ b/web/i18n/pt-BR/tools.ts @@ -139,19 +139,95 @@ const translation = { addToolModal: { category: 'categoria', type: 'tipo', - emptyTip: 'Vá para "Fluxo de trabalho - > Publicar como ferramenta"', add: 'adicionar', - emptyTitle: 'Nenhuma ferramenta de fluxo de trabalho disponível', added: 'Adicionado', manageInTools: 'Gerenciar em Ferramentas', - emptyTitleCustom: 'Nenhuma ferramenta personalizada disponível', - emptyTipCustom: 'Criar uma ferramenta personalizada', + custom: { + title: 'Nenhuma ferramenta personalizada disponível', + tip: 'Crie uma ferramenta personalizada', + }, + workflow: { + title: 'Nenhuma ferramenta de fluxo de trabalho disponível', + tip: 'Publique fluxos de trabalho como ferramentas no Studio', + }, + mcp: { + title: 'Nenhuma ferramenta MCP disponível', + tip: 'Adicionar um servidor MCP', + }, + agent: { + title: 'Nenhuma estratégia de agente disponível', + }, }, openInStudio: 'Abrir no Studio', customToolTip: 'Saiba mais sobre as ferramentas personalizadas da Dify', toolNameUsageTip: 'Nome da chamada da ferramenta para raciocínio e solicitação do agente', copyToolName: 'Nome da cópia', noTools: 'Nenhuma ferramenta encontrada', + mcp: { + create: { + cardTitle: 'Adicionar Servidor MCP (HTTP)', + cardLink: 'Saiba mais sobre a integração do servidor MCP', + }, + noConfigured: 'Servidor Não Configurado', + updateTime: 'Atualizado', + toolsCount: '{{count}} ferramentas', + noTools: 'Nenhuma ferramenta disponível', + modal: { + title: 'Adicionar Servidor MCP (HTTP)', + editTitle: 'Editar Servidor MCP (HTTP)', + name: 'Nome & Ícone', + namePlaceholder: 'Dê um nome ao seu servidor MCP', + serverUrl: 'URL do Servidor', + serverUrlPlaceholder: 'URL para o endpoint do servidor', + serverUrlWarning: 'Atualizar o endereço do servidor pode interromper aplicações que dependem deste servidor', + serverIdentifier: 'Identificador do Servidor', + serverIdentifierTip: 'Identificador único para o servidor MCP dentro do espaço de trabalho. Apenas letras minúsculas, números, sublinhados e hífens. Até 24 caracteres.', + serverIdentifierPlaceholder: 'Identificador único, ex: meu-servidor-mcp', + serverIdentifierWarning: 'O servidor não será reconhecido por aplicativos existentes após uma mudança de ID', + cancel: 'Cancelar', + save: 'Salvar', + confirm: 'Adicionar e Autorizar', + }, + delete: 'Remover Servidor MCP', + deleteConfirmTitle: 'Você gostaria de remover {{mcp}}?', + operation: { + edit: 'Editar', + remove: 'Remover', + }, + authorize: 'Autorizar', + authorizing: 'Autorizando...', + authorizingRequired: 'Autorização é necessária', + authorizeTip: 'Após a autorização, as ferramentas serão exibidas aqui.', + update: 'Atualizar', + updating: 'Atualizando', + gettingTools: 'Obtendo Ferramentas...', + updateTools: 'Atualizando Ferramentas...', + toolsEmpty: 'Ferramentas não carregadas', + getTools: 'Obter ferramentas', + toolUpdateConfirmTitle: 'Atualizar Lista de Ferramentas', + toolUpdateConfirmContent: 'Atualizar a lista de ferramentas pode afetar aplicativos existentes. Você deseja continuar?', + toolsNum: '{{count}} ferramentas incluídas', + onlyTool: '1 ferramenta incluída', + identifier: 'Identificador do Servidor (Clique para Copiar)', + server: { + title: 'Servidor MCP', + url: 'URL do Servidor', + reGen: 'Você deseja regenerar a URL do servidor?', + addDescription: 'Adicionar descrição', + edit: 'Editar descrição', + modal: { + addTitle: 'Adicionar descrição para habilitar o servidor MCP', + editTitle: 'Editar descrição', + description: 'Descrição', + descriptionPlaceholder: 'Explique o que esta ferramenta faz e como deve ser utilizada pelo LLM', + parameters: 'Parâmetros', + parametersTip: 'Adicione descrições para cada parâmetro para ajudar o LLM a entender seus propósitos e restrições.', + parametersPlaceholder: 'Propósito e restrições do parâmetro', + confirm: 'Habilitar Servidor MCP', + }, + publishTip: 'Aplicativo não publicado. Por favor, publique o aplicativo primeiro.', + }, + }, } export default translation diff --git a/web/i18n/ro-RO/plugin.ts b/web/i18n/ro-RO/plugin.ts index a88a841e51..1c7d173f8f 100644 --- a/web/i18n/ro-RO/plugin.ts +++ b/web/i18n/ro-RO/plugin.ts @@ -46,7 +46,7 @@ const translation = { }, toolSelector: { unsupportedContent: 'Versiunea de plugin instalată nu oferă această acțiune.', - auto: 'Automat', + auto: 'Auto', empty: 'Faceți clic pe butonul "+" pentru a adăuga instrumente. Puteți adăuga mai multe instrumente.', uninstalledContent: 'Acest plugin este instalat din depozitul local/GitHub. Vă rugăm să utilizați după instalare.', descriptionLabel: 'Descrierea instrumentului', @@ -54,7 +54,7 @@ const translation = { uninstalledLink: 'Gestionați în pluginuri', paramsTip1: 'Controlează parametrii de inferență LLM.', params: 'CONFIGURAREA RAȚIONAMENTULUI', - paramsTip2: 'Când "Automat" este dezactivat, se folosește valoarea implicită.', + paramsTip2: 'Când "Auto" este dezactivat, se folosește valoarea implicită.', settings: 'SETĂRI UTILIZATOR', unsupportedTitle: 'Acțiune neacceptată', placeholder: 'Selectați un instrument...', diff --git a/web/i18n/ro-RO/tools.ts b/web/i18n/ro-RO/tools.ts index 44530754e3..8d8c77a911 100644 --- a/web/i18n/ro-RO/tools.ts +++ b/web/i18n/ro-RO/tools.ts @@ -142,16 +142,92 @@ const translation = { manageInTools: 'Gestionați în Instrumente', add: 'adăuga', type: 'tip', - emptyTitle: 'Nu este disponibil niciun instrument de flux de lucru', - emptyTip: 'Accesați "Flux de lucru -> Publicați ca instrument"', - emptyTitleCustom: 'Nu este disponibil niciun instrument personalizat', - emptyTipCustom: 'Crearea unui instrument personalizat', + custom: { + title: 'Niciun instrument personalizat disponibil', + tip: 'Creează un instrument personalizat', + }, + workflow: { + title: 'Niciun instrument de flux de lucru disponibil', + tip: 'Publicați fluxuri de lucru ca instrumente în Studio', + }, + mcp: { + title: 'Niciun instrument MCP disponibil', + tip: 'Adăugați un server MCP', + }, + agent: { + title: 'Nicio strategie de agent disponibilă', + }, }, openInStudio: 'Deschide în Studio', customToolTip: 'Aflați mai multe despre instrumentele personalizate Dify', toolNameUsageTip: 'Numele de apel al instrumentului pentru raționamentul și solicitarea agentului', copyToolName: 'Copiază numele', noTools: 'Nu s-au găsit unelte', + mcp: { + create: { + cardTitle: 'Adăugare Server MCP (HTTP)', + cardLink: 'Aflați mai multe despre integrarea serverului MCP', + }, + noConfigured: 'Server Neconfigurat', + updateTime: 'Actualizat', + toolsCount: '{count} unelte', + noTools: 'Nu există unelte disponibile', + modal: { + title: 'Adăugare Server MCP (HTTP)', + editTitle: 'Editare Server MCP (HTTP)', + name: 'Nume și Pictogramă', + namePlaceholder: 'Denumiți-vă serverul MCP', + serverUrl: 'URL Server', + serverUrlPlaceholder: 'URL către endpoint-ul serverului', + serverUrlWarning: 'Actualizarea adresei serverului poate întrerupe aplicațiile care depind de acesta', + serverIdentifier: 'Identificator Server', + serverIdentifierTip: 'Identificator unic pentru serverul MCP în spațiul de lucru. Doar litere mici, cifre, underscore și cratime. Maxim 24 de caractere.', + serverIdentifierPlaceholder: 'Identificator unic, ex: my-mcp-server', + serverIdentifierWarning: 'Serverul nu va fi recunoscut de aplicațiile existente după schimbarea ID-ului', + cancel: 'Anulare', + save: 'Salvare', + confirm: 'Adăugare și Autorizare', + }, + delete: 'Eliminare Server MCP', + deleteConfirmTitle: 'Ștergeți {mcp}?', + operation: { + edit: 'Editare', + remove: 'Eliminare', + }, + authorize: 'Autorizare', + authorizing: 'Se autorizează...', + authorizingRequired: 'Autorizare necesară', + authorizeTip: 'După autorizare, uneltele vor fi afișate aici.', + update: 'Actualizare', + updating: 'Se actualizează...', + gettingTools: 'Se obțin unelte...', + updateTools: 'Se actualizează unelte...', + toolsEmpty: 'Unelte neîncărcate', + getTools: 'Obține unelte', + toolUpdateConfirmTitle: 'Actualizare Listă Unelte', + toolUpdateConfirmContent: 'Actualizarea listei de unelte poate afecta aplicațiile existente. Continuați?', + toolsNum: '{count} unelte incluse', + onlyTool: '1 unealtă inclusă', + identifier: 'Identificator Server (Clic pentru Copiere)', + server: { + title: 'Server MCP', + url: 'URL Server', + reGen: 'Regenerați URL server?', + addDescription: 'Adăugare descriere', + edit: 'Editare descriere', + modal: { + addTitle: 'Adăugați descriere pentru activarea serverului MCP', + editTitle: 'Editare descriere', + description: 'Descriere', + descriptionPlaceholder: 'Explicați funcționalitatea acestei unelte și cum ar trebui să fie utilizată de LLM', + parameters: 'Parametri', + parametersTip: 'Adăugați descrieri pentru fiecare parametru pentru a ajuta LLM să înțeleagă scopul și constrângerile.', + parametersPlaceholder: 'Scopul și constrângerile parametrului', + confirm: 'Activare Server MCP', + }, + publishTip: 'Aplicație nepublicată. Publicați aplicația mai întâi.', + }, + }, } export default translation diff --git a/web/i18n/ru-RU/tools.ts b/web/i18n/ru-RU/tools.ts index e1975ee538..caa1959318 100644 --- a/web/i18n/ru-RU/tools.ts +++ b/web/i18n/ru-RU/tools.ts @@ -28,10 +28,21 @@ const translation = { add: 'добавить', added: 'добавлено', manageInTools: 'Управлять в инструментах', - emptyTitle: 'Нет доступных инструментов рабочего процесса', - emptyTip: 'Перейдите в "Рабочий процесс -> Опубликовать как инструмент"', - emptyTitleCustom: 'Нет пользовательского инструмента', - emptyTipCustom: 'Создание пользовательского инструмента', + custom: { + title: 'Нет доступного пользовательского инструмента', + tip: 'Создать пользовательский инструмент', + }, + workflow: { + title: 'Нет доступного инструмента рабочего процесса', + tip: 'Публиковать рабочие процессы как инструменты в Студии', + }, + mcp: { + title: 'Нет доступного инструмента MCP', + tip: 'Добавить сервер MCP', + }, + agent: { + title: 'Нет доступной стратегии агента', + }, }, createTool: { title: 'Создать пользовательский инструмент', @@ -152,6 +163,71 @@ const translation = { toolNameUsageTip: 'Название вызова инструмента для рассуждений агента и подсказок', copyToolName: 'Копировать имя', noTools: 'Инструменты не найдены', + mcp: { + create: { + cardTitle: 'Добавить MCP сервер (HTTP)', + cardLink: 'Узнайте больше об интеграции MCP сервера', + }, + noConfigured: 'Неконфигурированный сервер', + updateTime: 'Обновлено', + toolsCount: '{count} инструментов', + noTools: 'Нет доступных инструментов', + modal: { + title: 'Добавить MCP сервер (HTTP)', + editTitle: 'Редактировать MCP сервер (HTTP)', + name: 'Имя и иконка', + namePlaceholder: 'Назовите ваш MCP сервер', + serverUrl: 'URL сервера', + serverUrlPlaceholder: 'URL конечной точки сервера', + serverUrlWarning: 'Обновление адреса сервера может нарушить работу приложений, которые зависят от этого сервера', + serverIdentifier: 'Идентификатор сервера', + serverIdentifierTip: 'Уникальный идентификатор MCP сервера в рабочем пространстве. Только строчные буквы, цифры, подчеркивания и дефисы. Максимум 24 символа.', + serverIdentifierPlaceholder: 'Уникальный идентификатор, например, мой-сервер-mcp', + serverIdentifierWarning: 'Сервер не будет распознан существующими приложениями после изменения ID', + cancel: 'Отмена', + save: 'Сохранить', + confirm: 'Добавить и авторизовать', + }, + delete: 'Удалить MCP сервер', + deleteConfirmTitle: 'Вы действительно хотите удалить {mcp}?', + operation: { + edit: 'Редактировать', + remove: 'Удалить', + }, + authorize: 'Авторизовать', + authorizing: 'Авторизация...', + authorizingRequired: 'Требуется авторизация', + authorizeTip: 'После авторизации инструменты будут отображены здесь.', + update: 'Обновить', + updating: 'Обновление', + gettingTools: 'Получение инструментов...', + updateTools: 'Обновление инструментов...', + toolsEmpty: 'Инструменты не загружены', + getTools: 'Получить инструменты', + toolUpdateConfirmTitle: 'Обновить список инструментов', + toolUpdateConfirmContent: 'Обновление списка инструментов может повлиять на существующие приложения. Вы хотите продолжить?', + toolsNum: '{count} инструментов включено', + onlyTool: '1 инструмент включен', + identifier: 'Идентификатор сервера (Нажмите, чтобы скопировать)', + server: { + title: 'MCP Сервер', + url: 'URL сервера', + reGen: 'Хотите регенерировать URL сервера?', + addDescription: 'Добавить описание', + edit: 'Редактировать описание', + modal: { + addTitle: 'Добавить описание, чтобы включить MCP сервер', + editTitle: 'Редактировать описание', + description: 'Описание', + descriptionPlaceholder: 'Объясните, что делает этот инструмент и как его должен использовать LLM', + parameters: 'Параметры', + parametersTip: 'Добавьте описания для каждого параметра, чтобы помочь LLM понять их назначение и ограничения.', + parametersPlaceholder: 'Назначение и ограничения параметра', + confirm: 'Активировать MCP сервер', + }, + publishTip: 'Приложение не опубликовано. Пожалуйста, сначала опубликуйте приложение.', + }, + }, } export default translation diff --git a/web/i18n/sl-SI/tools.ts b/web/i18n/sl-SI/tools.ts index e557725462..d83f218f68 100644 --- a/web/i18n/sl-SI/tools.ts +++ b/web/i18n/sl-SI/tools.ts @@ -28,10 +28,21 @@ const translation = { add: 'dodaj', added: 'dodano', manageInTools: 'Upravljaj v Orodjih', - emptyTitle: 'Orodje za potek dela ni na voljo', - emptyTip: 'Pojdite na "Potek dela -> Objavi kot orodje"', - emptyTipCustom: 'Ustvarjanje orodja po meri', - emptyTitleCustom: 'Orodje po meri ni na voljo', + custom: { + title: 'Žiadne prispôsobené nástroje nie sú k dispozícii', + tip: 'Vytvorte prispôsobený nástroj', + }, + workflow: { + title: 'Žiadny nástroj pracovného postupu nie je k dispozícii', + tip: 'Publikujte pracovné postupy ako nástroje v Studio', + }, + mcp: { + title: 'Žiadny nástroj MCP nie je k dispozícii', + tip: 'Pridajte server MCP', + }, + agent: { + title: 'Žiadna stratégia agenta nie je k dispozícii', + }, }, createTool: { title: 'Ustvari prilagojeno orodje', @@ -152,6 +163,71 @@ const translation = { toolNameUsageTip: 'Ime klica orodja za sklepanja in pozivanje agenta', copyToolName: 'Kopiraj ime', noTools: 'Orodja niso bila najdena', + mcp: { + create: { + cardTitle: 'Dodaj strežnik MCP (HTTP)', + cardLink: 'Več o integraciji strežnika MCP', + }, + noConfigured: 'Nekonfiguriran strežnik', + updateTime: 'Posodobljeno', + toolsCount: '{count} orodij', + noTools: 'Na voljo ni orodij', + modal: { + title: 'Dodaj strežnik MCP (HTTP)', + editTitle: 'Uredi strežnik MCP (HTTP)', + name: 'Ime in ikona', + namePlaceholder: 'Poimenuj svoj strežnik MCP', + serverUrl: 'URL strežnika', + serverUrlPlaceholder: 'URL do končne točke strežnika', + serverUrlWarning: 'Posodobitev naslova strežnika lahko prekine aplikacije, ki so odvisne od tega strežnika', + serverIdentifier: 'Identifikator strežnika', + serverIdentifierTip: 'Edinstven identifikator za strežnik MCP v delovnem prostoru. Samo male črke, številke, podčrtaji in vezaji. Največ 24 znakov.', + serverIdentifierPlaceholder: 'Edinstven identifikator, npr. moj-mcp-streznik', + serverIdentifierWarning: 'Strežnik po spremembi ID-ja ne bo prepoznan s strani obstoječih aplikacij', + cancel: 'Prekliči', + save: 'Shrani', + confirm: 'Dodaj in avtoriziraj', + }, + delete: 'Odstrani strežnik MCP', + deleteConfirmTitle: 'Odstraniti {mcp}?', + operation: { + edit: 'Uredi', + remove: 'Odstrani', + }, + authorize: 'Avtoriziraj', + authorizing: 'Avtoriziranje...', + authorizingRequired: 'Avtorizacija je zahtevana', + authorizeTip: 'Po avtorizaciji bodo orodja prikazana tukaj.', + update: 'Posodobi', + updating: 'Posodabljanje...', + gettingTools: 'Pridobivanje orodij...', + updateTools: 'Posodabljanje orodij...', + toolsEmpty: 'Orodja niso naložena', + getTools: 'Pridobi orodja', + toolUpdateConfirmTitle: 'Posodobi seznam orodij', + toolUpdateConfirmContent: 'Posodobitev seznama orodij lahko vpliva na obstoječe aplikacije. Želite nadaljevati?', + toolsNum: 'Vključenih {count} orodij', + onlyTool: 'Vključeno 1 orodje', + identifier: 'Identifikator strežnika (Kliknite za kopiranje)', + server: { + title: 'Strežnik MCP', + url: 'URL strežnika', + reGen: 'Želite ponovno ustvariti URL strežnika?', + addDescription: 'Dodaj opis', + edit: 'Uredi opis', + modal: { + addTitle: 'Dodajte opis za omogočitev strežnika MCP', + editTitle: 'Uredi opis', + description: 'Opis', + descriptionPlaceholder: 'Pojasnite, kaj to orodje počne in kako naj ga uporablja LLM', + parameters: 'Parametri', + parametersTip: 'Dodajte opise za vsak parameter, da pomagate LLM razumeti njihov namen in omejitve.', + parametersPlaceholder: 'Namen in omejitve parametra', + confirm: 'Omogoči strežnik MCP', + }, + publishTip: 'Aplikacija ni objavljena. Najprej objavite aplikacijo.', + }, + }, } export default translation diff --git a/web/i18n/th-TH/tools.ts b/web/i18n/th-TH/tools.ts index 14c9457c4e..df36463e57 100644 --- a/web/i18n/th-TH/tools.ts +++ b/web/i18n/th-TH/tools.ts @@ -28,10 +28,21 @@ const translation = { add: 'เพิ่ม', added: 'เพิ่ม', manageInTools: 'จัดการในเครื่องมือ', - emptyTitle: 'ไม่มีเครื่องมือเวิร์กโฟลว์', - emptyTip: 'ไปที่ "เวิร์กโฟลว์ -> เผยแพร่เป็นเครื่องมือ"', - emptyTitleCustom: 'ไม่มีเครื่องมือที่กําหนดเอง', - emptyTipCustom: 'สร้างเครื่องมือแบบกําหนดเอง', + custom: { + title: 'ไม่มีเครื่องมือกำหนดเอง', + tip: 'สร้างเครื่องมือกำหนดเอง', + }, + workflow: { + title: 'ไม่มีเครื่องมือเวิร์กโฟลว์', + tip: 'เผยแพร่เวิร์กโฟลว์เป็นเครื่องมือใน Studio', + }, + mcp: { + title: 'ไม่มีเครื่องมือ MCP', + tip: 'เพิ่มเซิร์ฟเวอร์ MCP', + }, + agent: { + title: 'ไม่มีกลยุทธ์เอเจนต์', + }, }, createTool: { title: 'สร้างเครื่องมือที่กําหนดเอง', @@ -152,6 +163,71 @@ const translation = { toolNameUsageTip: 'ชื่อการเรียกเครื่องมือสําหรับการใช้เหตุผลและการแจ้งเตือนของตัวแทน', noTools: 'ไม่พบเครื่องมือ', copyToolName: 'คัดลอกชื่อ', + mcp: { + create: { + cardTitle: 'เพิ่มเซิร์ฟเวอร์ MCP (HTTP)', + cardLink: 'เรียนรู้เพิ่มเติมเกี่ยวกับการรวมเซิร์ฟเวอร์ MCP', + }, + noConfigured: 'เซิร์ฟเวอร์ที่ยังไม่ได้กำหนดค่า', + updateTime: 'อัปเดตแล้ว', + toolsCount: '{count} เครื่องมือ', + noTools: 'ไม่มีเครื่องมือที่ใช้ได้', + modal: { + title: 'เพิ่มเซิร์ฟเวอร์ MCP (HTTP)', + editTitle: 'แก้ไขเซิร์ฟเวอร์ MCP (HTTP)', + name: 'ชื่อ & ไอคอน', + namePlaceholder: 'ตั้งชื่อเซิร์ฟเวอร์ MCP ของคุณ', + serverUrl: 'URL ของเซิร์ฟเวอร์', + serverUrlPlaceholder: 'URL สำหรับจุดสิ้นสุดของเซิร์ฟเวอร์', + serverUrlWarning: 'การอัปเดตที่อยู่เซิร์ฟเวอร์อาจทำให้แอปพลิเคชันที่พึ่งพาเซิร์ฟเวอร์นี้หยุดทำงาน', + serverIdentifier: 'ตัวระบุเซิร์ฟเวอร์', + serverIdentifierTip: 'ตัวระบุที่ไม่ซ้ำกันสำหรับเซิร์ฟเวอร์ MCP ภายในพื้นที่ทำงาน ตัวอักษรเล็ก ตัวเลข ขีดล่าง และขีดกลางเท่านั้น ความยาวไม่เกิน 24 ตัวอักษร', + serverIdentifierPlaceholder: 'ตัวระบุที่ไม่ซ้ำกัน เช่น my-mcp-server', + serverIdentifierWarning: 'เซิร์ฟเวอร์จะไม่ถูกต้องในแอปพลิเคชันที่มีอยู่หลังจากการเปลี่ยน ID', + cancel: 'ยกเลิก', + save: 'บันทึก', + confirm: 'เพิ่มและอนุญาต', + }, + delete: 'ลบเซิร์ฟเวอร์ MCP', + deleteConfirmTitle: 'คุณต้องการลบ {mcp} หรือไม่?', + operation: { + edit: 'แก้ไข', + remove: 'ลบ', + }, + authorize: 'อนุญาต', + authorizing: 'กำลังอนุญาต...', + authorizingRequired: 'ต้องมีการอนุญาต', + authorizeTip: 'หลังจากอนุญาต เครื่องมือจะถูกแสดงที่นี่', + update: 'อัปเดต', + updating: 'กำลังอัปเดต', + gettingTools: 'กำลังโหลดเครื่องมือ...', + updateTools: 'กำลังอัปเดตเครื่องมือ...', + toolsEmpty: 'ยังไม่โหลดเครื่องมือ', + getTools: 'รับเครื่องมือ', + toolUpdateConfirmTitle: 'อัปเดตรายการเครื่องมือ', + toolUpdateConfirmContent: 'การอัปเดตรายการเครื่องมืออาจส่งผลต่อแอปพลิเคชันที่มีอยู่ คุณต้องการดำเนินการต่อหรือไม่?', + toolsNum: '{count} เครื่องมือที่รวมอยู่', + onlyTool: 'รวม 1 เครื่องมือ', + identifier: 'ตัวระบุเซิร์ฟเวอร์ (คลิกเพื่อคัดลอก)', + server: { + title: 'เซิร์ฟเวอร์ MCP', + url: 'URL ของเซิร์ฟเวอร์', + reGen: 'คุณต้องการสร้าง URL ของเซิร์ฟเวอร์ใหม่หรือไม่?', + addDescription: 'เพิ่มคำอธิบาย', + edit: 'แก้ไขคำอธิบาย', + modal: { + addTitle: 'เพิ่มคำอธิบายเพื่อเปิดใช้งานเซิร์ฟเวอร์ MCP', + editTitle: 'แก้ไขคำอธิบาย', + description: 'คำอธิบาย', + descriptionPlaceholder: 'อธิบายว่าเครื่องมือนี้ทำอะไรและควรใช้กับ LLM อย่างไร', + parameters: 'พารามิเตอร์', + parametersTip: 'เพิ่มคำอธิบายสำหรับแต่ละพารามิเตอร์เพื่อช่วยให้ LLM เข้าใจวัตถุประสงค์และข้อจำกัดของมัน', + parametersPlaceholder: 'วัตถุประสงค์และข้อจำกัดของพารามิเตอร์', + confirm: 'เปิดใช้งานเซิร์ฟเวอร์ MCP', + }, + publishTip: 'แอปไม่ถูกเผยแพร่ กรุณาเผยแพร่แอปก่อน', + }, + }, } export default translation diff --git a/web/i18n/tr-TR/tools.ts b/web/i18n/tr-TR/tools.ts index af9ddf182f..6e641165e2 100644 --- a/web/i18n/tr-TR/tools.ts +++ b/web/i18n/tr-TR/tools.ts @@ -28,10 +28,21 @@ const translation = { add: 'Ekle', added: 'Eklendi', manageInTools: 'Araçlarda Yönet', - emptyTitle: 'Kullanılabilir workflow aracı yok', - emptyTip: 'Git "Workflow -> Araç olarak Yayınla"', - emptyTitleCustom: 'Özel bir araç yok', - emptyTipCustom: 'Özel bir araç oluşturun', + custom: { + title: 'Mevcut özel araç yok', + tip: 'Özel bir araç oluşturun', + }, + workflow: { + title: 'Mevcut iş akışı aracı yok', + tip: 'İş akışlarını Studio\'da araç olarak yayınlayın', + }, + mcp: { + title: 'Mevcut MCP aracı yok', + tip: 'Bir MCP sunucusu ekleyin', + }, + agent: { + title: 'Mevcut ajan stratejisi yok', + }, }, createTool: { title: 'Özel Araç Oluştur', @@ -152,6 +163,71 @@ const translation = { toolNameUsageTip: 'Agent akıl yürütme ve prompt için araç çağrı adı', copyToolName: 'Adı Kopyala', noTools: 'Araç bulunamadı', + mcp: { + create: { + cardTitle: 'MCP Sunucusu Ekle (HTTP)', + cardLink: 'MCP sunucu entegrasyonu hakkında daha fazla bilgi edinin', + }, + noConfigured: 'Yapılandırılmamış Sunucu', + updateTime: 'Güncellendi', + toolsCount: '{count} araç', + noTools: 'Kullanılabilir araç yok', + modal: { + title: 'MCP Sunucusu Ekle (HTTP)', + editTitle: 'MCP Sunucusunu Düzenle (HTTP)', + name: 'Ad ve Simge', + namePlaceholder: 'MCP sunucunuza ad verin', + serverUrl: 'Sunucu URL', + serverUrlPlaceholder: 'Sunucu endpoint URL', + serverUrlWarning: 'Sunucu adresini güncellemek, bu sunucuya bağımlı uygulamaları kesintiye uğratabilir', + serverIdentifier: 'Sunucu Tanımlayıcı', + serverIdentifierTip: 'Çalışma alanındaki MCP sunucusu için benzersiz tanımlayıcı. Sadece küçük harf, rakam, alt çizgi ve tire. En fazla 24 karakter.', + serverIdentifierPlaceholder: 'Benzersiz tanımlayıcı, örn. my-mcp-server', + serverIdentifierWarning: 'ID değiştirildikten sonra sunucu mevcut uygulamalar tarafından tanınmayacak', + cancel: 'İptal', + save: 'Kaydet', + confirm: 'Ekle ve Yetkilendir', + }, + delete: 'MCP Sunucusunu Kaldır', + deleteConfirmTitle: '{mcp} kaldırılsın mı?', + operation: { + edit: 'Düzenle', + remove: 'Kaldır', + }, + authorize: 'Yetkilendir', + authorizing: 'Yetkilendiriliyor...', + authorizingRequired: 'Yetkilendirme gerekli', + authorizeTip: 'Yetkilendirmeden sonra araçlar burada görüntülenecektir.', + update: 'Güncelle', + updating: 'Güncelleniyor...', + gettingTools: 'Araçlar alınıyor...', + updateTools: 'Araçlar güncelleniyor...', + toolsEmpty: 'Araçlar yüklenmedi', + getTools: 'Araçları al', + toolUpdateConfirmTitle: 'Araç Listesini Güncelle', + toolUpdateConfirmContent: 'Araç listesini güncellemek mevcut uygulamaları etkileyebilir. Devam etmek istiyor musunuz?', + toolsNum: '{count} araç dahil', + onlyTool: '1 araç dahil', + identifier: 'Sunucu Tanımlayıcı (Kopyalamak için Tıklayın)', + server: { + title: 'MCP Sunucusu', + url: 'Sunucu URL', + reGen: 'Sunucu URL yeniden oluşturulsun mu?', + addDescription: 'Açıklama ekle', + edit: 'Açıklamayı düzenle', + modal: { + addTitle: 'MCP Sunucusunu etkinleştirmek için açıklama ekleyin', + editTitle: 'Açıklamayı düzenle', + description: 'Açıklama', + descriptionPlaceholder: 'Bu aracın ne yaptığını ve LLM tarafından nasıl kullanılması gerektiğini açıklayın', + parameters: 'Parametreler', + parametersTip: 'LLM\'nin amaçlarını ve kısıtlamalarını anlamasına yardımcı olmak için her parametreye açıklamalar ekleyin.', + parametersPlaceholder: 'Parametre amacı ve kısıtlamaları', + confirm: 'MCP Sunucusunu Etkinleştir', + }, + publishTip: 'Uygulama yayınlanmadı. Lütfen önce uygulamayı yayınlayın.', + }, + }, } export default translation diff --git a/web/i18n/uk-UA/tools.ts b/web/i18n/uk-UA/tools.ts index d390b500d3..535c17b1ef 100644 --- a/web/i18n/uk-UA/tools.ts +++ b/web/i18n/uk-UA/tools.ts @@ -142,16 +142,92 @@ const translation = { added: 'Додано', type: 'тип', manageInTools: 'Керування в інструментах', - emptyTip: 'Перейдіть до розділу "Робочий процес -> Опублікувати як інструмент"', - emptyTitle: 'Немає доступного інструменту для роботи з робочими процесами', - emptyTitleCustom: 'Немає доступного спеціального інструменту', - emptyTipCustom: 'Створення власного інструмента', + custom: { + title: 'Немає доступного користувацького інструмента', + tip: 'Створити користувацький інструмент', + }, + workflow: { + title: 'Немає доступного інструмента робочого процесу', + tip: 'Опублікуйте робочі процеси як інструменти в Studio', + }, + mcp: { + title: 'Немає доступного інструмента MCP', + tip: 'Додати сервер MCP', + }, + agent: { + title: 'Немає доступної стратегії агента', + }, }, openInStudio: 'Відкрити в Студії', customToolTip: 'Дізнайтеся більше про користувацькі інструменти Dify', toolNameUsageTip: 'Ім\'я виклику інструменту для міркувань і підказок агента', copyToolName: 'Ім\'я копії', noTools: 'Інструментів не знайдено', + mcp: { + create: { + cardTitle: 'Додати сервер MCP (HTTP)', + cardLink: 'Дізнатися більше про інтеграцію сервера MCP', + }, + noConfigured: 'Сервер не налаштовано', + updateTime: 'Оновлено', + toolsCount: '{count} інструментів', + noTools: 'Інструменти відсутні', + modal: { + title: 'Додати сервер MCP (HTTP)', + editTitle: 'Редагувати сервер MCP (HTTP)', + name: 'Назва та значок', + namePlaceholder: 'Назвіть ваш сервер MCP', + serverUrl: 'URL сервера', + serverUrlPlaceholder: 'URL кінцевої точки сервера', + serverUrlWarning: 'Оновлення адреси сервера може порушити роботу додатків, що залежать від нього', + serverIdentifier: 'Ідентифікатор сервера', + serverIdentifierTip: 'Унікальний ідентифікатор сервера MCP у робочому просторі. Лише малі літери, цифри, підкреслення та дефіси. До 24 символів.', + serverIdentifierPlaceholder: 'Унікальний ідентифікатор, напр. my-mcp-server', + serverIdentifierWarning: 'Після зміни ID існуючі додатки не зможуть розпізнати сервер', + cancel: 'Скасувати', + save: 'Зберегти', + confirm: 'Додати та Авторизувати', + }, + delete: 'Видалити сервер MCP', + deleteConfirmTitle: 'Видалити {mcp}?', + operation: { + edit: 'Редагувати', + remove: 'Видалити', + }, + authorize: 'Авторизувати', + authorizing: 'Авторизація...', + authorizingRequired: 'Потрібна авторизація', + authorizeTip: 'Після авторизації інструменти відображатимуться тут.', + update: 'Оновити', + updating: 'Оновлення...', + gettingTools: 'Отримання інструментів...', + updateTools: 'Оновлення інструментів...', + toolsEmpty: 'Інструменти не завантажено', + getTools: 'Отримати інструменти', + toolUpdateConfirmTitle: 'Оновити список інструментів', + toolUpdateConfirmContent: 'Оновлення списку інструментів може вплинути на існуючі додатки. Продовжити?', + toolsNum: '{count} інструментів включено', + onlyTool: '1 інструмент включено', + identifier: 'Ідентифікатор сервера (Натисніть, щоб скопіювати)', + server: { + title: 'Сервер MCP', + url: 'URL сервера', + reGen: 'Згенерувати URL сервера знову?', + addDescription: 'Додати опис', + edit: 'Редагувати опис', + modal: { + addTitle: 'Додайте опис для активації сервера MCP', + editTitle: 'Редагувати опис', + description: 'Опис', + descriptionPlaceholder: 'Поясніть функціонал інструменту та його використання LLM', + parameters: 'Параметри', + parametersTip: 'Додайте описи параметрів, щоб допомогти LLM зрозуміти їх призначення та обмеження.', + parametersPlaceholder: 'Призначення та обмеження параметра', + confirm: 'Активувати сервер MCP', + }, + publishTip: 'Додаток не опубліковано. Спочатку опублікуйте додаток.', + }, + }, } export default translation diff --git a/web/i18n/vi-VN/tools.ts b/web/i18n/vi-VN/tools.ts index ec4665cbf5..4f3893cade 100644 --- a/web/i18n/vi-VN/tools.ts +++ b/web/i18n/vi-VN/tools.ts @@ -142,16 +142,92 @@ const translation = { type: 'kiểu', add: 'thêm', added: 'Thêm', - emptyTip: 'Đi tới "Quy trình làm việc -> Xuất bản dưới dạng công cụ"', - emptyTitle: 'Không có sẵn công cụ quy trình làm việc', - emptyTitleCustom: 'Không có công cụ tùy chỉnh nào có sẵn', - emptyTipCustom: 'Tạo công cụ tùy chỉnh', + custom: { + title: 'Không có công cụ tùy chỉnh nào', + tip: 'Tạo một công cụ tùy chỉnh', + }, + workflow: { + title: 'Không có công cụ quy trình nào', + tip: 'Xuất bản các quy trình dưới dạng công cụ trong Studio', + }, + mcp: { + title: 'Không có công cụ MCP nào', + tip: 'Thêm máy chủ MCP', + }, + agent: { + title: 'Không có chiến lược đại lý nào', + }, }, toolNameUsageTip: 'Tên cuộc gọi công cụ để lý luận và nhắc nhở tổng đài viên', customToolTip: 'Tìm hiểu thêm về các công cụ tùy chỉnh Dify', openInStudio: 'Mở trong Studio', noTools: 'Không tìm thấy công cụ', copyToolName: 'Sao chép tên', + mcp: { + create: { + cardTitle: 'Thêm Máy chủ MCP (HTTP)', + cardLink: 'Tìm hiểu thêm về tích hợp máy chủ MCP', + }, + noConfigured: 'Máy chủ Chưa được Cấu hình', + updateTime: 'Cập nhật', + toolsCount: '{count} công cụ', + noTools: 'Không có công cụ nào', + modal: { + title: 'Thêm Máy chủ MCP (HTTP)', + editTitle: 'Sửa Máy chủ MCP (HTTP)', + name: 'Tên & Biểu tượng', + namePlaceholder: 'Đặt tên máy chủ MCP', + serverUrl: 'URL Máy chủ', + serverUrlPlaceholder: 'URL đến điểm cuối máy chủ', + serverUrlWarning: 'Cập nhật địa chỉ máy chủ có thể làm gián đoạn ứng dụng phụ thuộc vào máy chủ này', + serverIdentifier: 'Định danh Máy chủ', + serverIdentifierTip: 'Định danh duy nhất cho máy chủ MCP trong không gian làm việc. Chỉ chữ thường, số, gạch dưới và gạch ngang. Tối đa 24 ký tự.', + serverIdentifierPlaceholder: 'Định danh duy nhất, VD: my-mcp-server', + serverIdentifierWarning: 'Máy chủ sẽ không được nhận diện bởi ứng dụng hiện có sau khi thay đổi ID', + cancel: 'Hủy', + save: 'Lưu', + confirm: 'Thêm & Ủy quyền', + }, + delete: 'Xóa Máy chủ MCP', + deleteConfirmTitle: 'Xóa {mcp}?', + operation: { + edit: 'Sửa', + remove: 'Xóa', + }, + authorize: 'Ủy quyền', + authorizing: 'Đang ủy quyền...', + authorizingRequired: 'Cần ủy quyền', + authorizeTip: 'Sau khi ủy quyền, công cụ sẽ hiển thị tại đây.', + update: 'Cập nhật', + updating: 'Đang cập nhật...', + gettingTools: 'Đang lấy công cụ...', + updateTools: 'Đang cập nhật công cụ...', + toolsEmpty: 'Công cụ chưa tải', + getTools: 'Lấy công cụ', + toolUpdateConfirmTitle: 'Cập nhật Danh sách Công cụ', + toolUpdateConfirmContent: 'Cập nhật danh sách công cụ có thể ảnh hưởng ứng dụng hiện có. Tiếp tục?', + toolsNum: 'Bao gồm {count} công cụ', + onlyTool: 'Bao gồm 1 công cụ', + identifier: 'Định danh Máy chủ (Nhấn để Sao chép)', + server: { + title: 'Máy chủ MCP', + url: 'URL Máy chủ', + reGen: 'Tạo lại URL máy chủ?', + addDescription: 'Thêm mô tả', + edit: 'Sửa mô tả', + modal: { + addTitle: 'Thêm mô tả để kích hoạt máy chủ MCP', + editTitle: 'Sửa mô tả', + description: 'Mô tả', + descriptionPlaceholder: 'Giải thích chức năng công cụ và cách LLM sử dụng', + parameters: 'Tham số', + parametersTip: 'Thêm mô tả cho từng tham số để giúp LLM hiểu mục đích và ràng buộc.', + parametersPlaceholder: 'Mục đích và ràng buộc của tham số', + confirm: 'Kích hoạt Máy chủ MCP', + }, + publishTip: 'Ứng dụng chưa xuất bản. Vui lòng xuất bản ứng dụng trước.', + }, + }, } export default translation diff --git a/web/i18n/zh-Hans/plugin.ts b/web/i18n/zh-Hans/plugin.ts index eddd117012..89cdddc1e3 100644 --- a/web/i18n/zh-Hans/plugin.ts +++ b/web/i18n/zh-Hans/plugin.ts @@ -94,6 +94,7 @@ const translation = { unsupportedTitle: '不支持的 Action', unsupportedContent: '已安装的插件版本不提供这个 action。', unsupportedContent2: '点击切换版本', + unsupportedMCPTool: '当前选定的 Agent 策略插件版本不支持 MCP 工具。', }, configureApp: '应用设置', configureModel: '模型设置', diff --git a/web/i18n/zh-Hans/tools.ts b/web/i18n/zh-Hans/tools.ts index 9a573ad308..4e0ccf476f 100644 --- a/web/i18n/zh-Hans/tools.ts +++ b/web/i18n/zh-Hans/tools.ts @@ -28,10 +28,21 @@ const translation = { add: '添加', added: '已添加', manageInTools: '去工具列表管理', - emptyTitle: '没有可用的工作流工具', - emptyTip: '去“工作流 -> 发布为工具”添加', - emptyTitleCustom: '没有可用的自定义工具', - emptyTipCustom: '创建自定义工具', + custom: { + title: '没有可用的自定义工具', + tip: '创建自定义工具', + }, + workflow: { + title: '没有可用的工作流工具', + tip: '在工作室中发布工作流作为工具', + }, + mcp: { + title: '没有可用的 MCP 工具', + tip: '添加 MCP 服务器', + }, + agent: { + title: '没有可用的 agent 策略', + }, }, createTool: { title: '创建自定义工具', @@ -152,6 +163,71 @@ const translation = { toolNameUsageTip: '工具调用名称,用于 Agent 推理和提示词', copyToolName: '复制名称', noTools: '没有工具', + mcp: { + create: { + cardTitle: '添加 MCP 服务 (HTTP)', + cardLink: '了解更多关于 MCP 服务集成的信息', + }, + noConfigured: '未配置', + updateTime: '更新于', + toolsCount: '{{count}} 个工具', + noTools: '没有可用的工具', + modal: { + title: '添加 MCP 服务 (HTTP)', + editTitle: '修改 MCP 服务 (HTTP)', + name: '名称和图标', + namePlaceholder: '命名你的 MCP 服务', + serverUrl: '服务端点 URL', + serverUrlPlaceholder: '服务端点的 URL', + serverUrlWarning: '修改服务端点 URL 可能会影响使用当前 MCP 的应用。', + serverIdentifier: '服务器标识符', + serverIdentifierTip: '工作空间内服务器的唯一标识。支持小写字母、数字、下划线和连字符,最多 24 个字符。', + serverIdentifierPlaceholder: '服务器唯一标识,例如 my-mcp-server', + serverIdentifierWarning: '更改服务器标识符后,现有应用将无法识别此服务器', + cancel: '取消', + save: '保存', + confirm: '添加并授权', + }, + delete: '删除 MCP 服务', + deleteConfirmTitle: '你想要删除 {{mcp}} 吗?', + operation: { + edit: '修改', + remove: '删除', + }, + authorize: '授权', + authorizing: '授权中...', + authorizingRequired: '需要授权', + authorizeTip: '授权后,工具将显示在这里。', + update: '更新', + updating: '更新中', + gettingTools: '获取工具中...', + updateTools: '更新工具中...', + toolsEmpty: '工具未加载', + getTools: '获取工具', + toolUpdateConfirmTitle: '更新工具列表', + toolUpdateConfirmContent: '更新工具列表可能影响现有应用。您想继续吗?', + toolsNum: '包含 {{count}} 个工具', + onlyTool: '包含 1 个工具', + identifier: '服务器标识符 (点击复制)', + server: { + title: 'MCP 服务', + url: '服务端点 URL', + reGen: '你想要重新生成服务端点 URL 吗?', + addDescription: '添加描述', + edit: '编辑描述', + modal: { + addTitle: '添加描述以启用 MCP 服务', + editTitle: '编辑 MCP 服务描述', + description: '描述', + descriptionPlaceholder: '解释此工具的功能以及 LLM 应如何使用它', + parameters: '参数', + parametersTip: '为每个参数添加描述,以帮助 LLM 理解其目的和约束条件。', + parametersPlaceholder: '参数的用途和约束条件', + confirm: '启用 MCP 服务', + }, + publishTip: '应用未发布。请先发布应用。', + }, + }, } export default translation diff --git a/web/i18n/zh-Hans/workflow.ts b/web/i18n/zh-Hans/workflow.ts index 28a5f8aa95..6bd202d58f 100644 --- a/web/i18n/zh-Hans/workflow.ts +++ b/web/i18n/zh-Hans/workflow.ts @@ -232,6 +232,8 @@ const translation = { 'utilities': '工具', 'noResult': '未找到匹配项', 'agent': 'Agent 策略', + 'allAdded': '已添加全部', + 'addAll': '添加全部', }, blocks: { 'start': '开始', @@ -369,6 +371,10 @@ const translation = { ms: '毫秒', retries: '{{num}} 重试次数', }, + typeSwitch: { + input: '输入值', + variable: '使用变量', + }, }, start: { required: '必填', @@ -663,6 +669,9 @@ const translation = { tool: { authorize: '授权', inputVars: '输入变量', + settings: '设置', + insertPlaceholder1: '键入', + insertPlaceholder2: '插入变量', outputVars: { text: '工具生成的内容', files: { @@ -890,6 +899,8 @@ const translation = { install: '安装', cancel: '取消', }, + clickToViewParameterSchema: '点击查看参数 schema', + parameterSchema: '参数 Schema', }, }, tracing: { diff --git a/web/i18n/zh-Hant/tools.ts b/web/i18n/zh-Hant/tools.ts index 6e5a95f2a5..93c3fda5c4 100644 --- a/web/i18n/zh-Hant/tools.ts +++ b/web/i18n/zh-Hant/tools.ts @@ -142,16 +142,92 @@ const translation = { added: '添加', manageInTools: '在工具中管理', category: '類別', - emptyTitle: '沒有可用的工作流程工具', - emptyTip: '轉到“工作流 - >發佈為工具”', - emptyTipCustom: '創建自訂工具', - emptyTitleCustom: '沒有可用的自訂工具', + custom: { + title: '沒有可用的自訂工具', + tip: '創建一個自訂工具', + }, + workflow: { + title: '沒有可用的工作流程工具', + tip: '在 Studio 中將工作流程發佈為工具', + }, + mcp: { + title: '沒有可用的 MCP 工具', + tip: '新增一個 MCP 伺服器', + }, + agent: { + title: '沒有可用的代理策略', + }, }, customToolTip: '瞭解有關 Dify 自訂工具的更多資訊', toolNameUsageTip: '用於代理推理和提示的工具調用名稱', openInStudio: '在 Studio 中打開', noTools: '未找到工具', copyToolName: '複製名稱', + mcp: { + create: { + cardTitle: '新增 MCP 伺服器 (HTTP)', + cardLink: '了解更多關於 MCP 伺服器整合', + }, + noConfigured: '未配置的伺服器', + updateTime: '已更新', + toolsCount: '{{count}} 個工具', + noTools: '沒有可用的工具', + modal: { + title: '新增 MCP 伺服器 (HTTP)', + editTitle: '編輯 MCP 伺服器 (HTTP)', + name: '名稱與圖示', + namePlaceholder: '為您的 MCP 伺服器命名', + serverUrl: '伺服器 URL', + serverUrlPlaceholder: '伺服器端點的 URL', + serverUrlWarning: '更新伺服器地址可能會干擾依賴於此伺服器的應用程式', + serverIdentifier: '伺服器識別碼', + serverIdentifierTip: '在工作區內 MCP 伺服器的唯一識別碼。僅限小寫字母、數字、底線和連字符。最多 24 個字元。', + serverIdentifierPlaceholder: '唯一識別碼,例如:my-mcp-server', + serverIdentifierWarning: '更改 ID 之後,現有應用程式將無法識別伺服器', + cancel: '取消', + save: '儲存', + confirm: '新增並授權', + }, + delete: '刪除 MCP 伺服器', + deleteConfirmTitle: '您確定要刪除 {{mcp}} 嗎?', + operation: { + edit: '編輯', + remove: '移除', + }, + authorize: '授權', + authorizing: '正在授權...', + authorizingRequired: '需要授權', + authorizeTip: '授權後,這裡將顯示工具。', + update: '更新', + updating: '更新中', + gettingTools: '獲取工具...', + updateTools: '更新工具...', + toolsEmpty: '工具未加載', + getTools: '獲取工具', + toolUpdateConfirmTitle: '更新工具列表', + toolUpdateConfirmContent: '更新工具列表可能會影響現有應用程式。您要繼續嗎?', + toolsNum: '{{count}} 個工具包含', + onlyTool: '包含 1 個工具', + identifier: '伺服器識別碼 (點擊複製)', + server: { + title: 'MCP 伺服器', + url: '伺服器 URL', + reGen: '您想要重新生成伺服器 URL 嗎?', + addDescription: '新增描述', + edit: '編輯描述', + modal: { + addTitle: '新增描述以啟用 MCP 伺服器', + editTitle: '編輯描述', + description: '描述', + descriptionPlaceholder: '說明此工具的用途及如何被 LLM 使用', + parameters: '參數', + parametersTip: '為每個參數添加描述,以幫助 LLM 理解其目的和約束。', + parametersPlaceholder: '參數的目的和約束', + confirm: '啟用 MCP 伺服器', + }, + publishTip: '應用程式尚未發布。請先發布應用程式。', + }, + }, } export default translation diff --git a/web/package.json b/web/package.json index 0862ddbb07..254c2ec1fd 100644 --- a/web/package.json +++ b/web/package.json @@ -144,6 +144,7 @@ "sortablejs": "^1.15.0", "swr": "^2.3.0", "tailwind-merge": "^2.5.4", + "tldts": "^7.0.9", "use-context-selector": "^2.0.0", "uuid": "^10.0.0", "zod": "^3.23.8", diff --git a/web/pnpm-lock.yaml b/web/pnpm-lock.yaml index ef945dfc54..a69dea9088 100644 --- a/web/pnpm-lock.yaml +++ b/web/pnpm-lock.yaml @@ -333,6 +333,9 @@ importers: tailwind-merge: specifier: ^2.5.4 version: 2.6.0 + tldts: + specifier: ^7.0.9 + version: 7.0.9 use-context-selector: specifier: ^2.0.0 version: 2.0.0(react@19.0.0)(scheduler@0.23.2) @@ -8042,6 +8045,13 @@ packages: resolution: {integrity: sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==} engines: {node: '>=14.0.0'} + tldts-core@7.0.9: + resolution: {integrity: sha512-/FGY1+CryHsxF9SFiPZlMOcwQsfABkAvOJO5VEKE8TNifVEqgMF7+UVXHGhm1z4gPUfvVS/EYcwhiRU3vUa1ag==} + + tldts@7.0.9: + resolution: {integrity: sha512-/nFtBeNs9nAKIAZE1i3ssOAroci8UqRldFVw5H6RCsNZw7NzDr+Yc3Ek7Tm8XSQKMzw7NSyRSszNxCM0ENsUbg==} + hasBin: true + tmpl@1.0.5: resolution: {integrity: sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==} @@ -18054,6 +18064,12 @@ snapshots: tinyspy@3.0.2: {} + tldts-core@7.0.9: {} + + tldts@7.0.9: + dependencies: + tldts-core: 7.0.9 + tmpl@1.0.5: {} to-buffer@1.2.1: diff --git a/web/service/common.ts b/web/service/common.ts index 700cd4bf51..e071d556d1 100644 --- a/web/service/common.ts +++ b/web/service/common.ts @@ -337,8 +337,8 @@ export const verifyWebAppForgotPasswordToken: Fetcher = ({ url, body }) => post(url, { body }, { isPublicAPI: true }) -export const uploadRemoteFileInfo = (url: string, isPublic?: boolean) => { - return post<{ id: string; name: string; size: number; mime_type: string; url: string }>('/remote-files/upload', { body: { url } }, { isPublicAPI: isPublic }) +export const uploadRemoteFileInfo = (url: string, isPublic?: boolean, silent?: boolean) => { + return post<{ id: string; name: string; size: number; mime_type: string; url: string }>('/remote-files/upload', { body: { url } }, { isPublicAPI: isPublic, silent }) } export const sendEMailLoginCode = (email: string, language = 'en-US') => diff --git a/web/service/tools.ts b/web/service/tools.ts index 38dcf382e6..6a88d8d567 100644 --- a/web/service/tools.ts +++ b/web/service/tools.ts @@ -124,6 +124,10 @@ export const fetchAllWorkflowTools = () => { return get('/workspaces/current/tools/workflow') } +export const fetchAllMCPTools = () => { + return get('/workspaces/current/tools/mcp') +} + export const fetchLabelList = () => { return get('/workspaces/current/tool-labels') } diff --git a/web/service/use-tools.ts b/web/service/use-tools.ts index ceaa4b14b3..64a3ce7a1f 100644 --- a/web/service/use-tools.ts +++ b/web/service/use-tools.ts @@ -1,9 +1,11 @@ -import { get, post } from './base' +import { del, get, post, put } from './base' import type { Collection, + MCPServerDetail, Tool, } from '@/app/components/tools/types' import type { ToolWithProvider } from '@/app/components/workflow/types' +import type { AppIconType } from '@/types/app' import { useInvalid } from './use-base' import { useMutation, @@ -61,6 +63,191 @@ export const useInvalidateAllWorkflowTools = () => { return useInvalid(useAllWorkflowToolsKey) } +const useAllMCPToolsKey = [NAME_SPACE, 'MCPTools'] +export const useAllMCPTools = () => { + return useQuery({ + queryKey: useAllMCPToolsKey, + queryFn: () => get('/workspaces/current/tools/mcp'), + }) +} + +export const useInvalidateAllMCPTools = () => { + return useInvalid(useAllMCPToolsKey) +} + +export const useCreateMCP = () => { + return useMutation({ + mutationKey: [NAME_SPACE, 'create-mcp'], + mutationFn: (payload: { + name: string + server_url: string + icon_type: AppIconType + icon: string + icon_background?: string | null + }) => { + return post('workspaces/current/tool-provider/mcp', { + body: { + ...payload, + }, + }) + }, + }) +} + +export const useUpdateMCP = ({ + onSuccess, +}: { + onSuccess?: () => void +}) => { + return useMutation({ + mutationKey: [NAME_SPACE, 'update-mcp'], + mutationFn: (payload: { + name: string + server_url: string + icon_type: AppIconType + icon: string + icon_background?: string | null + provider_id: string + }) => { + return put('workspaces/current/tool-provider/mcp', { + body: { + ...payload, + }, + }) + }, + onSuccess, + }) +} + +export const useDeleteMCP = ({ + onSuccess, +}: { + onSuccess?: () => void +}) => { + return useMutation({ + mutationKey: [NAME_SPACE, 'delete-mcp'], + mutationFn: (id: string) => { + return del('/workspaces/current/tool-provider/mcp', { + body: { + provider_id: id, + }, + }) + }, + onSuccess, + }) +} + +export const useAuthorizeMCP = () => { + return useMutation({ + mutationKey: [NAME_SPACE, 'authorize-mcp'], + mutationFn: (payload: { provider_id: string; }) => { + return post<{ result?: string; authorization_url?: string }>('/workspaces/current/tool-provider/mcp/auth', { + body: payload, + }) + }, + }) +} + +export const useUpdateMCPAuthorizationToken = () => { + return useMutation({ + mutationKey: [NAME_SPACE, 'refresh-mcp-server-code'], + mutationFn: (payload: { provider_id: string; authorization_code: string }) => { + return get('/workspaces/current/tool-provider/mcp/token', { + params: { + ...payload, + }, + }) + }, + }) +} + +export const useMCPTools = (providerID: string) => { + return useQuery({ + enabled: !!providerID, + queryKey: [NAME_SPACE, 'get-MCP-provider-tool', providerID], + queryFn: () => get<{ tools: Tool[] }>(`/workspaces/current/tool-provider/mcp/tools/${providerID}`), + }) +} +export const useInvalidateMCPTools = () => { + const queryClient = useQueryClient() + return (providerID: string) => { + queryClient.invalidateQueries( + { + queryKey: [NAME_SPACE, 'get-MCP-provider-tool', providerID], + }) + } +} + +export const useUpdateMCPTools = () => { + return useMutation({ + mutationFn: (providerID: string) => get<{ tools: Tool[] }>(`/workspaces/current/tool-provider/mcp/update/${providerID}`), + }) +} + +export const useMCPServerDetail = (appID: string) => { + return useQuery({ + queryKey: [NAME_SPACE, 'MCPServerDetail', appID], + queryFn: () => get(`/apps/${appID}/server`), + }) +} + +export const useInvalidateMCPServerDetail = () => { + const queryClient = useQueryClient() + return (appID: string) => { + queryClient.invalidateQueries( + { + queryKey: [NAME_SPACE, 'MCPServerDetail', appID], + }) + } +} + +export const useCreateMCPServer = () => { + return useMutation({ + mutationKey: [NAME_SPACE, 'create-mcp-server'], + mutationFn: (payload: { + appID: string + description: string + parameters?: Record + }) => { + const { appID, ...rest } = payload + return post(`apps/${appID}/server`, { + body: { + ...rest, + }, + }) + }, + }) +} + +export const useUpdateMCPServer = () => { + return useMutation({ + mutationKey: [NAME_SPACE, 'update-mcp-server'], + mutationFn: (payload: { + appID: string + id: string + description?: string + status?: string + parameters?: Record + }) => { + const { appID, ...rest } = payload + return put(`apps/${appID}/server`, { + body: { + ...rest, + }, + }) + }, + }) +} + +export const useRefreshMCPServerCode = () => { + return useMutation({ + mutationKey: [NAME_SPACE, 'refresh-mcp-server-code'], + mutationFn: (appID: string) => { + return get(`apps/${appID}/server/refresh`) + }, + }) +} + export const useBuiltinProviderInfo = (providerName: string) => { return useQuery({ queryKey: [NAME_SPACE, 'builtin-provider-info', providerName], diff --git a/web/service/use-workflow.ts b/web/service/use-workflow.ts index eb07109857..2c8f86ae5d 100644 --- a/web/service/use-workflow.ts +++ b/web/service/use-workflow.ts @@ -1,5 +1,5 @@ import { del, get, patch, post, put } from './base' -import { useInfiniteQuery, useMutation, useQuery } from '@tanstack/react-query' +import { useInfiniteQuery, useMutation, useQuery, useQueryClient } from '@tanstack/react-query' import type { FetchWorkflowDraftPageParams, FetchWorkflowDraftPageResponse, @@ -23,6 +23,16 @@ export const useAppWorkflow = (appID: string) => { }) } +export const useInvalidateAppWorkflow = () => { + const queryClient = useQueryClient() + return (appID: string) => { + queryClient.invalidateQueries( + { + queryKey: [NAME_SPACE, 'publish', appID], + }) + } +} + export const useWorkflowConfig = (appId: string, onSuccess: (v: WorkflowConfigResponse) => void) => { return useQuery({ queryKey: [NAME_SPACE, 'config', appId], diff --git a/web/tailwind-common-config.ts b/web/tailwind-common-config.ts index 3f64afcc29..eff1530017 100644 --- a/web/tailwind-common-config.ts +++ b/web/tailwind-common-config.ts @@ -71,6 +71,7 @@ const config = { boxShadow: { 'xs': '0px 1px 2px 0px rgba(16, 24, 40, 0.05)', 'sm': '0px 1px 2px 0px rgba(16, 24, 40, 0.06), 0px 1px 3px 0px rgba(16, 24, 40, 0.10)', + 'sm-no-bottom': '0px -1px 2px 0px rgba(16, 24, 40, 0.06), 0px -1px 3px 0px rgba(16, 24, 40, 0.10)', 'md': '0px 2px 4px -2px rgba(16, 24, 40, 0.06), 0px 4px 8px -2px rgba(16, 24, 40, 0.10)', 'lg': '0px 4px 6px -2px rgba(16, 24, 40, 0.03), 0px 12px 16px -4px rgba(16, 24, 40, 0.08)', 'xl': '0px 8px 8px -4px rgba(16, 24, 40, 0.03), 0px 20px 24px -4px rgba(16, 24, 40, 0.08)', diff --git a/web/utils/plugin-version-feature.spec.ts b/web/utils/plugin-version-feature.spec.ts new file mode 100644 index 0000000000..12ca239aa9 --- /dev/null +++ b/web/utils/plugin-version-feature.spec.ts @@ -0,0 +1,26 @@ +import { isSupportMCP } from './plugin-version-feature' + +describe('plugin-version-feature', () => { + beforeEach(() => { + jest.clearAllMocks() + }) + + describe('isSupportMCP', () => { + it('should call isEqualOrLaterThanVersion with the correct parameters', () => { + expect(isSupportMCP('0.0.3')).toBe(true) + expect(isSupportMCP('1.0.0')).toBe(true) + }) + + it('should return true when version is equal to the supported MCP version', () => { + const mockVersion = '0.0.2' + const result = isSupportMCP(mockVersion) + expect(result).toBe(true) + }) + + it('should return false when version is less than the supported MCP version', () => { + const mockVersion = '0.0.1' + const result = isSupportMCP(mockVersion) + expect(result).toBe(false) + }) + }) +}) diff --git a/web/utils/plugin-version-feature.ts b/web/utils/plugin-version-feature.ts new file mode 100644 index 0000000000..51d366bf9c --- /dev/null +++ b/web/utils/plugin-version-feature.ts @@ -0,0 +1,10 @@ +import { isEqualOrLaterThanVersion } from './semver' + +const SUPPORT_MCP_VERSION = '0.0.2' + +export const isSupportMCP = (version?: string): boolean => { + if (!version) + return false + + return isEqualOrLaterThanVersion(version, SUPPORT_MCP_VERSION) +} diff --git a/web/utils/semver.spec.ts b/web/utils/semver.spec.ts new file mode 100644 index 0000000000..c2188a976c --- /dev/null +++ b/web/utils/semver.spec.ts @@ -0,0 +1,75 @@ +import { compareVersion, getLatestVersion, isEqualOrLaterThanVersion } from './semver' + +describe('semver utilities', () => { + describe('getLatestVersion', () => { + it('should return the latest version from a list of versions', () => { + expect(getLatestVersion(['1.0.0', '1.1.0', '1.0.1'])).toBe('1.1.0') + expect(getLatestVersion(['2.0.0', '1.9.9', '1.10.0'])).toBe('2.0.0') + expect(getLatestVersion(['1.0.0-alpha', '1.0.0-beta', '1.0.0'])).toBe('1.0.0') + }) + + it('should handle patch versions correctly', () => { + expect(getLatestVersion(['1.0.1', '1.0.2', '1.0.0'])).toBe('1.0.2') + expect(getLatestVersion(['1.0.10', '1.0.9', '1.0.11'])).toBe('1.0.11') + }) + + it('should handle mixed version formats', () => { + expect(getLatestVersion(['v1.0.0', '1.1.0', 'v1.2.0'])).toBe('v1.2.0') + expect(getLatestVersion(['1.0.0-rc.1', '1.0.0', '1.0.0-beta'])).toBe('1.0.0') + }) + + it('should return the only version if only one version is provided', () => { + expect(getLatestVersion(['1.0.0'])).toBe('1.0.0') + }) + }) + + describe('compareVersion', () => { + it('should return 1 when first version is greater', () => { + expect(compareVersion('1.1.0', '1.0.0')).toBe(1) + expect(compareVersion('2.0.0', '1.9.9')).toBe(1) + expect(compareVersion('1.0.1', '1.0.0')).toBe(1) + }) + + it('should return -1 when first version is less', () => { + expect(compareVersion('1.0.0', '1.1.0')).toBe(-1) + expect(compareVersion('1.9.9', '2.0.0')).toBe(-1) + expect(compareVersion('1.0.0', '1.0.1')).toBe(-1) + }) + + it('should return 0 when versions are equal', () => { + expect(compareVersion('1.0.0', '1.0.0')).toBe(0) + expect(compareVersion('2.1.3', '2.1.3')).toBe(0) + }) + + it('should handle pre-release versions correctly', () => { + expect(compareVersion('1.0.0-beta', '1.0.0-alpha')).toBe(1) + expect(compareVersion('1.0.0', '1.0.0-beta')).toBe(1) + expect(compareVersion('1.0.0-alpha', '1.0.0-beta')).toBe(-1) + }) + }) + + describe('isEqualOrLaterThanVersion', () => { + it('should return true when baseVersion is greater than targetVersion', () => { + expect(isEqualOrLaterThanVersion('1.1.0', '1.0.0')).toBe(true) + expect(isEqualOrLaterThanVersion('2.0.0', '1.9.9')).toBe(true) + expect(isEqualOrLaterThanVersion('1.0.1', '1.0.0')).toBe(true) + }) + + it('should return true when baseVersion is equal to targetVersion', () => { + expect(isEqualOrLaterThanVersion('1.0.0', '1.0.0')).toBe(true) + expect(isEqualOrLaterThanVersion('2.1.3', '2.1.3')).toBe(true) + }) + + it('should return false when baseVersion is less than targetVersion', () => { + expect(isEqualOrLaterThanVersion('1.0.0', '1.1.0')).toBe(false) + expect(isEqualOrLaterThanVersion('1.9.9', '2.0.0')).toBe(false) + expect(isEqualOrLaterThanVersion('1.0.0', '1.0.1')).toBe(false) + }) + + it('should handle pre-release versions correctly', () => { + expect(isEqualOrLaterThanVersion('1.0.0', '1.0.0-beta')).toBe(true) + expect(isEqualOrLaterThanVersion('1.0.0-beta', '1.0.0-alpha')).toBe(true) + expect(isEqualOrLaterThanVersion('1.0.0-alpha', '1.0.0')).toBe(false) + }) + }) +}) diff --git a/web/utils/semver.ts b/web/utils/semver.ts index f1b9eb8d7e..aea84153ec 100644 --- a/web/utils/semver.ts +++ b/web/utils/semver.ts @@ -7,3 +7,7 @@ export const getLatestVersion = (versionList: string[]) => { export const compareVersion = (v1: string, v2: string) => { return semver.compare(v1, v2) } + +export const isEqualOrLaterThanVersion = (baseVersion: string, targetVersion: string) => { + return semver.gte(baseVersion, targetVersion) +} From b834131f50a86ef03f3e96b9bc8fe4640cd3c172 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 10 Jul 2025 14:19:26 +0800 Subject: [PATCH 272/329] chore: translate i18n files (#22132) Co-authored-by: iamjoel <2120155+iamjoel@users.noreply.github.com> --- web/i18n/de-DE/app.ts | 1 + web/i18n/de-DE/workflow.ts | 1 + web/i18n/es-ES/app.ts | 1 + web/i18n/es-ES/workflow.ts | 1 + web/i18n/fa-IR/app.ts | 1 + web/i18n/fa-IR/workflow.ts | 1 + web/i18n/fr-FR/app.ts | 1 + web/i18n/fr-FR/workflow.ts | 1 + web/i18n/hi-IN/app.ts | 1 + web/i18n/hi-IN/workflow.ts | 1 + web/i18n/it-IT/app.ts | 1 + web/i18n/it-IT/workflow.ts | 1 + web/i18n/ja-JP/app.ts | 1 + web/i18n/ja-JP/tools.ts | 1 - web/i18n/ja-JP/workflow.ts | 1 + web/i18n/ko-KR/app.ts | 1 + web/i18n/ko-KR/workflow.ts | 1 + web/i18n/pl-PL/app.ts | 1 + web/i18n/pl-PL/workflow.ts | 1 + web/i18n/pt-BR/app.ts | 1 + web/i18n/pt-BR/workflow.ts | 1 + web/i18n/ro-RO/app.ts | 3 +++ web/i18n/ro-RO/workflow.ts | 1 + web/i18n/ru-RU/app.ts | 1 + web/i18n/ru-RU/workflow.ts | 1 + web/i18n/sl-SI/app.ts | 1 + web/i18n/sl-SI/workflow.ts | 1 + web/i18n/th-TH/app.ts | 1 + web/i18n/th-TH/workflow.ts | 1 + web/i18n/tr-TR/app.ts | 1 + web/i18n/tr-TR/workflow.ts | 1 + web/i18n/uk-UA/app.ts | 1 + web/i18n/uk-UA/workflow.ts | 1 + web/i18n/vi-VN/app.ts | 1 + web/i18n/vi-VN/workflow.ts | 1 + web/i18n/zh-Hant/app.ts | 1 + web/i18n/zh-Hant/workflow.ts | 1 + 37 files changed, 38 insertions(+), 1 deletion(-) diff --git a/web/i18n/de-DE/app.ts b/web/i18n/de-DE/app.ts index d29a475bcd..52819d0c7e 100644 --- a/web/i18n/de-DE/app.ts +++ b/web/i18n/de-DE/app.ts @@ -174,6 +174,7 @@ const translation = { title: 'Weben', description: 'Weave ist eine Open-Source-Plattform zur Bewertung, Testung und Überwachung von LLM-Anwendungen.', }, + aliyun: {}, }, answerIcon: { descriptionInExplore: 'Gibt an, ob das web app Symbol zum Ersetzen 🤖 in Explore verwendet werden soll', diff --git a/web/i18n/de-DE/workflow.ts b/web/i18n/de-DE/workflow.ts index f7f757f91b..ba49f72b69 100644 --- a/web/i18n/de-DE/workflow.ts +++ b/web/i18n/de-DE/workflow.ts @@ -364,6 +364,7 @@ const translation = { ms: 'Frau', retries: '{{num}} Wiederholungen', }, + typeSwitch: {}, }, start: { required: 'erforderlich', diff --git a/web/i18n/es-ES/app.ts b/web/i18n/es-ES/app.ts index 18741226fa..4c9497e16d 100644 --- a/web/i18n/es-ES/app.ts +++ b/web/i18n/es-ES/app.ts @@ -172,6 +172,7 @@ const translation = { description: 'Weave es una plataforma de código abierto para evaluar, probar y monitorear aplicaciones de LLM.', title: 'Tejer', }, + aliyun: {}, }, answerIcon: { title: 'Usar el icono de la aplicación web para reemplazar 🤖', diff --git a/web/i18n/es-ES/workflow.ts b/web/i18n/es-ES/workflow.ts index 519577150d..44516317e8 100644 --- a/web/i18n/es-ES/workflow.ts +++ b/web/i18n/es-ES/workflow.ts @@ -364,6 +364,7 @@ const translation = { retries: '{{num}} Reintentos', retry: 'Reintentar', }, + typeSwitch: {}, }, start: { required: 'requerido', diff --git a/web/i18n/fa-IR/app.ts b/web/i18n/fa-IR/app.ts index 5e9bd938f2..bf2fa00c11 100644 --- a/web/i18n/fa-IR/app.ts +++ b/web/i18n/fa-IR/app.ts @@ -176,6 +176,7 @@ const translation = { title: 'بافندگی', description: 'ویو یک پلتفرم متن باز برای ارزیابی، آزمایش و نظارت بر برنامه‌های LLM است.', }, + aliyun: {}, }, answerIcon: { descriptionInExplore: 'آیا از نماد web app برای جایگزینی 🤖 در Explore استفاده کنیم یا خیر', diff --git a/web/i18n/fa-IR/workflow.ts b/web/i18n/fa-IR/workflow.ts index 94903c2bd0..800dba06b8 100644 --- a/web/i18n/fa-IR/workflow.ts +++ b/web/i18n/fa-IR/workflow.ts @@ -364,6 +364,7 @@ const translation = { retrySuccessful: 'امتحان مجدد با موفقیت انجام دهید', retryFailedTimes: '{{بار}} تلاش های مجدد ناموفق بود', }, + typeSwitch: {}, }, start: { required: 'الزامی', diff --git a/web/i18n/fr-FR/app.ts b/web/i18n/fr-FR/app.ts index 29d7a9b3de..18cd04a1e1 100644 --- a/web/i18n/fr-FR/app.ts +++ b/web/i18n/fr-FR/app.ts @@ -172,6 +172,7 @@ const translation = { title: 'Tisser', description: 'Weave est une plateforme open-source pour évaluer, tester et surveiller les applications LLM.', }, + aliyun: {}, }, answerIcon: { description: 'S’il faut utiliser l’icône web app pour remplacer 🤖 dans l’application partagée', diff --git a/web/i18n/fr-FR/workflow.ts b/web/i18n/fr-FR/workflow.ts index a008a2a735..8c8180abff 100644 --- a/web/i18n/fr-FR/workflow.ts +++ b/web/i18n/fr-FR/workflow.ts @@ -364,6 +364,7 @@ const translation = { ms: 'ms', retries: '{{num}} Tentatives', }, + typeSwitch: {}, }, start: { required: 'requis', diff --git a/web/i18n/hi-IN/app.ts b/web/i18n/hi-IN/app.ts index 9485d7359b..e9073722f7 100644 --- a/web/i18n/hi-IN/app.ts +++ b/web/i18n/hi-IN/app.ts @@ -172,6 +172,7 @@ const translation = { title: 'बुनना', description: 'वीव एक ओपन-सोर्स प्लेटफ़ॉर्म है जो LLM अनुप्रयोगों का मूल्यांकन, परीक्षण और निगरानी करने के लिए है।', }, + aliyun: {}, }, answerIcon: { title: 'बदलने 🤖 के लिए web app चिह्न का उपयोग करें', diff --git a/web/i18n/hi-IN/workflow.ts b/web/i18n/hi-IN/workflow.ts index 34293d95ba..ffddacaf3a 100644 --- a/web/i18n/hi-IN/workflow.ts +++ b/web/i18n/hi-IN/workflow.ts @@ -376,6 +376,7 @@ const translation = { retry: 'पुनर्प्रयास', retryOnFailure: 'विफलता पर पुनः प्रयास करें', }, + typeSwitch: {}, }, start: { required: 'आवश्यक', diff --git a/web/i18n/it-IT/app.ts b/web/i18n/it-IT/app.ts index feade00168..a08714df71 100644 --- a/web/i18n/it-IT/app.ts +++ b/web/i18n/it-IT/app.ts @@ -184,6 +184,7 @@ const translation = { title: 'Intrecciare', description: 'Weave è una piattaforma open-source per valutare, testare e monitorare le applicazioni LLM.', }, + aliyun: {}, }, answerIcon: { description: 'Se utilizzare l\'icona web app per la sostituzione 🤖 nell\'applicazione condivisa', diff --git a/web/i18n/it-IT/workflow.ts b/web/i18n/it-IT/workflow.ts index 0f1837e084..d7e85dcc19 100644 --- a/web/i18n/it-IT/workflow.ts +++ b/web/i18n/it-IT/workflow.ts @@ -379,6 +379,7 @@ const translation = { retryFailed: 'Nuovo tentativo non riuscito', ms: 'ms', }, + typeSwitch: {}, }, start: { required: 'richiesto', diff --git a/web/i18n/ja-JP/app.ts b/web/i18n/ja-JP/app.ts index 1d94d67eb0..2058b29d9e 100644 --- a/web/i18n/ja-JP/app.ts +++ b/web/i18n/ja-JP/app.ts @@ -180,6 +180,7 @@ const translation = { title: '織る', description: 'Weave は、LLM アプリケーションを評価、テスト、および監視するためのオープンソースプラットフォームです。', }, + aliyun: {}, }, answerIcon: { title: 'Web アプリアイコンを使用して🤖を置き換える', diff --git a/web/i18n/ja-JP/tools.ts b/web/i18n/ja-JP/tools.ts index d69cd4a6f5..f96a5f4182 100644 --- a/web/i18n/ja-JP/tools.ts +++ b/web/i18n/ja-JP/tools.ts @@ -228,7 +228,6 @@ const translation = { publishTip: 'アプリが公開されていません。まずアプリを公開してください。', }, }, - } export default translation diff --git a/web/i18n/ja-JP/workflow.ts b/web/i18n/ja-JP/workflow.ts index e4b31548bf..04702194f8 100644 --- a/web/i18n/ja-JP/workflow.ts +++ b/web/i18n/ja-JP/workflow.ts @@ -369,6 +369,7 @@ const translation = { ms: 'ミリ秒', retries: '再試行回数:{{num}}', }, + typeSwitch: {}, }, start: { required: '必須', diff --git a/web/i18n/ko-KR/app.ts b/web/i18n/ko-KR/app.ts index e9634bfbde..96407f829b 100644 --- a/web/i18n/ko-KR/app.ts +++ b/web/i18n/ko-KR/app.ts @@ -192,6 +192,7 @@ const translation = { description: 'Weave 는 LLM 애플리케이션을 평가하고 테스트하며 모니터링하기 위한 오픈 소스 플랫폼입니다.', }, + aliyun: {}, }, answerIcon: { description: diff --git a/web/i18n/ko-KR/workflow.ts b/web/i18n/ko-KR/workflow.ts index 1270408c6c..6a9f97862e 100644 --- a/web/i18n/ko-KR/workflow.ts +++ b/web/i18n/ko-KR/workflow.ts @@ -388,6 +388,7 @@ const translation = { ms: '미에스', retries: '{{숫자}} 재시도', }, + typeSwitch: {}, }, start: { required: '필수', diff --git a/web/i18n/pl-PL/app.ts b/web/i18n/pl-PL/app.ts index ee422a8b56..141ab190e0 100644 --- a/web/i18n/pl-PL/app.ts +++ b/web/i18n/pl-PL/app.ts @@ -179,6 +179,7 @@ const translation = { title: 'Tkaj', description: 'Weave to platforma open-source do oceny, testowania i monitorowania aplikacji LLM.', }, + aliyun: {}, }, answerIcon: { description: 'Czy w aplikacji udostępnionej ma być używana ikona aplikacji internetowej do zamiany 🤖.', diff --git a/web/i18n/pl-PL/workflow.ts b/web/i18n/pl-PL/workflow.ts index 07c5fa1f92..6d1c9ccc8c 100644 --- a/web/i18n/pl-PL/workflow.ts +++ b/web/i18n/pl-PL/workflow.ts @@ -364,6 +364,7 @@ const translation = { retryFailedTimes: '{{times}} ponawianie prób nie powiodło się', ms: 'Ms', }, + typeSwitch: {}, }, start: { required: 'wymagane', diff --git a/web/i18n/pt-BR/app.ts b/web/i18n/pt-BR/app.ts index 742159692c..099d0a33ea 100644 --- a/web/i18n/pt-BR/app.ts +++ b/web/i18n/pt-BR/app.ts @@ -172,6 +172,7 @@ const translation = { description: 'Weave é uma plataforma de código aberto para avaliar, testar e monitorar aplicações de LLM.', title: 'Trançar', }, + aliyun: {}, }, answerIcon: { descriptionInExplore: 'Se o ícone do web app deve ser usado para substituir 🤖 no Explore', diff --git a/web/i18n/pt-BR/workflow.ts b/web/i18n/pt-BR/workflow.ts index 85b05ef06f..1cb323f59a 100644 --- a/web/i18n/pt-BR/workflow.ts +++ b/web/i18n/pt-BR/workflow.ts @@ -364,6 +364,7 @@ const translation = { ms: 'ms', retries: '{{num}} Tentativas', }, + typeSwitch: {}, }, start: { required: 'requerido', diff --git a/web/i18n/ro-RO/app.ts b/web/i18n/ro-RO/app.ts index 5fdb32945c..2d63bdfba7 100644 --- a/web/i18n/ro-RO/app.ts +++ b/web/i18n/ro-RO/app.ts @@ -172,6 +172,9 @@ const translation = { title: 'Împletește', description: 'Weave este o platformă open-source pentru evaluarea, testarea și monitorizarea aplicațiilor LLM.', }, + aliyun: { + description: 'Platforma de observabilitate SaaS oferită de Alibaba Cloud permite monitorizarea, urmărirea și evaluarea aplicațiilor Dify din cutie.', + }, }, answerIcon: { descriptionInExplore: 'Dacă să utilizați pictograma web app pentru a înlocui 🤖 în Explore', diff --git a/web/i18n/ro-RO/workflow.ts b/web/i18n/ro-RO/workflow.ts index 6942c1eea5..886dc3b790 100644 --- a/web/i18n/ro-RO/workflow.ts +++ b/web/i18n/ro-RO/workflow.ts @@ -364,6 +364,7 @@ const translation = { retries: '{{num}} Încercări', retryTimes: 'Reîncercați {{times}} ori în caz de eșec', }, + typeSwitch: {}, }, start: { required: 'necesar', diff --git a/web/i18n/ru-RU/app.ts b/web/i18n/ru-RU/app.ts index 03283180dc..de8047b080 100644 --- a/web/i18n/ru-RU/app.ts +++ b/web/i18n/ru-RU/app.ts @@ -176,6 +176,7 @@ const translation = { description: 'Weave — это открытая платформа для оценки, тестирования и мониторинга приложений LLM.', title: 'Ткать', }, + aliyun: {}, }, answerIcon: { title: 'Использование значка web app для замены 🤖', diff --git a/web/i18n/ru-RU/workflow.ts b/web/i18n/ru-RU/workflow.ts index dfdbac3ce9..aecd9e652c 100644 --- a/web/i18n/ru-RU/workflow.ts +++ b/web/i18n/ru-RU/workflow.ts @@ -364,6 +364,7 @@ const translation = { retryFailedTimes: 'Повторные попытки {{times}} не увенчались успехом', retries: '{{число}} Повторных попыток', }, + typeSwitch: {}, }, start: { required: 'обязательно', diff --git a/web/i18n/sl-SI/app.ts b/web/i18n/sl-SI/app.ts index bba09351aa..152b7d63dc 100644 --- a/web/i18n/sl-SI/app.ts +++ b/web/i18n/sl-SI/app.ts @@ -181,6 +181,7 @@ const translation = { title: 'Tkanje', description: 'Weave je odprtokodna platforma za vrednotenje, testiranje in spremljanje aplikacij LLM.', }, + aliyun: {}, }, mermaid: { handDrawn: 'Ročno narisano', diff --git a/web/i18n/sl-SI/workflow.ts b/web/i18n/sl-SI/workflow.ts index 16e3ae72cb..a7c2626264 100644 --- a/web/i18n/sl-SI/workflow.ts +++ b/web/i18n/sl-SI/workflow.ts @@ -366,6 +366,7 @@ const translation = { }, insertVarTip: 'Vstavite spremenljivko', outputVars: 'Izhodne spremenljivke', + typeSwitch: {}, }, start: { outputVars: { diff --git a/web/i18n/th-TH/app.ts b/web/i18n/th-TH/app.ts index 3d4809e874..9d2b9af398 100644 --- a/web/i18n/th-TH/app.ts +++ b/web/i18n/th-TH/app.ts @@ -177,6 +177,7 @@ const translation = { title: 'ทอ', description: 'Weave เป็นแพลตฟอร์มโอเพนซอร์สสำหรับการประเมินผล ทดสอบ และตรวจสอบแอปพลิเคชัน LLM', }, + aliyun: {}, }, mermaid: { handDrawn: 'วาดด้วยมือ', diff --git a/web/i18n/th-TH/workflow.ts b/web/i18n/th-TH/workflow.ts index 8d99c1c514..28be5c57e8 100644 --- a/web/i18n/th-TH/workflow.ts +++ b/web/i18n/th-TH/workflow.ts @@ -364,6 +364,7 @@ const translation = { retries: '{{num}} ลอง', ms: 'นางสาว', }, + typeSwitch: {}, }, start: { required: 'ต้องระบุ', diff --git a/web/i18n/tr-TR/app.ts b/web/i18n/tr-TR/app.ts index 639319fd81..16bad22231 100644 --- a/web/i18n/tr-TR/app.ts +++ b/web/i18n/tr-TR/app.ts @@ -172,6 +172,7 @@ const translation = { title: 'Dokuma', description: 'Weave, LLM uygulamalarını değerlendirmek, test etmek ve izlemek için açık kaynaklı bir platformdur.', }, + aliyun: {}, }, answerIcon: { descriptionInExplore: 'Keşfet\'te değiştirilecek 🤖 web app simgesinin kullanılıp kullanılmayacağı', diff --git a/web/i18n/tr-TR/workflow.ts b/web/i18n/tr-TR/workflow.ts index 63eed4bb9c..a09e5d9068 100644 --- a/web/i18n/tr-TR/workflow.ts +++ b/web/i18n/tr-TR/workflow.ts @@ -364,6 +364,7 @@ const translation = { retrying: 'Yeniden deneniyor...', ms: 'Ms', }, + typeSwitch: {}, }, start: { required: 'gerekli', diff --git a/web/i18n/uk-UA/app.ts b/web/i18n/uk-UA/app.ts index 9e1d3c60c6..9786fd36db 100644 --- a/web/i18n/uk-UA/app.ts +++ b/web/i18n/uk-UA/app.ts @@ -172,6 +172,7 @@ const translation = { title: 'Ткати', description: 'Weave є платформою з відкритим кодом для оцінки, тестування та моніторингу LLM додатків.', }, + aliyun: {}, }, answerIcon: { title: 'Використовуйте піктограму web app для заміни 🤖', diff --git a/web/i18n/uk-UA/workflow.ts b/web/i18n/uk-UA/workflow.ts index a6a04246c2..dd61582129 100644 --- a/web/i18n/uk-UA/workflow.ts +++ b/web/i18n/uk-UA/workflow.ts @@ -364,6 +364,7 @@ const translation = { retryFailedTimes: '{{times}} повторні спроби не вдалися', retryTimes: 'Повторіть спробу {{times}} у разі невдачі', }, + typeSwitch: {}, }, start: { required: 'обов\'язковий', diff --git a/web/i18n/vi-VN/app.ts b/web/i18n/vi-VN/app.ts index 0968fcb458..d8f80d9df0 100644 --- a/web/i18n/vi-VN/app.ts +++ b/web/i18n/vi-VN/app.ts @@ -172,6 +172,7 @@ const translation = { title: 'Dệt', description: 'Weave là một nền tảng mã nguồn mở để đánh giá, thử nghiệm và giám sát các ứng dụng LLM.', }, + aliyun: {}, }, answerIcon: { description: 'Có nên sử dụng biểu tượng web app để thay thế 🤖 trong ứng dụng được chia sẻ hay không', diff --git a/web/i18n/vi-VN/workflow.ts b/web/i18n/vi-VN/workflow.ts index 47d8c13f96..05cd279728 100644 --- a/web/i18n/vi-VN/workflow.ts +++ b/web/i18n/vi-VN/workflow.ts @@ -364,6 +364,7 @@ const translation = { times: 'lần', ms: 'Ms', }, + typeSwitch: {}, }, start: { required: 'bắt buộc', diff --git a/web/i18n/zh-Hant/app.ts b/web/i18n/zh-Hant/app.ts index 41cd6324c9..e5f997daff 100644 --- a/web/i18n/zh-Hant/app.ts +++ b/web/i18n/zh-Hant/app.ts @@ -171,6 +171,7 @@ const translation = { title: '編織', description: 'Weave 是一個開源平台,用於評估、測試和監控大型語言模型應用程序。', }, + aliyun: {}, }, answerIcon: { descriptionInExplore: '是否使用 web app 圖示在 Explore 中取代 🤖', diff --git a/web/i18n/zh-Hant/workflow.ts b/web/i18n/zh-Hant/workflow.ts index 3368aee1b7..1d29d2f5ab 100644 --- a/web/i18n/zh-Hant/workflow.ts +++ b/web/i18n/zh-Hant/workflow.ts @@ -364,6 +364,7 @@ const translation = { ms: '毫秒', retries: '{{num}}重試', }, + typeSwitch: {}, }, start: { required: '必填', From edf5fd28c9279cb3b53d65591b671aba497582b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B9=9B=E9=9C=B2=E5=85=88=E7=94=9F?= Date: Thu, 10 Jul 2025 14:21:34 +0800 Subject: [PATCH 273/329] update worklow events logs. (#19871) Signed-off-by: zhanluxianshen --- api/core/workflow/callbacks/workflow_logging_callback.py | 6 +++--- api/core/workflow/graph_engine/entities/graph.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/api/core/workflow/callbacks/workflow_logging_callback.py b/api/core/workflow/callbacks/workflow_logging_callback.py index e6813a3997..12b5203ca3 100644 --- a/api/core/workflow/callbacks/workflow_logging_callback.py +++ b/api/core/workflow/callbacks/workflow_logging_callback.py @@ -232,14 +232,14 @@ class WorkflowLoggingCallback(WorkflowCallback): Publish loop started """ self.print_text("\n[LoopRunStartedEvent]", color="blue") - self.print_text(f"Loop Node ID: {event.loop_id}", color="blue") + self.print_text(f"Loop Node ID: {event.loop_node_id}", color="blue") def on_workflow_loop_next(self, event: LoopRunNextEvent) -> None: """ Publish loop next """ self.print_text("\n[LoopRunNextEvent]", color="blue") - self.print_text(f"Loop Node ID: {event.loop_id}", color="blue") + self.print_text(f"Loop Node ID: {event.loop_node_id}", color="blue") self.print_text(f"Loop Index: {event.index}", color="blue") def on_workflow_loop_completed(self, event: LoopRunSucceededEvent | LoopRunFailedEvent) -> None: @@ -250,7 +250,7 @@ class WorkflowLoggingCallback(WorkflowCallback): "\n[LoopRunSucceededEvent]" if isinstance(event, LoopRunSucceededEvent) else "\n[LoopRunFailedEvent]", color="blue", ) - self.print_text(f"Node ID: {event.loop_id}", color="blue") + self.print_text(f"Loop Node ID: {event.loop_node_id}", color="blue") def print_text(self, text: str, color: Optional[str] = None, end: str = "\n") -> None: """Print text with highlighting and no end characters.""" diff --git a/api/core/workflow/graph_engine/entities/graph.py b/api/core/workflow/graph_engine/entities/graph.py index 8e5b1e7142..362777a199 100644 --- a/api/core/workflow/graph_engine/entities/graph.py +++ b/api/core/workflow/graph_engine/entities/graph.py @@ -334,7 +334,7 @@ class Graph(BaseModel): parallel = GraphParallel( start_from_node_id=start_node_id, - parent_parallel_id=parent_parallel.id if parent_parallel else None, + parent_parallel_id=parent_parallel_id, parent_parallel_start_node_id=parent_parallel.start_from_node_id if parent_parallel else None, ) parallel_mapping[parallel.id] = parallel From 94a13d7d62067f427d2419a5b8541c2236d95a40 Mon Sep 17 00:00:00 2001 From: Yeuoly <45712896+Yeuoly@users.noreply.github.com> Date: Thu, 10 Jul 2025 14:43:31 +0800 Subject: [PATCH 274/329] feat: add support for dark icons in provider and tool entities (#22081) --- api/core/model_runtime/entities/provider_entities.py | 2 ++ api/core/plugin/entities/plugin.py | 1 + api/core/tools/entities/api_entities.py | 2 ++ api/core/tools/entities/tool_entities.py | 1 + api/core/workflow/nodes/tool/tool_node.py | 2 ++ api/services/tools/tools_transform_service.py | 10 ++++++++++ 6 files changed, 18 insertions(+) diff --git a/api/core/model_runtime/entities/provider_entities.py b/api/core/model_runtime/entities/provider_entities.py index d0f9ee13e5..c9aa8d1474 100644 --- a/api/core/model_runtime/entities/provider_entities.py +++ b/api/core/model_runtime/entities/provider_entities.py @@ -123,6 +123,8 @@ class ProviderEntity(BaseModel): description: Optional[I18nObject] = None icon_small: Optional[I18nObject] = None icon_large: Optional[I18nObject] = None + icon_small_dark: Optional[I18nObject] = None + icon_large_dark: Optional[I18nObject] = None background: Optional[str] = None help: Optional[ProviderHelpEntity] = None supported_model_types: Sequence[ModelType] diff --git a/api/core/plugin/entities/plugin.py b/api/core/plugin/entities/plugin.py index d6bbf05097..e5cf7ee03a 100644 --- a/api/core/plugin/entities/plugin.py +++ b/api/core/plugin/entities/plugin.py @@ -79,6 +79,7 @@ class PluginDeclaration(BaseModel): name: str = Field(..., pattern=r"^[a-z0-9_-]{1,128}$") description: I18nObject icon: str + icon_dark: Optional[str] = Field(default=None) label: I18nObject category: PluginCategory created_at: datetime.datetime diff --git a/api/core/tools/entities/api_entities.py b/api/core/tools/entities/api_entities.py index b94d6bba21..90134ba71d 100644 --- a/api/core/tools/entities/api_entities.py +++ b/api/core/tools/entities/api_entities.py @@ -28,6 +28,7 @@ class ToolProviderApiEntity(BaseModel): name: str # identifier description: I18nObject icon: str | dict + icon_dark: Optional[str | dict] = Field(default=None, description="The dark icon of the tool") label: I18nObject # label type: ToolProviderType masked_credentials: Optional[dict] = None @@ -72,6 +73,7 @@ class ToolProviderApiEntity(BaseModel): "plugin_unique_identifier": self.plugin_unique_identifier, "description": self.description.to_dict(), "icon": self.icon, + "icon_dark": self.icon_dark, "label": self.label.to_dict(), "type": self.type.value, "team_credentials": self.masked_credentials, diff --git a/api/core/tools/entities/tool_entities.py b/api/core/tools/entities/tool_entities.py index ba5fa5e156..bd216dad64 100644 --- a/api/core/tools/entities/tool_entities.py +++ b/api/core/tools/entities/tool_entities.py @@ -317,6 +317,7 @@ class ToolProviderIdentity(BaseModel): name: str = Field(..., description="The name of the tool") description: I18nObject = Field(..., description="The description of the tool") icon: str = Field(..., description="The icon of the tool") + icon_dark: Optional[str] = Field(default=None, description="The dark icon of the tool") label: I18nObject = Field(..., description="The label of the tool") tags: Optional[list[ToolLabelEnum]] = Field( default=[], diff --git a/api/core/workflow/nodes/tool/tool_node.py b/api/core/workflow/nodes/tool/tool_node.py index f5898dd605..5c6fac9080 100644 --- a/api/core/workflow/nodes/tool/tool_node.py +++ b/api/core/workflow/nodes/tool/tool_node.py @@ -339,10 +339,12 @@ class ToolNode(BaseNode[ToolNodeData]): if provider.name == dict_metadata["provider"] ) icon = builtin_tool.icon + icon_dark = builtin_tool.icon_dark except StopIteration: pass dict_metadata["icon"] = icon + dict_metadata["icon_dark"] = icon_dark message.message.metadata = dict_metadata agent_log = AgentLogEvent( id=message.message.id, diff --git a/api/services/tools/tools_transform_service.py b/api/services/tools/tools_transform_service.py index 8009c384b7..ac127ae93e 100644 --- a/api/services/tools/tools_transform_service.py +++ b/api/services/tools/tools_transform_service.py @@ -75,10 +75,18 @@ class ToolTransformService: provider.icon = ToolTransformService.get_plugin_icon_url( tenant_id=tenant_id, filename=provider.icon ) + if isinstance(provider.icon_dark, str) and provider.icon_dark: + provider.icon_dark = ToolTransformService.get_plugin_icon_url( + tenant_id=tenant_id, filename=provider.icon_dark + ) else: provider.icon = ToolTransformService.get_tool_provider_icon_url( provider_type=provider.type.value, provider_name=provider.name, icon=provider.icon ) + if provider.icon_dark: + provider.icon_dark = ToolTransformService.get_tool_provider_icon_url( + provider_type=provider.type.value, provider_name=provider.name, icon=provider.icon_dark + ) @classmethod def builtin_provider_to_user_provider( @@ -96,6 +104,7 @@ class ToolTransformService: name=provider_controller.entity.identity.name, description=provider_controller.entity.identity.description, icon=provider_controller.entity.identity.icon, + icon_dark=provider_controller.entity.identity.icon_dark, label=provider_controller.entity.identity.label, type=ToolProviderType.BUILT_IN, masked_credentials={}, @@ -179,6 +188,7 @@ class ToolTransformService: name=provider_controller.entity.identity.name, description=provider_controller.entity.identity.description, icon=provider_controller.entity.identity.icon, + icon_dark=provider_controller.entity.identity.icon_dark, label=provider_controller.entity.identity.label, type=ToolProviderType.WORKFLOW, masked_credentials={}, From c51b4290dc77d194f1c54778dbe14e3eced9ac77 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9D=9E=E6=B3=95=E6=93=8D=E4=BD=9C?= Date: Thu, 10 Jul 2025 16:14:18 +0800 Subject: [PATCH 275/329] fix: mcp server card button display (#22141) --- web/app/components/tools/mcp/mcp-service-card.tsx | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/web/app/components/tools/mcp/mcp-service-card.tsx b/web/app/components/tools/mcp/mcp-service-card.tsx index 443d7a1d1f..c0c542da26 100644 --- a/web/app/components/tools/mcp/mcp-service-card.tsx +++ b/web/app/components/tools/mcp/mcp-service-card.tsx @@ -1,9 +1,7 @@ 'use client' import React, { useEffect, useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' -import { - RiLoopLeftLine, -} from '@remixicon/react' +import { RiEditLine, RiLoopLeftLine } from '@remixicon/react' import { Mcp, } from '@/app/components/base/icons/src/vender/other' @@ -209,7 +207,11 @@ function MCPServiceCard({ variant='ghost' onClick={() => setShowMCPServerModal(true)} > - {serverPublished ? t('tools.mcp.server.edit') : t('tools.mcp.server.addDescription')} + +
+ +
{serverPublished ? t('tools.mcp.server.edit') : t('tools.mcp.server.addDescription')}
+
From 7b2cab576775b4c36bcfd4763b0227c8bf52ffaa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9D=9E=E6=B3=95=E6=93=8D=E4=BD=9C?= Date: Thu, 10 Jul 2025 16:14:46 +0800 Subject: [PATCH 276/329] feat: support ping method for MCP server (#22144) --- api/core/mcp/server/streamable_http.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/api/core/mcp/server/streamable_http.py b/api/core/mcp/server/streamable_http.py index 6b422ae4ae..37eec3cd9c 100644 --- a/api/core/mcp/server/streamable_http.py +++ b/api/core/mcp/server/streamable_http.py @@ -89,6 +89,7 @@ class MCPServerStreamableHTTPRequestHandler: types.ListToolsRequest: self.list_tools, types.CallToolRequest: self.invoke_tool, types.InitializedNotification: self.handle_notification, + types.PingRequest: self.handle_ping, } try: if self.request_type in handle_map: @@ -105,6 +106,9 @@ class MCPServerStreamableHTTPRequestHandler: def handle_notification(self): return "ping" + def handle_ping(self): + return types.EmptyResult() + def initialize(self): request = cast(types.InitializeRequest, self.request.root) client_info = request.params.clientInfo From 0e793a660de9f7f47a177ad76cdb36071972c2e7 Mon Sep 17 00:00:00 2001 From: Novice Date: Thu, 10 Jul 2025 17:13:48 +0800 Subject: [PATCH 277/329] fix: add the default value to the dark icon (#22149) --- api/core/workflow/nodes/tool/tool_node.py | 1 + 1 file changed, 1 insertion(+) diff --git a/api/core/workflow/nodes/tool/tool_node.py b/api/core/workflow/nodes/tool/tool_node.py index 5c6fac9080..48627a229d 100644 --- a/api/core/workflow/nodes/tool/tool_node.py +++ b/api/core/workflow/nodes/tool/tool_node.py @@ -329,6 +329,7 @@ class ToolNode(BaseNode[ToolNodeData]): icon = current_plugin.declaration.icon except StopIteration: pass + icon_dark = None try: builtin_tool = next( provider From 11f9a897e89a383c08730193b0de2660cdfc59af Mon Sep 17 00:00:00 2001 From: Joel Date: Thu, 10 Jul 2025 17:33:11 +0800 Subject: [PATCH 278/329] chore: fix schema editor can not hover item (#22155) --- .../visual-editor/schema-node.tsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/web/app/components/workflow/nodes/llm/components/json-schema-config-modal/visual-editor/schema-node.tsx b/web/app/components/workflow/nodes/llm/components/json-schema-config-modal/visual-editor/schema-node.tsx index 96bbf999db..36671ab050 100644 --- a/web/app/components/workflow/nodes/llm/components/json-schema-config-modal/visual-editor/schema-node.tsx +++ b/web/app/components/workflow/nodes/llm/components/json-schema-config-modal/visual-editor/schema-node.tsx @@ -79,13 +79,13 @@ const SchemaNode: FC = ({ } const handleMouseEnter = () => { - if(!readOnly) return + if(readOnly) return if (advancedEditing || isAddingNewField) return setHoveringPropertyDebounced(path.join('.')) } const handleMouseLeave = () => { - if(!readOnly) return + if(readOnly) return if (advancedEditing || isAddingNewField) return setHoveringPropertyDebounced(null) } @@ -95,7 +95,7 @@ const SchemaNode: FC = ({
{depth > 0 && hasChildren && (
Date: Thu, 10 Jul 2025 17:49:32 +0800 Subject: [PATCH 279/329] chore(version): bump to 1.6.0 (#22136) --- api/pyproject.toml | 2 +- api/uv.lock | 2 +- docker/docker-compose-template.yaml | 6 +++--- docker/docker-compose.yaml | 6 +++--- web/package.json | 2 +- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/api/pyproject.toml b/api/pyproject.toml index 9f2e3ed331..420bc771b6 100644 --- a/api/pyproject.toml +++ b/api/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "dify-api" -version = "1.5.1" +version = "1.6.0" requires-python = ">=3.11,<3.13" dependencies = [ diff --git a/api/uv.lock b/api/uv.lock index 45831e24a1..e108e0c445 100644 --- a/api/uv.lock +++ b/api/uv.lock @@ -1217,7 +1217,7 @@ wheels = [ [[package]] name = "dify-api" -version = "1.5.1" +version = "1.6.0" source = { virtual = "." } dependencies = [ { name = "arize-phoenix-otel" }, diff --git a/docker/docker-compose-template.yaml b/docker/docker-compose-template.yaml index fd7c78c7e7..7c1544acb9 100644 --- a/docker/docker-compose-template.yaml +++ b/docker/docker-compose-template.yaml @@ -2,7 +2,7 @@ x-shared-env: &shared-api-worker-env services: # API service api: - image: langgenius/dify-api:1.5.1 + image: langgenius/dify-api:1.6.0 restart: always environment: # Use the shared environment variables. @@ -31,7 +31,7 @@ services: # worker service # The Celery worker for processing the queue. worker: - image: langgenius/dify-api:1.5.1 + image: langgenius/dify-api:1.6.0 restart: always environment: # Use the shared environment variables. @@ -57,7 +57,7 @@ services: # Frontend web application. web: - image: langgenius/dify-web:1.5.1 + image: langgenius/dify-web:1.6.0 restart: always environment: CONSOLE_API_URL: ${CONSOLE_API_URL:-} diff --git a/docker/docker-compose.yaml b/docker/docker-compose.yaml index 0a95251ff0..647af62d96 100644 --- a/docker/docker-compose.yaml +++ b/docker/docker-compose.yaml @@ -518,7 +518,7 @@ x-shared-env: &shared-api-worker-env services: # API service api: - image: langgenius/dify-api:1.5.1 + image: langgenius/dify-api:1.6.0 restart: always environment: # Use the shared environment variables. @@ -547,7 +547,7 @@ services: # worker service # The Celery worker for processing the queue. worker: - image: langgenius/dify-api:1.5.1 + image: langgenius/dify-api:1.6.0 restart: always environment: # Use the shared environment variables. @@ -573,7 +573,7 @@ services: # Frontend web application. web: - image: langgenius/dify-web:1.5.1 + image: langgenius/dify-web:1.6.0 restart: always environment: CONSOLE_API_URL: ${CONSOLE_API_URL:-} diff --git a/web/package.json b/web/package.json index 254c2ec1fd..c9219b53d0 100644 --- a/web/package.json +++ b/web/package.json @@ -1,6 +1,6 @@ { "name": "dify-web", - "version": "1.5.1", + "version": "1.6.0", "private": true, "engines": { "node": ">=v22.11.0" From f4df80e093dfc433013fde12f52bb1b32b3f7b85 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=AF=97=E6=B5=93?= Date: Thu, 10 Jul 2025 20:56:45 +0800 Subject: [PATCH 280/329] fix(custom_tool): omit optional parameters instead of setting them to None (#22171) --- api/core/tools/custom_tool/tool.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/api/core/tools/custom_tool/tool.py b/api/core/tools/custom_tool/tool.py index 2f5cc6d4c0..5cba4cf7f5 100644 --- a/api/core/tools/custom_tool/tool.py +++ b/api/core/tools/custom_tool/tool.py @@ -213,7 +213,8 @@ class ApiTool(Tool): elif "default" in property: body[name] = property["default"] else: - body[name] = None + # omit optional parameters that weren't provided, instead of setting them to None + pass break # replace path parameters From f929bfb94c240d87dbd22599f091129e0c45ef68 Mon Sep 17 00:00:00 2001 From: NeatGuyCoding <15627489+NeatGuyCoding@users.noreply.github.com> Date: Fri, 11 Jul 2025 09:40:17 +0800 Subject: [PATCH 281/329] minor fix: remove duplicates, fix typo, and add restriction for get mcp server (#22170) Signed-off-by: neatguycoding <15627489+NeatGuyCoding@users.noreply.github.com> --- api/controllers/console/app/mcp_server.py | 6 +++++- api/core/mcp/server/streamable_http.py | 4 ++-- api/services/tools/mcp_tools_mange_service.py | 1 - 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/api/controllers/console/app/mcp_server.py b/api/controllers/console/app/mcp_server.py index 4f9e75c0d3..ccda97d80c 100644 --- a/api/controllers/console/app/mcp_server.py +++ b/api/controllers/console/app/mcp_server.py @@ -90,7 +90,11 @@ class AppMCPServerRefreshController(Resource): def get(self, server_id): if not current_user.is_editor: raise NotFound() - server = db.session.query(AppMCPServer).filter(AppMCPServer.id == server_id).first() + server = ( + db.session.query(AppMCPServer) + .filter(AppMCPServer.id == server_id and AppMCPServer.tenant_id == current_user.current_tenant_id) + .first() + ) if not server: raise NotFound() server.server_code = AppMCPServer.generate_server_code(16) diff --git a/api/core/mcp/server/streamable_http.py b/api/core/mcp/server/streamable_http.py index 37eec3cd9c..1c2cf570e2 100644 --- a/api/core/mcp/server/streamable_http.py +++ b/api/core/mcp/server/streamable_http.py @@ -112,13 +112,13 @@ class MCPServerStreamableHTTPRequestHandler: def initialize(self): request = cast(types.InitializeRequest, self.request.root) client_info = request.params.clientInfo - clinet_name = f"{client_info.name}@{client_info.version}" + client_name = f"{client_info.name}@{client_info.version}" if not self.end_user: end_user = EndUser( tenant_id=self.app.tenant_id, app_id=self.app.id, type="mcp", - name=clinet_name, + name=client_name, session_id=generate_session_id(), external_user_id=self.mcp_server.id, ) diff --git a/api/services/tools/mcp_tools_mange_service.py b/api/services/tools/mcp_tools_mange_service.py index 3b1592230a..7c23abda4b 100644 --- a/api/services/tools/mcp_tools_mange_service.py +++ b/api/services/tools/mcp_tools_mange_service.py @@ -69,7 +69,6 @@ class MCPToolManageService: MCPToolProvider.server_url_hash == server_url_hash, MCPToolProvider.server_identifier == server_identifier, ), - MCPToolProvider.tenant_id == tenant_id, ) .first() ) From e576b989b8eca4a7d8cfe6bdc91e9a451580489b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=AF=97=E6=B5=93?= Date: Fri, 11 Jul 2025 10:39:20 +0800 Subject: [PATCH 282/329] feat(tool): add support for API key authentication via query parameter (#21656) --- api/core/tools/custom_tool/provider.py | 28 ++++++++-- api/core/tools/custom_tool/tool.py | 18 ++++++- api/core/tools/entities/tool_entities.py | 3 +- api/core/tools/tool_manager.py | 18 ++++++- api/services/tools/tools_transform_service.py | 11 ++-- .../config-credentials.tsx | 52 ++++++++++++++++--- web/app/components/tools/types.ts | 4 +- web/i18n/en-US/tools.ts | 6 ++- web/i18n/zh-Hans/tools.ts | 6 ++- 9 files changed, 126 insertions(+), 20 deletions(-) diff --git a/api/core/tools/custom_tool/provider.py b/api/core/tools/custom_tool/provider.py index 3137d32013..fbe1d79137 100644 --- a/api/core/tools/custom_tool/provider.py +++ b/api/core/tools/custom_tool/provider.py @@ -39,19 +39,22 @@ class ApiToolProviderController(ToolProviderController): type=ProviderConfig.Type.SELECT, options=[ ProviderConfig.Option(value="none", label=I18nObject(en_US="None", zh_Hans="无")), - ProviderConfig.Option(value="api_key", label=I18nObject(en_US="api_key", zh_Hans="api_key")), + ProviderConfig.Option(value="api_key_header", label=I18nObject(en_US="Header", zh_Hans="请求头")), + ProviderConfig.Option( + value="api_key_query", label=I18nObject(en_US="Query Param", zh_Hans="查询参数") + ), ], default="none", help=I18nObject(en_US="The auth type of the api provider", zh_Hans="api provider 的认证类型"), ) ] - if auth_type == ApiProviderAuthType.API_KEY: + if auth_type == ApiProviderAuthType.API_KEY_HEADER: credentials_schema = [ *credentials_schema, ProviderConfig( name="api_key_header", required=False, - default="api_key", + default="Authorization", type=ProviderConfig.Type.TEXT_INPUT, help=I18nObject(en_US="The header name of the api key", zh_Hans="携带 api key 的 header 名称"), ), @@ -74,6 +77,25 @@ class ApiToolProviderController(ToolProviderController): ], ), ] + elif auth_type == ApiProviderAuthType.API_KEY_QUERY: + credentials_schema = [ + *credentials_schema, + ProviderConfig( + name="api_key_query_param", + required=False, + default="key", + type=ProviderConfig.Type.TEXT_INPUT, + help=I18nObject( + en_US="The query parameter name of the api key", zh_Hans="携带 api key 的查询参数名称" + ), + ), + ProviderConfig( + name="api_key_value", + required=True, + type=ProviderConfig.Type.SECRET_INPUT, + help=I18nObject(en_US="The api key", zh_Hans="api key 的值"), + ), + ] elif auth_type == ApiProviderAuthType.NONE: pass diff --git a/api/core/tools/custom_tool/tool.py b/api/core/tools/custom_tool/tool.py index 5cba4cf7f5..10653b9948 100644 --- a/api/core/tools/custom_tool/tool.py +++ b/api/core/tools/custom_tool/tool.py @@ -78,8 +78,8 @@ class ApiTool(Tool): if "auth_type" not in credentials: raise ToolProviderCredentialValidationError("Missing auth_type") - if credentials["auth_type"] == "api_key": - api_key_header = "api_key" + if credentials["auth_type"] in ("api_key_header", "api_key"): # backward compatibility: + api_key_header = "Authorization" if "api_key_header" in credentials: api_key_header = credentials["api_key_header"] @@ -100,6 +100,11 @@ class ApiTool(Tool): headers[api_key_header] = credentials["api_key_value"] + elif credentials["auth_type"] == "api_key_query": + # For query parameter authentication, we don't add anything to headers + # The query parameter will be added in do_http_request method + pass + needed_parameters = [parameter for parameter in (self.api_bundle.parameters or []) if parameter.required] for parameter in needed_parameters: if parameter.required and parameter.name not in parameters: @@ -154,6 +159,15 @@ class ApiTool(Tool): cookies = {} files = [] + # Add API key to query parameters if auth_type is api_key_query + if self.runtime and self.runtime.credentials: + credentials = self.runtime.credentials + if credentials.get("auth_type") == "api_key_query": + api_key_query_param = credentials.get("api_key_query_param", "key") + api_key_value = credentials.get("api_key_value") + if api_key_value: + params[api_key_query_param] = api_key_value + # check parameters for parameter in self.api_bundle.openapi.get("parameters", []): value = self.get_parameter_value(parameter, parameters) diff --git a/api/core/tools/entities/tool_entities.py b/api/core/tools/entities/tool_entities.py index bd216dad64..b5148e245f 100644 --- a/api/core/tools/entities/tool_entities.py +++ b/api/core/tools/entities/tool_entities.py @@ -96,7 +96,8 @@ class ApiProviderAuthType(Enum): """ NONE = "none" - API_KEY = "api_key" + API_KEY_HEADER = "api_key_header" + API_KEY_QUERY = "api_key_query" @classmethod def value_of(cls, value: str) -> "ApiProviderAuthType": diff --git a/api/core/tools/tool_manager.py b/api/core/tools/tool_manager.py index adae56cd27..22a9853b41 100644 --- a/api/core/tools/tool_manager.py +++ b/api/core/tools/tool_manager.py @@ -684,9 +684,16 @@ class ToolManager: if provider is None: raise ToolProviderNotFoundError(f"api provider {provider_id} not found") + auth_type = ApiProviderAuthType.NONE + provider_auth_type = provider.credentials.get("auth_type") + if provider_auth_type in ("api_key_header", "api_key"): # backward compatibility + auth_type = ApiProviderAuthType.API_KEY_HEADER + elif provider_auth_type == "api_key_query": + auth_type = ApiProviderAuthType.API_KEY_QUERY + controller = ApiToolProviderController.from_db( provider, - ApiProviderAuthType.API_KEY if provider.credentials["auth_type"] == "api_key" else ApiProviderAuthType.NONE, + auth_type, ) controller.load_bundled_tools(provider.tools) @@ -745,9 +752,16 @@ class ToolManager: credentials = {} # package tool provider controller + auth_type = ApiProviderAuthType.NONE + credentials_auth_type = credentials.get("auth_type") + if credentials_auth_type in ("api_key_header", "api_key"): # backward compatibility + auth_type = ApiProviderAuthType.API_KEY_HEADER + elif credentials_auth_type == "api_key_query": + auth_type = ApiProviderAuthType.API_KEY_QUERY + controller = ApiToolProviderController.from_db( provider_obj, - ApiProviderAuthType.API_KEY if credentials["auth_type"] == "api_key" else ApiProviderAuthType.NONE, + auth_type, ) # init tool configuration tool_configuration = ProviderConfigEncrypter( diff --git a/api/services/tools/tools_transform_service.py b/api/services/tools/tools_transform_service.py index ac127ae93e..3d0c35cd9b 100644 --- a/api/services/tools/tools_transform_service.py +++ b/api/services/tools/tools_transform_service.py @@ -159,11 +159,16 @@ class ToolTransformService: convert provider controller to user provider """ # package tool provider controller + auth_type = ApiProviderAuthType.NONE + credentials_auth_type = db_provider.credentials.get("auth_type") + if credentials_auth_type in ("api_key_header", "api_key"): # backward compatibility + auth_type = ApiProviderAuthType.API_KEY_HEADER + elif credentials_auth_type == "api_key_query": + auth_type = ApiProviderAuthType.API_KEY_QUERY + controller = ApiToolProviderController.from_db( db_provider=db_provider, - auth_type=ApiProviderAuthType.API_KEY - if db_provider.credentials["auth_type"] == "api_key" - else ApiProviderAuthType.NONE, + auth_type=auth_type, ) return controller diff --git a/web/app/components/tools/edit-custom-collection-modal/config-credentials.tsx b/web/app/components/tools/edit-custom-collection-modal/config-credentials.tsx index cbf1048b09..f0ad13f9b1 100644 --- a/web/app/components/tools/edit-custom-collection-modal/config-credentials.tsx +++ b/web/app/components/tools/edit-custom-collection-modal/config-credentials.tsx @@ -68,23 +68,34 @@ const ConfigCredential: FC = ({ text={t('tools.createTool.authMethod.types.none')} value={AuthType.none} isChecked={tempCredential.auth_type === AuthType.none} - onClick={value => setTempCredential({ ...tempCredential, auth_type: value as AuthType })} + onClick={value => setTempCredential({ + auth_type: value as AuthType, + })} /> setTempCredential({ - ...tempCredential, auth_type: value as AuthType, api_key_header: tempCredential.api_key_header || 'Authorization', api_key_value: tempCredential.api_key_value || '', api_key_header_prefix: tempCredential.api_key_header_prefix || AuthHeaderPrefix.custom, })} /> + setTempCredential({ + auth_type: value as AuthType, + api_key_query_param: tempCredential.api_key_query_param || 'key', + api_key_value: tempCredential.api_key_value || '', + })} + />
- {tempCredential.auth_type === AuthType.apiKey && ( + {tempCredential.auth_type === AuthType.apiKeyHeader && ( <>
{t('tools.createTool.authHeaderPrefix.title')}
@@ -136,6 +147,35 @@ const ConfigCredential: FC = ({ />
)} + {tempCredential.auth_type === AuthType.apiKeyQuery && ( + <> +
+
+ {t('tools.createTool.authMethod.queryParam')} + + {t('tools.createTool.authMethod.queryParamTooltip')} +
+ } + triggerClassName='ml-0.5 w-4 h-4' + /> +
+ setTempCredential({ ...tempCredential, api_key_query_param: e.target.value })} + placeholder={t('tools.createTool.authMethod.types.queryParamPlaceholder')!} + /> +
+
+
{t('tools.createTool.authMethod.value')}
+ setTempCredential({ ...tempCredential, api_key_value: e.target.value })} + placeholder={t('tools.createTool.authMethod.types.apiValuePlaceholder')!} + /> +
+ )}
diff --git a/web/app/components/tools/types.ts b/web/app/components/tools/types.ts index d444ee1f38..b83919ad18 100644 --- a/web/app/components/tools/types.ts +++ b/web/app/components/tools/types.ts @@ -7,7 +7,8 @@ export enum LOC { export enum AuthType { none = 'none', - apiKey = 'api_key', + apiKeyHeader = 'api_key_header', + apiKeyQuery = 'api_key_query', } export enum AuthHeaderPrefix { @@ -21,6 +22,7 @@ export type Credential = { api_key_header?: string api_key_value?: string api_key_header_prefix?: AuthHeaderPrefix + api_key_query_param?: string } export enum CollectionType { diff --git a/web/i18n/en-US/tools.ts b/web/i18n/en-US/tools.ts index 418d1cb076..4e1ce1308a 100644 --- a/web/i18n/en-US/tools.ts +++ b/web/i18n/en-US/tools.ts @@ -80,11 +80,15 @@ const translation = { title: 'Authorization method', type: 'Authorization type', keyTooltip: 'Http Header Key, You can leave it with "Authorization" if you have no idea what it is or set it to a custom value', + queryParam: 'Query Parameter', + queryParamTooltip: 'The name of the API key query parameter to pass, e.g. "key" in "https://example.com/test?key=API_KEY".', types: { none: 'None', - api_key: 'API Key', + api_key_header: 'Header', + api_key_query: 'Query Param', apiKeyPlaceholder: 'HTTP header name for API Key', apiValuePlaceholder: 'Enter API Key', + queryParamPlaceholder: 'Query parameter name for API Key', }, key: 'Key', value: 'Value', diff --git a/web/i18n/zh-Hans/tools.ts b/web/i18n/zh-Hans/tools.ts index 4e0ccf476f..5c1eb13236 100644 --- a/web/i18n/zh-Hans/tools.ts +++ b/web/i18n/zh-Hans/tools.ts @@ -80,11 +80,15 @@ const translation = { title: '鉴权方法', type: '鉴权类型', keyTooltip: 'HTTP 头部名称,如果你不知道是什么,可以将其保留为 Authorization 或设置为自定义值', + queryParam: '查询参数', + queryParamTooltip: '用于传递 API 密钥查询参数的名称, 如 "https://example.com/test?key=API_KEY" 中的 "key"参数', types: { none: '无', - api_key: 'API Key', + api_key_header: '请求头', + api_key_query: '查询参数', apiKeyPlaceholder: 'HTTP 头部名称,用于传递 API Key', apiValuePlaceholder: '输入 API Key', + queryParamPlaceholder: '查询参数名称,用于传递 API Key', }, key: '键', value: '值', From c805238471eb4f15daa830730dbc828bd468f051 Mon Sep 17 00:00:00 2001 From: Wu Tianwei <30284043+WTW0313@users.noreply.github.com> Date: Fri, 11 Jul 2025 11:17:28 +0800 Subject: [PATCH 283/329] fix: adjust layout styles for header and dataset update (#22182) --- web/app/components/datasets/create/index.tsx | 2 +- .../notion-page-preview/index.module.css | 2 +- .../datasets/create/step-one/index.tsx | 43 ++++++++++++------- web/app/components/header/index.tsx | 2 +- 4 files changed, 30 insertions(+), 19 deletions(-) diff --git a/web/app/components/datasets/create/index.tsx b/web/app/components/datasets/create/index.tsx index b1e4087226..a1ff2f5d87 100644 --- a/web/app/components/datasets/create/index.tsx +++ b/web/app/components/datasets/create/index.tsx @@ -122,7 +122,7 @@ const DatasetUpdateForm = ({ datasetId }: DatasetUpdateFormProps) => { return return ( -
+
{step === 1 && setShowModal(true) const modalCloseHandle = () => setShowModal(false) - const updateCurrentFile = (file: File) => { + const updateCurrentFile = useCallback((file: File) => { setCurrentFile(file) - } - const hideFilePreview = () => { + }, []) + + const hideFilePreview = useCallback(() => { setCurrentFile(undefined) - } + }, []) - const updateCurrentPage = (page: NotionPage) => { + const updateCurrentPage = useCallback((page: NotionPage) => { setCurrentNotionPage(page) - } + }, []) - const hideNotionPagePreview = () => { + const hideNotionPagePreview = useCallback(() => { setCurrentNotionPage(undefined) - } + }, []) + + const updateWebsite = useCallback((website: CrawlResultItem) => { + setCurrentWebsite(website) + }, []) - const hideWebsitePreview = () => { + const hideWebsitePreview = useCallback(() => { setCurrentWebsite(undefined) - } + }, []) const shouldShowDataSourceTypeList = !datasetId || (datasetId && !dataset?.data_source_type) const isInCreatePage = shouldShowDataSourceTypeList @@ -139,7 +144,7 @@ const StepOne = ({
{ shouldShowDataSourceTypeList && ( -
+
{t('datasetCreation.steps.one')}
) @@ -158,8 +163,8 @@ const StepOne = ({ if (dataSourceTypeDisable) return changeType(DataSourceType.FILE) - hideFilePreview() hideNotionPagePreview() + hideWebsitePreview() }} > @@ -182,7 +187,7 @@ const StepOne = ({ return changeType(DataSourceType.NOTION) hideFilePreview() - hideNotionPagePreview() + hideWebsitePreview() }} > @@ -201,7 +206,13 @@ const StepOne = ({ dataSourceType === DataSourceType.WEB && s.active, dataSourceTypeDisable && dataSourceType !== DataSourceType.WEB && s.disabled, )} - onClick={() => changeType(DataSourceType.WEB)} + onClick={() => { + if (dataSourceTypeDisable) + return + changeType(DataSourceType.WEB) + hideFilePreview() + hideNotionPagePreview() + }} >
{ } return ( -
+
{systemFeatures.branding.enabled && systemFeatures.branding.workspace_logo From d5624ba671201137c32b80ad6e2131feb85fed19 Mon Sep 17 00:00:00 2001 From: K <84141602+krikera@users.noreply.github.com> Date: Fri, 11 Jul 2025 09:41:59 +0530 Subject: [PATCH 284/329] fix: resolve Docker file URL networking issue for plugins (#21334) (#21382) Co-authored-by: crazywoola <427733928@qq.com> --- api/.env.example | 5 +++++ api/configs/feature/__init__.py | 7 +++++++ api/core/file/helpers.py | 4 +++- api/core/tools/signature.py | 5 +++-- api/core/tools/tool_file_manager.py | 5 +++-- docker/.env.example | 5 +++++ docker/docker-compose.yaml | 1 + 7 files changed, 27 insertions(+), 5 deletions(-) diff --git a/api/.env.example b/api/.env.example index baa9c382c8..a7ea6cf937 100644 --- a/api/.env.example +++ b/api/.env.example @@ -17,6 +17,11 @@ APP_WEB_URL=http://127.0.0.1:3000 # Files URL FILES_URL=http://127.0.0.1:5001 +# INTERNAL_FILES_URL is used for plugin daemon communication within Docker network. +# Set this to the internal Docker service URL for proper plugin file access. +# Example: INTERNAL_FILES_URL=http://api:5001 +INTERNAL_FILES_URL=http://127.0.0.1:5001 + # The time in seconds after the signature is rejected FILES_ACCESS_TIMEOUT=300 diff --git a/api/configs/feature/__init__.py b/api/configs/feature/__init__.py index df15b92c35..963fcbedf9 100644 --- a/api/configs/feature/__init__.py +++ b/api/configs/feature/__init__.py @@ -237,6 +237,13 @@ class FileAccessConfig(BaseSettings): default="", ) + INTERNAL_FILES_URL: str = Field( + description="Internal base URL for file access within Docker network," + " used for plugin daemon and internal service communication." + " Falls back to FILES_URL if not specified.", + default="", + ) + FILES_ACCESS_TIMEOUT: int = Field( description="Expiration time in seconds for file access URLs", default=300, diff --git a/api/core/file/helpers.py b/api/core/file/helpers.py index 73fabdb11b..335ad2266a 100644 --- a/api/core/file/helpers.py +++ b/api/core/file/helpers.py @@ -21,7 +21,9 @@ def get_signed_file_url(upload_file_id: str) -> str: def get_signed_file_url_for_plugin(filename: str, mimetype: str, tenant_id: str, user_id: str) -> str: - url = f"{dify_config.FILES_URL}/files/upload/for-plugin" + # Plugin access should use internal URL for Docker network communication + base_url = dify_config.INTERNAL_FILES_URL or dify_config.FILES_URL + url = f"{base_url}/files/upload/for-plugin" if user_id is None: user_id = "DEFAULT-USER" diff --git a/api/core/tools/signature.py b/api/core/tools/signature.py index e80005d7bf..5cdf473542 100644 --- a/api/core/tools/signature.py +++ b/api/core/tools/signature.py @@ -9,9 +9,10 @@ from configs import dify_config def sign_tool_file(tool_file_id: str, extension: str) -> str: """ - sign file to get a temporary url + sign file to get a temporary url for plugin access """ - base_url = dify_config.FILES_URL + # Use internal URL for plugin/tool file access in Docker environments + base_url = dify_config.INTERNAL_FILES_URL or dify_config.FILES_URL file_preview_url = f"{base_url}/files/tools/{tool_file_id}{extension}" timestamp = str(int(time.time())) diff --git a/api/core/tools/tool_file_manager.py b/api/core/tools/tool_file_manager.py index b849f51064..ece02f9d59 100644 --- a/api/core/tools/tool_file_manager.py +++ b/api/core/tools/tool_file_manager.py @@ -35,9 +35,10 @@ class ToolFileManager: @staticmethod def sign_file(tool_file_id: str, extension: str) -> str: """ - sign file to get a temporary url + sign file to get a temporary url for plugin access """ - base_url = dify_config.FILES_URL + # Use internal URL for plugin/tool file access in Docker environments + base_url = dify_config.INTERNAL_FILES_URL or dify_config.FILES_URL file_preview_url = f"{base_url}/files/tools/{tool_file_id}{extension}" timestamp = str(int(time.time())) diff --git a/docker/.env.example b/docker/.env.example index a403f25cb2..84b6152f0a 100644 --- a/docker/.env.example +++ b/docker/.env.example @@ -47,6 +47,11 @@ APP_WEB_URL= # ensuring port 5001 is externally accessible (see docker-compose.yaml). FILES_URL= +# INTERNAL_FILES_URL is used for plugin daemon communication within Docker network. +# Set this to the internal Docker service URL for proper plugin file access. +# Example: INTERNAL_FILES_URL=http://api:5001 +INTERNAL_FILES_URL= + # ------------------------------ # Server Configuration # ------------------------------ diff --git a/docker/docker-compose.yaml b/docker/docker-compose.yaml index 647af62d96..ac9953aa33 100644 --- a/docker/docker-compose.yaml +++ b/docker/docker-compose.yaml @@ -11,6 +11,7 @@ x-shared-env: &shared-api-worker-env APP_API_URL: ${APP_API_URL:-} APP_WEB_URL: ${APP_WEB_URL:-} FILES_URL: ${FILES_URL:-} + INTERNAL_FILES_URL: ${INTERNAL_FILES_URL:-} LOG_LEVEL: ${LOG_LEVEL:-INFO} LOG_FILE: ${LOG_FILE:-/app/logs/server.log} LOG_FILE_MAX_SIZE: ${LOG_FILE_MAX_SIZE:-20} From 9a9ec0c99bd7ddbf4b1aaa8767e4280510f8c67d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcelo=20D=C3=ADaz?= <40875838+marcelodiaz558@users.noreply.github.com> Date: Fri, 11 Jul 2025 02:04:42 -0400 Subject: [PATCH 285/329] feat: Add Audio configuration setting to app configuration UI (#21957) --- .../app/configuration/config/config-audio.tsx | 78 +++++++++++++++++++ .../app/configuration/config/index.tsx | 3 + .../components/app/configuration/index.tsx | 2 + web/context/debug-configuration.ts | 2 + web/i18n/en-US/app-debug.ts | 4 + 5 files changed, 89 insertions(+) create mode 100644 web/app/components/app/configuration/config/config-audio.tsx diff --git a/web/app/components/app/configuration/config/config-audio.tsx b/web/app/components/app/configuration/config/config-audio.tsx new file mode 100644 index 0000000000..5600f8cbb6 --- /dev/null +++ b/web/app/components/app/configuration/config/config-audio.tsx @@ -0,0 +1,78 @@ +'use client' +import type { FC } from 'react' +import React, { useCallback } from 'react' +import { useTranslation } from 'react-i18next' +import produce from 'immer' +import { useContext } from 'use-context-selector' + +import { Microphone01 } from '@/app/components/base/icons/src/vender/features' +import Tooltip from '@/app/components/base/tooltip' +import ConfigContext from '@/context/debug-configuration' +import { SupportUploadFileTypes } from '@/app/components/workflow/types' +import { useFeatures, useFeaturesStore } from '@/app/components/base/features/hooks' +import Switch from '@/app/components/base/switch' + +const ConfigAudio: FC = () => { + const { t } = useTranslation() + const file = useFeatures(s => s.features.file) + const featuresStore = useFeaturesStore() + const { isShowAudioConfig } = useContext(ConfigContext) + + const isAudioEnabled = file?.allowed_file_types?.includes(SupportUploadFileTypes.audio) ?? false + + const handleChange = useCallback((value: boolean) => { + const { + features, + setFeatures, + } = featuresStore!.getState() + + const newFeatures = produce(features, (draft) => { + if (value) { + draft.file!.allowed_file_types = Array.from(new Set([ + ...(draft.file?.allowed_file_types || []), + SupportUploadFileTypes.audio, + ])) + } + else { + draft.file!.allowed_file_types = draft.file!.allowed_file_types?.filter( + type => type !== SupportUploadFileTypes.audio, + ) + } + if (draft.file) + draft.file.enabled = (draft.file.allowed_file_types?.length ?? 0) > 0 + }) + setFeatures(newFeatures) + }, [featuresStore]) + + if (!isShowAudioConfig) + return null + + return ( +
+
+
+ +
+
+
+
{t('appDebug.feature.audioUpload.title')}
+ + {t('appDebug.feature.audioUpload.description')} +
+ } + /> +
+
+
+ +
+
+ ) +} +export default React.memo(ConfigAudio) diff --git a/web/app/components/app/configuration/config/index.tsx b/web/app/components/app/configuration/config/index.tsx index dc2095502e..d0375c6de9 100644 --- a/web/app/components/app/configuration/config/index.tsx +++ b/web/app/components/app/configuration/config/index.tsx @@ -8,6 +8,7 @@ import DatasetConfig from '../dataset-config' import HistoryPanel from '../config-prompt/conversation-history/history-panel' import ConfigVision from '../config-vision' import ConfigDocument from './config-document' +import ConfigAudio from './config-audio' import AgentTools from './agent/agent-tools' import ConfigContext from '@/context/debug-configuration' import ConfigPrompt from '@/app/components/app/configuration/config-prompt' @@ -85,6 +86,8 @@ const Config: FC = () => { + + {/* Chat History */} {isAdvancedMode && isChatApp && modelModeType === ModelModeType.completion && ( { const isShowVisionConfig = !!currModel?.features?.includes(ModelFeatureEnum.vision) const isShowDocumentConfig = !!currModel?.features?.includes(ModelFeatureEnum.document) + const isShowAudioConfig = !!currModel?.features?.includes(ModelFeatureEnum.audio) const isAllowVideoUpload = !!currModel?.features?.includes(ModelFeatureEnum.video) // *** web app features *** const featuresData: FeaturesData = useMemo(() => { @@ -920,6 +921,7 @@ const Configuration: FC = () => { setVisionConfig: handleSetVisionConfig, isAllowVideoUpload, isShowDocumentConfig, + isShowAudioConfig, rerankSettingModalOpen, setRerankSettingModalOpen, }} diff --git a/web/context/debug-configuration.ts b/web/context/debug-configuration.ts index 47710c8fc1..cb737c5288 100644 --- a/web/context/debug-configuration.ts +++ b/web/context/debug-configuration.ts @@ -102,6 +102,7 @@ type IDebugConfiguration = { setVisionConfig: (visionConfig: VisionSettings, noNotice?: boolean) => void isAllowVideoUpload: boolean isShowDocumentConfig: boolean + isShowAudioConfig: boolean rerankSettingModalOpen: boolean setRerankSettingModalOpen: (rerankSettingModalOpen: boolean) => void } @@ -254,6 +255,7 @@ const DebugConfigurationContext = createContext({ setVisionConfig: noop, isAllowVideoUpload: false, isShowDocumentConfig: false, + isShowAudioConfig: false, rerankSettingModalOpen: false, setRerankSettingModalOpen: noop, }) diff --git a/web/i18n/en-US/app-debug.ts b/web/i18n/en-US/app-debug.ts index 349ff37118..5282dab360 100644 --- a/web/i18n/en-US/app-debug.ts +++ b/web/i18n/en-US/app-debug.ts @@ -222,6 +222,10 @@ const translation = { title: 'Document', description: 'Enable Document will allows the model to take in documents and answer questions about them.', }, + audioUpload: { + title: 'Audio', + description: 'Enable Audio will allow the model to process audio files for transcription and analysis.', + }, }, codegen: { title: 'Code Generator', From fe4e2f7921e4362f699cb07f2bcd75f5774632ba Mon Sep 17 00:00:00 2001 From: le0zh Date: Fri, 11 Jul 2025 15:07:32 +0800 Subject: [PATCH 286/329] feat: support var in suggested questions (#17340) Co-authored-by: crazywoola <427733928@qq.com> --- web/app/components/base/chat/chat/hooks.ts | 5 ++--- .../features/new-feature-panel/conversation-opener/modal.tsx | 1 + web/app/components/workflow/panel/debug-and-preview/hooks.ts | 4 ++-- web/i18n/de-DE/app-debug.ts | 1 + web/i18n/en-US/app-debug.ts | 1 + web/i18n/es-ES/app-debug.ts | 1 + web/i18n/fa-IR/app-debug.ts | 1 + web/i18n/fr-FR/app-debug.ts | 1 + web/i18n/hi-IN/app-debug.ts | 1 + web/i18n/it-IT/app-debug.ts | 1 + web/i18n/ja-JP/app-debug.ts | 1 + web/i18n/ko-KR/app-debug.ts | 1 + web/i18n/pl-PL/app-debug.ts | 1 + web/i18n/pt-BR/app-debug.ts | 1 + web/i18n/ru-RU/app-debug.ts | 1 + web/i18n/tr-TR/app-debug.ts | 1 + web/i18n/uk-UA/app-debug.ts | 1 + web/i18n/vi-VN/app-debug.ts | 1 + web/i18n/zh-Hans/app-debug.ts | 1 + web/i18n/zh-Hant/app-debug.ts | 1 + 20 files changed, 22 insertions(+), 5 deletions(-) diff --git a/web/app/components/base/chat/chat/hooks.ts b/web/app/components/base/chat/chat/hooks.ts index 10fb455d33..17373cec9d 100644 --- a/web/app/components/base/chat/chat/hooks.ts +++ b/web/app/components/base/chat/chat/hooks.ts @@ -83,12 +83,11 @@ export const useChat = ( const ret = [...threadMessages] if (config?.opening_statement) { const index = threadMessages.findIndex(item => item.isOpeningStatement) - if (index > -1) { ret[index] = { ...ret[index], content: getIntroduction(config.opening_statement), - suggestedQuestions: config.suggested_questions, + suggestedQuestions: config.suggested_questions?.map(item => getIntroduction(item)), } } else { @@ -97,7 +96,7 @@ export const useChat = ( content: getIntroduction(config.opening_statement), isAnswer: true, isOpeningStatement: true, - suggestedQuestions: config.suggested_questions, + suggestedQuestions: config.suggested_questions?.map(item => getIntroduction(item)), }) } } diff --git a/web/app/components/base/features/new-feature-panel/conversation-opener/modal.tsx b/web/app/components/base/features/new-feature-panel/conversation-opener/modal.tsx index 117c8a5558..51e33c43d2 100644 --- a/web/app/components/base/features/new-feature-panel/conversation-opener/modal.tsx +++ b/web/app/components/base/features/new-feature-panel/conversation-opener/modal.tsx @@ -130,6 +130,7 @@ const OpeningSettingModal = ({ { const value = e.target.value setTempSuggestedQuestions(tempSuggestedQuestions.map((item, i) => { diff --git a/web/app/components/workflow/panel/debug-and-preview/hooks.ts b/web/app/components/workflow/panel/debug-and-preview/hooks.ts index 26d604ef11..d98cf49812 100644 --- a/web/app/components/workflow/panel/debug-and-preview/hooks.ts +++ b/web/app/components/workflow/panel/debug-and-preview/hooks.ts @@ -92,7 +92,7 @@ export const useChat = ( ret[index] = { ...ret[index], content: getIntroduction(config.opening_statement), - suggestedQuestions: config.suggested_questions, + suggestedQuestions: config.suggested_questions?.map((item: string) => getIntroduction(item)), } } else { @@ -101,7 +101,7 @@ export const useChat = ( content: getIntroduction(config.opening_statement), isAnswer: true, isOpeningStatement: true, - suggestedQuestions: config.suggested_questions, + suggestedQuestions: config.suggested_questions?.map((item: string) => getIntroduction(item)), }) } } diff --git a/web/i18n/de-DE/app-debug.ts b/web/i18n/de-DE/app-debug.ts index 4022e755e9..1adaf6f05d 100644 --- a/web/i18n/de-DE/app-debug.ts +++ b/web/i18n/de-DE/app-debug.ts @@ -298,6 +298,7 @@ const translation = { add: 'Hinzufügen', writeOpener: 'Eröffnung schreiben', placeholder: 'Schreiben Sie hier Ihre Eröffnungsnachricht, Sie können Variablen verwenden, versuchen Sie {{Variable}} zu tippen.', + openingQuestionPlaceholder: 'Sie können Variablen verwenden, versuchen Sie {{variable}} einzugeben.', openingQuestion: 'Eröffnungsfragen', noDataPlaceHolder: 'Den Dialog mit dem Benutzer zu beginnen, kann helfen, in konversationellen Anwendungen eine engere Verbindung mit ihnen herzustellen.', diff --git a/web/i18n/en-US/app-debug.ts b/web/i18n/en-US/app-debug.ts index 5282dab360..938cb27c12 100644 --- a/web/i18n/en-US/app-debug.ts +++ b/web/i18n/en-US/app-debug.ts @@ -446,6 +446,7 @@ const translation = { writeOpener: 'Edit opener', placeholder: 'Write your opener message here, you can use variables, try type {{variable}}.', openingQuestion: 'Opening Questions', + openingQuestionPlaceholder: 'You can use variables, try typing {{variable}}.', noDataPlaceHolder: 'Starting the conversation with the user can help AI establish a closer connection with them in conversational applications.', varTip: 'You can use variables, try type {{variable}}', diff --git a/web/i18n/es-ES/app-debug.ts b/web/i18n/es-ES/app-debug.ts index 8c986bf669..afdea66338 100644 --- a/web/i18n/es-ES/app-debug.ts +++ b/web/i18n/es-ES/app-debug.ts @@ -329,6 +329,7 @@ const translation = { writeOpener: 'Escribir apertura', placeholder: 'Escribe tu mensaje de apertura aquí, puedes usar variables, intenta escribir {{variable}}.', openingQuestion: 'Preguntas de Apertura', + openingQuestionPlaceholder: 'Puede usar variables, intente escribir {{variable}}.', noDataPlaceHolder: 'Iniciar la conversación con el usuario puede ayudar a la IA a establecer una conexión más cercana con ellos en aplicaciones de conversación.', varTip: 'Puedes usar variables, intenta escribir {{variable}}', tooShort: 'Se requieren al menos 20 palabras en la indicación inicial para generar una apertura de conversación.', diff --git a/web/i18n/fa-IR/app-debug.ts b/web/i18n/fa-IR/app-debug.ts index 5cf9c15efe..75085ef30e 100644 --- a/web/i18n/fa-IR/app-debug.ts +++ b/web/i18n/fa-IR/app-debug.ts @@ -364,6 +364,7 @@ const translation = { writeOpener: 'نوشتن آغازگر', placeholder: 'پیام آغازگر خود را اینجا بنویسید، می‌توانید از متغیرها استفاده کنید، سعی کنید {{variable}} را تایپ کنید.', openingQuestion: 'سوالات آغازین', + openingQuestionPlaceholder: 'می‌توانید از متغیرها استفاده کنید، سعی کنید {{variable}} را تایپ کنید.', noDataPlaceHolder: 'شروع مکالمه با کاربر می‌تواند به AI کمک کند تا ارتباط نزدیک‌تری با آنها برقرار کند.', varTip: 'می‌توانید از متغیرها استفاده کنید، سعی کنید {{variable}} را تایپ کنید', tooShort: 'حداقل 20 کلمه از پرسش اولیه برای تولید نظرات آغازین مکالمه مورد نیاز است.', diff --git a/web/i18n/fr-FR/app-debug.ts b/web/i18n/fr-FR/app-debug.ts index 6671092930..f3984c0435 100644 --- a/web/i18n/fr-FR/app-debug.ts +++ b/web/i18n/fr-FR/app-debug.ts @@ -317,6 +317,7 @@ const translation = { writeOpener: 'Écrire l\'introduction', placeholder: 'Rédigez votre message d\'ouverture ici, vous pouvez utiliser des variables, essayez de taper {{variable}}.', openingQuestion: 'Questions d\'ouverture', + openingQuestionPlaceholder: 'Vous pouvez utiliser des variables, essayez de taper {{variable}}.', noDataPlaceHolder: 'Commencer la conversation avec l\'utilisateur peut aider l\'IA à établir une connexion plus proche avec eux dans les applications conversationnelles.', varTip: 'Vous pouvez utiliser des variables, essayez de taper {{variable}}', diff --git a/web/i18n/hi-IN/app-debug.ts b/web/i18n/hi-IN/app-debug.ts index 3f4b06c08b..ded2af4132 100644 --- a/web/i18n/hi-IN/app-debug.ts +++ b/web/i18n/hi-IN/app-debug.ts @@ -362,6 +362,7 @@ const translation = { placeholder: 'यहां अपना प्रारंभक संदेश लिखें, आप वेरिएबल्स का उपयोग कर सकते हैं, {{variable}} टाइप करने का प्रयास करें।', openingQuestion: 'प्रारंभिक प्रश्न', + openingQuestionPlaceholder: 'आप वेरिएबल्स का उपयोग कर सकते हैं, {{variable}} टाइप करके देखें।', noDataPlaceHolder: 'उपयोगकर्ता के साथ संवाद प्रारंभ करने से एआई को संवादात्मक अनुप्रयोगों में उनके साथ निकट संबंध स्थापित करने में मदद मिल सकती है।', varTip: diff --git a/web/i18n/it-IT/app-debug.ts b/web/i18n/it-IT/app-debug.ts index c8b3c08302..bfa75b282b 100644 --- a/web/i18n/it-IT/app-debug.ts +++ b/web/i18n/it-IT/app-debug.ts @@ -365,6 +365,7 @@ const translation = { placeholder: 'Scrivi qui il tuo messaggio introduttivo, puoi usare variabili, prova a scrivere {{variable}}.', openingQuestion: 'Domande iniziali', + openingQuestionPlaceholder: 'Puoi usare variabili, prova a digitare {{variable}}.', noDataPlaceHolder: 'Iniziare la conversazione con l\'utente può aiutare l\'IA a stabilire un legame più stretto con loro nelle applicazioni conversazionali.', varTip: 'Puoi usare variabili, prova a scrivere {{variable}}', diff --git a/web/i18n/ja-JP/app-debug.ts b/web/i18n/ja-JP/app-debug.ts index f862f3f2f7..decbe4863e 100644 --- a/web/i18n/ja-JP/app-debug.ts +++ b/web/i18n/ja-JP/app-debug.ts @@ -434,6 +434,7 @@ const translation = { writeOpener: 'オープナーを書く', placeholder: 'ここにオープナーメッセージを書いてください。変数を使用できます。{{variable}} を入力してみてください。', openingQuestion: '開始質問', + openingQuestionPlaceholder: '変数を使用できます。{{variable}} と入力してみてください。', noDataPlaceHolder: 'ユーザーとの会話を開始すると、会話アプリケーションで彼らとのより密接な関係を築くのに役立ちます。', varTip: '変数を使用できます。{{variable}} を入力してみてください', diff --git a/web/i18n/ko-KR/app-debug.ts b/web/i18n/ko-KR/app-debug.ts index 3c5a3f4b1f..b84946841f 100644 --- a/web/i18n/ko-KR/app-debug.ts +++ b/web/i18n/ko-KR/app-debug.ts @@ -328,6 +328,7 @@ const translation = { writeOpener: '오프너 작성', placeholder: '여기에 오프너 메시지를 작성하세요. 변수를 사용할 수 있습니다. {{variable}}를 입력해보세요.', openingQuestion: '시작 질문', + openingQuestionPlaceholder: '변수를 사용할 수 있습니다. {{variable}}을(를) 입력해 보세요.', noDataPlaceHolder: '사용자와의 대화를 시작하면 대화 애플리케이션에서 그들과 더 밀접한 관계를 구축하는 데 도움이 됩니다.', varTip: '변수를 사용할 수 있습니다. {{variable}}를 입력해보세요.', tooShort: '대화 시작에는 최소 20 단어의 초기 프롬프트가 필요합니다.', diff --git a/web/i18n/pl-PL/app-debug.ts b/web/i18n/pl-PL/app-debug.ts index 48b44c0cbb..dcd286d351 100644 --- a/web/i18n/pl-PL/app-debug.ts +++ b/web/i18n/pl-PL/app-debug.ts @@ -360,6 +360,7 @@ const translation = { placeholder: 'Tutaj napisz swoją wiadomość wprowadzającą, możesz użyć zmiennych, spróbuj wpisać {{variable}}.', openingQuestion: 'Pytania otwierające', + openingQuestionPlaceholder: 'Możesz używać zmiennych, spróbuj wpisać {{variable}}.', noDataPlaceHolder: 'Rozpoczynanie rozmowy z użytkownikiem może pomóc AI nawiązać bliższe połączenie z nim w aplikacjach konwersacyjnych.', varTip: 'Możesz używać zmiennych, spróbuj wpisać {{variable}}', diff --git a/web/i18n/pt-BR/app-debug.ts b/web/i18n/pt-BR/app-debug.ts index 64f7a85fe7..96d78dc9a3 100644 --- a/web/i18n/pt-BR/app-debug.ts +++ b/web/i18n/pt-BR/app-debug.ts @@ -334,6 +334,7 @@ const translation = { writeOpener: 'Escrever abertura', placeholder: 'Escreva sua mensagem de abertura aqui, você pode usar variáveis, tente digitar {{variável}}.', openingQuestion: 'Perguntas de Abertura', + openingQuestionPlaceholder: 'Você pode usar variáveis, tente digitar {{variable}}.', noDataPlaceHolder: 'Iniciar a conversa com o usuário pode ajudar a IA a estabelecer uma conexão mais próxima com eles em aplicativos de conversação.', varTip: 'Você pode usar variáveis, tente digitar {{variável}}', diff --git a/web/i18n/ru-RU/app-debug.ts b/web/i18n/ru-RU/app-debug.ts index 00cd6e8a75..5d4dbb53d3 100644 --- a/web/i18n/ru-RU/app-debug.ts +++ b/web/i18n/ru-RU/app-debug.ts @@ -370,6 +370,7 @@ const translation = { writeOpener: 'Написать начальное сообщение', placeholder: 'Напишите здесь свое начальное сообщение, вы можете использовать переменные, попробуйте ввести {{variable}}.', openingQuestion: 'Начальные вопросы', + openingQuestionPlaceholder: 'Вы можете использовать переменные, попробуйте ввести {{variable}}.', noDataPlaceHolder: 'Начало разговора с пользователем может помочь ИИ установить более тесную связь с ним в диалоговых приложениях.', varTip: 'Вы можете использовать переменные, попробуйте ввести {{variable}}', diff --git a/web/i18n/tr-TR/app-debug.ts b/web/i18n/tr-TR/app-debug.ts index f08d221d45..6ed0e0a5eb 100644 --- a/web/i18n/tr-TR/app-debug.ts +++ b/web/i18n/tr-TR/app-debug.ts @@ -368,6 +368,7 @@ const translation = { writeOpener: 'Başlangıç mesajı yaz', placeholder: 'Başlangıç mesajınızı buraya yazın, değişkenler kullanabilirsiniz, örneğin {{variable}} yazmayı deneyin.', openingQuestion: 'Açılış Soruları', + openingQuestionPlaceholder: 'Değişkenler kullanabilirsiniz, {{variable}} yazmayı deneyin.', noDataPlaceHolder: 'Kullanıcı ile konuşmayı başlatmak, AI\'ın konuşma uygulamalarında onlarla daha yakın bir bağlantı kurmasına yardımcı olabilir.', varTip: 'Değişkenler kullanabilirsiniz, örneğin {{variable}} yazmayı deneyin', diff --git a/web/i18n/uk-UA/app-debug.ts b/web/i18n/uk-UA/app-debug.ts index 7e410ffef9..70bbebe37e 100644 --- a/web/i18n/uk-UA/app-debug.ts +++ b/web/i18n/uk-UA/app-debug.ts @@ -328,6 +328,7 @@ const translation = { writeOpener: 'Напишіть вступне повідомлення', // Write opener placeholder: 'Напишіть тут своє вступне повідомлення, ви можете використовувати змінні, спробуйте ввести {{variable}}.', // Write your opener message here... openingQuestion: 'Відкриваючі питання', // Opening Questions + openingQuestionPlaceholder: 'Ви можете використовувати змінні, спробуйте ввести {{variable}}.', noDataPlaceHolder: 'Початок розмови з користувачем може допомогти ШІ встановити більш тісний зв’язок з ним у розмовних застосунках.', // ... conversational applications. varTip: 'Ви можете використовувати змінні, спробуйте ввести {{variable}}', // You can use variables, try type {{variable}} tooShort: 'Для створення вступних зауважень для розмови потрібно принаймні 20 слів вступного запиту.', // ... are required to generate an opening remarks for the conversation. diff --git a/web/i18n/vi-VN/app-debug.ts b/web/i18n/vi-VN/app-debug.ts index c091cb5abb..cd57f78e79 100644 --- a/web/i18n/vi-VN/app-debug.ts +++ b/web/i18n/vi-VN/app-debug.ts @@ -328,6 +328,7 @@ const translation = { writeOpener: 'Viết câu mở đầu', placeholder: 'Viết thông điệp mở đầu của bạn ở đây, bạn có thể sử dụng biến, hãy thử nhập {{biến}}.', openingQuestion: 'Câu hỏi mở đầu', + openingQuestionPlaceholder: 'Bạn có thể sử dụng biến, hãy thử nhập {{variable}}.', noDataPlaceHolder: 'Bắt đầu cuộc trò chuyện với người dùng có thể giúp AI thiết lập mối quan hệ gần gũi hơn với họ trong các ứng dụng trò chuyện.', varTip: 'Bạn có thể sử dụng biến, hãy thử nhập {{biến}}', tooShort: 'Cần ít nhất 20 từ trong lời nhắc ban đầu để tạo ra các câu mở đầu cho cuộc trò chuyện.', diff --git a/web/i18n/zh-Hans/app-debug.ts b/web/i18n/zh-Hans/app-debug.ts index 4f84b396d0..3728d86e58 100644 --- a/web/i18n/zh-Hans/app-debug.ts +++ b/web/i18n/zh-Hans/app-debug.ts @@ -436,6 +436,7 @@ const translation = { writeOpener: '编写开场白', placeholder: '在这里写下你的开场白,你可以使用变量,尝试输入 {{variable}}。', openingQuestion: '开场问题', + openingQuestionPlaceholder: '可以使用变量,尝试输入 {{variable}}。', noDataPlaceHolder: '在对话型应用中,让 AI 主动说第一段话可以拉近与用户间的距离。', varTip: '你可以使用变量,试试输入 {{variable}}', diff --git a/web/i18n/zh-Hant/app-debug.ts b/web/i18n/zh-Hant/app-debug.ts index 16374992b5..b31b9a9d66 100644 --- a/web/i18n/zh-Hant/app-debug.ts +++ b/web/i18n/zh-Hant/app-debug.ts @@ -313,6 +313,7 @@ const translation = { writeOpener: '編寫開場白', placeholder: '在這裡寫下你的開場白,你可以使用變數,嘗試輸入 {{variable}}。', openingQuestion: '開場問題', + openingQuestionPlaceholder: '可以使用變量,嘗試輸入 {{variable}}。', noDataPlaceHolder: '在對話型應用中,讓 AI 主動說第一段話可以拉近與使用者間的距離。', varTip: '你可以使用變數,試試輸入 {{variable}}', From 2a85f28963b6e055ec0b439704d10d66475c4784 Mon Sep 17 00:00:00 2001 From: Garden12138 <847686279@qq.com> Date: Fri, 11 Jul 2025 15:18:42 +0800 Subject: [PATCH 287/329] =?UTF-8?q?fix=EF=BC=9AFixed=20the=20problem=20of?= =?UTF-8?q?=20plugin=20installation=20failure=20caused=20by=20incons?= =?UTF-8?q?=E2=80=A6=20(#22156)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/services/plugin/plugin_service.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/api/services/plugin/plugin_service.py b/api/services/plugin/plugin_service.py index d7fb4a7c1b..0f22afd8dd 100644 --- a/api/services/plugin/plugin_service.py +++ b/api/services/plugin/plugin_service.py @@ -427,6 +427,9 @@ class PluginService: manager = PluginInstaller() + # collect actual plugin_unique_identifiers + actual_plugin_unique_identifiers = [] + metas = [] features = FeatureService.get_system_features() # check if already downloaded @@ -437,6 +440,8 @@ class PluginService: # check if the plugin is available to install PluginService._check_plugin_installation_scope(plugin_decode_response.verification) # already downloaded, skip + actual_plugin_unique_identifiers.append(plugin_unique_identifier) + metas.append({"plugin_unique_identifier": plugin_unique_identifier}) except Exception: # plugin not installed, download and upload pkg pkg = download_plugin_pkg(plugin_unique_identifier) @@ -447,17 +452,15 @@ class PluginService: ) # check if the plugin is available to install PluginService._check_plugin_installation_scope(response.verification) + # use response plugin_unique_identifier + actual_plugin_unique_identifiers.append(response.unique_identifier) + metas.append({"plugin_unique_identifier": response.unique_identifier}) return manager.install_from_identifiers( tenant_id, - plugin_unique_identifiers, + actual_plugin_unique_identifiers, PluginInstallationSource.Marketplace, - [ - { - "plugin_unique_identifier": plugin_unique_identifier, - } - for plugin_unique_identifier in plugin_unique_identifiers - ], + metas, ) @staticmethod From 1d85979a7464d8d4d01e582eb6083d4206ea46ba Mon Sep 17 00:00:00 2001 From: Joel Date: Fri, 11 Jul 2025 16:41:25 +0800 Subject: [PATCH 288/329] chore:extract last run common logic (#22214) --- .../workflow-app/components/workflow-main.tsx | 8 +- .../components/workflow-app/hooks/index.ts | 2 +- .../hooks/use-inspect-vars-crud.ts | 234 +---------------- .../workflow-app/hooks/use-workflow-run.ts | 9 +- .../hooks/use-fetch-workflow-inspect-vars.ts | 19 +- .../hooks/use-inspect-vars-crud-common.ts | 240 ++++++++++++++++++ 6 files changed, 275 insertions(+), 237 deletions(-) rename web/app/components/{workflow-app => workflow}/hooks/use-fetch-workflow-inspect-vars.ts (85%) create mode 100644 web/app/components/workflow/hooks/use-inspect-vars-crud-common.ts diff --git a/web/app/components/workflow-app/components/workflow-main.tsx b/web/app/components/workflow-app/components/workflow-main.tsx index f0b9bab2cf..d425e6f595 100644 --- a/web/app/components/workflow-app/components/workflow-main.tsx +++ b/web/app/components/workflow-app/components/workflow-main.tsx @@ -15,7 +15,7 @@ import { useWorkflowRun, useWorkflowStartRun, } from '../hooks' -import { useWorkflowStore } from '@/app/components/workflow/store' +import { useStore, useWorkflowStore } from '@/app/components/workflow/store' type WorkflowMainProps = Pick const WorkflowMain = ({ @@ -64,7 +64,11 @@ const WorkflowMain = ({ handleWorkflowStartRunInChatflow, handleWorkflowStartRunInWorkflow, } = useWorkflowStartRun() - const { fetchInspectVars } = useSetWorkflowVarsWithValue() + const appId = useStore(s => s.appId) + const { fetchInspectVars } = useSetWorkflowVarsWithValue({ + flowId: appId, + ...useConfigsMap(), + }) const { hasNodeInspectVars, hasSetInspectVar, diff --git a/web/app/components/workflow-app/hooks/index.ts b/web/app/components/workflow-app/hooks/index.ts index 1ee7c030b9..9e4f94965b 100644 --- a/web/app/components/workflow-app/hooks/index.ts +++ b/web/app/components/workflow-app/hooks/index.ts @@ -5,6 +5,6 @@ export * from './use-workflow-run' export * from './use-workflow-start-run' export * from './use-is-chat-mode' export * from './use-workflow-refresh-draft' -export * from './use-fetch-workflow-inspect-vars' +export * from '../../workflow/hooks/use-fetch-workflow-inspect-vars' export * from './use-inspect-vars-crud' export * from './use-configs-map' diff --git a/web/app/components/workflow-app/hooks/use-inspect-vars-crud.ts b/web/app/components/workflow-app/hooks/use-inspect-vars-crud.ts index ce052b7ed4..109ee85f96 100644 --- a/web/app/components/workflow-app/hooks/use-inspect-vars-crud.ts +++ b/web/app/components/workflow-app/hooks/use-inspect-vars-crud.ts @@ -1,234 +1,16 @@ -import { fetchNodeInspectVars } from '@/service/workflow' -import { useStore, useWorkflowStore } from '@/app/components/workflow/store' -import type { ValueSelector } from '@/app/components/workflow/types' -import type { VarInInspect } from '@/types/workflow' -import { VarInInspectType } from '@/types/workflow' -import { - useDeleteAllInspectorVars, - useDeleteInspectVar, - useDeleteNodeInspectorVars, - useEditInspectorVar, - useInvalidateConversationVarValues, - useInvalidateSysVarValues, - useResetConversationVar, - useResetToLastRunValue, -} from '@/service/use-workflow' -import { useCallback } from 'react' -import { isConversationVar, isENV, isSystemVar } from '@/app/components/workflow/nodes/_base/components/variable/utils' -import produce from 'immer' -import type { Node } from '@/app/components/workflow/types' -import { useNodesInteractionsWithoutSync } from '@/app/components/workflow/hooks/use-nodes-interactions-without-sync' -import { useEdgesInteractionsWithoutSync } from '@/app/components/workflow/hooks/use-edges-interactions-without-sync' +import { useStore } from '@/app/components/workflow/store' +import { useInspectVarsCrudCommon } from '../../workflow/hooks/use-inspect-vars-crud-common' import { useConfigsMap } from './use-configs-map' export const useInspectVarsCrud = () => { - const workflowStore = useWorkflowStore() const appId = useStore(s => s.appId) - const { conversationVarsUrl, systemVarsUrl } = useConfigsMap() - const invalidateConversationVarValues = useInvalidateConversationVarValues(conversationVarsUrl) - const { mutateAsync: doResetConversationVar } = useResetConversationVar(appId) - const { mutateAsync: doResetToLastRunValue } = useResetToLastRunValue(appId) - const invalidateSysVarValues = useInvalidateSysVarValues(systemVarsUrl) - - const { mutateAsync: doDeleteAllInspectorVars } = useDeleteAllInspectorVars(appId) - const { mutate: doDeleteNodeInspectorVars } = useDeleteNodeInspectorVars(appId) - const { mutate: doDeleteInspectVar } = useDeleteInspectVar(appId) - - const { mutateAsync: doEditInspectorVar } = useEditInspectorVar(appId) - const { handleCancelNodeSuccessStatus } = useNodesInteractionsWithoutSync() - const { handleEdgeCancelRunningStatus } = useEdgesInteractionsWithoutSync() - const getNodeInspectVars = useCallback((nodeId: string) => { - const { nodesWithInspectVars } = workflowStore.getState() - const node = nodesWithInspectVars.find(node => node.nodeId === nodeId) - return node - }, [workflowStore]) - - const getVarId = useCallback((nodeId: string, varName: string) => { - const node = getNodeInspectVars(nodeId) - if (!node) - return undefined - const varId = node.vars.find((varItem) => { - return varItem.selector[1] === varName - })?.id - return varId - }, [getNodeInspectVars]) - - const getInspectVar = useCallback((nodeId: string, name: string): VarInInspect | undefined => { - const node = getNodeInspectVars(nodeId) - if (!node) - return undefined - - const variable = node.vars.find((varItem) => { - return varItem.name === name - }) - return variable - }, [getNodeInspectVars]) - - const hasSetInspectVar = useCallback((nodeId: string, name: string, sysVars: VarInInspect[], conversationVars: VarInInspect[]) => { - const isEnv = isENV([nodeId]) - if (isEnv) // always have value - return true - const isSys = isSystemVar([nodeId]) - if (isSys) - return sysVars.some(varItem => varItem.selector?.[1]?.replace('sys.', '') === name) - const isChatVar = isConversationVar([nodeId]) - if (isChatVar) - return conversationVars.some(varItem => varItem.selector?.[1] === name) - return getInspectVar(nodeId, name) !== undefined - }, [getInspectVar]) - - const hasNodeInspectVars = useCallback((nodeId: string) => { - return !!getNodeInspectVars(nodeId) - }, [getNodeInspectVars]) - - const fetchInspectVarValue = useCallback(async (selector: ValueSelector) => { - const { - appId, - setNodeInspectVars, - } = workflowStore.getState() - const nodeId = selector[0] - const isSystemVar = nodeId === 'sys' - const isConversationVar = nodeId === 'conversation' - if (isSystemVar) { - invalidateSysVarValues() - return - } - if (isConversationVar) { - invalidateConversationVarValues() - return - } - const vars = await fetchNodeInspectVars(appId, nodeId) - setNodeInspectVars(nodeId, vars) - }, [workflowStore, invalidateSysVarValues, invalidateConversationVarValues]) - - // after last run would call this - const appendNodeInspectVars = useCallback((nodeId: string, payload: VarInInspect[], allNodes: Node[]) => { - const { - nodesWithInspectVars, - setNodesWithInspectVars, - } = workflowStore.getState() - const nodes = produce(nodesWithInspectVars, (draft) => { - const nodeInfo = allNodes.find(node => node.id === nodeId) - if (nodeInfo) { - const index = draft.findIndex(node => node.nodeId === nodeId) - if (index === -1) { - draft.unshift({ - nodeId, - nodeType: nodeInfo.data.type, - title: nodeInfo.data.title, - vars: payload, - nodePayload: nodeInfo.data, - }) - } - else { - draft[index].vars = payload - // put the node to the topAdd commentMore actions - draft.unshift(draft.splice(index, 1)[0]) - } - } - }) - setNodesWithInspectVars(nodes) - handleCancelNodeSuccessStatus(nodeId) - }, [workflowStore, handleCancelNodeSuccessStatus]) - - const hasNodeInspectVar = useCallback((nodeId: string, varId: string) => { - const { nodesWithInspectVars } = workflowStore.getState() - const targetNode = nodesWithInspectVars.find(item => item.nodeId === nodeId) - if(!targetNode || !targetNode.vars) - return false - return targetNode.vars.some(item => item.id === varId) - }, [workflowStore]) - - const deleteInspectVar = useCallback(async (nodeId: string, varId: string) => { - const { deleteInspectVar } = workflowStore.getState() - if(hasNodeInspectVar(nodeId, varId)) { - await doDeleteInspectVar(varId) - deleteInspectVar(nodeId, varId) - } - }, [doDeleteInspectVar, workflowStore, hasNodeInspectVar]) - - const resetConversationVar = useCallback(async (varId: string) => { - await doResetConversationVar(varId) - invalidateConversationVarValues() - }, [doResetConversationVar, invalidateConversationVarValues]) - - const deleteNodeInspectorVars = useCallback(async (nodeId: string) => { - const { deleteNodeInspectVars } = workflowStore.getState() - if (hasNodeInspectVars(nodeId)) { - await doDeleteNodeInspectorVars(nodeId) - deleteNodeInspectVars(nodeId) - } - }, [doDeleteNodeInspectorVars, workflowStore, hasNodeInspectVars]) - - const deleteAllInspectorVars = useCallback(async () => { - const { deleteAllInspectVars } = workflowStore.getState() - await doDeleteAllInspectorVars() - await invalidateConversationVarValues() - await invalidateSysVarValues() - deleteAllInspectVars() - handleEdgeCancelRunningStatus() - }, [doDeleteAllInspectorVars, invalidateConversationVarValues, invalidateSysVarValues, workflowStore, handleEdgeCancelRunningStatus]) - - const editInspectVarValue = useCallback(async (nodeId: string, varId: string, value: any) => { - const { setInspectVarValue } = workflowStore.getState() - await doEditInspectorVar({ - varId, - value, - }) - setInspectVarValue(nodeId, varId, value) - if (nodeId === VarInInspectType.conversation) - invalidateConversationVarValues() - if (nodeId === VarInInspectType.system) - invalidateSysVarValues() - }, [doEditInspectorVar, invalidateConversationVarValues, invalidateSysVarValues, workflowStore]) - - const renameInspectVarName = useCallback(async (nodeId: string, oldName: string, newName: string) => { - const { renameInspectVarName } = workflowStore.getState() - const varId = getVarId(nodeId, oldName) - if (!varId) - return - - const newSelector = [nodeId, newName] - await doEditInspectorVar({ - varId, - name: newName, - }) - renameInspectVarName(nodeId, varId, newSelector) - }, [doEditInspectorVar, getVarId, workflowStore]) - - const isInspectVarEdited = useCallback((nodeId: string, name: string) => { - const inspectVar = getInspectVar(nodeId, name) - if (!inspectVar) - return false - - return inspectVar.edited - }, [getInspectVar]) - - const resetToLastRunVar = useCallback(async (nodeId: string, varId: string) => { - const { resetToLastRunVar } = workflowStore.getState() - const isSysVar = nodeId === 'sys' - const data = await doResetToLastRunValue(varId) - - if(isSysVar) - invalidateSysVarValues() - else - resetToLastRunVar(nodeId, varId, data.value) - }, [doResetToLastRunValue, invalidateSysVarValues, workflowStore]) + const configsMap = useConfigsMap() + const apis = useInspectVarsCrudCommon({ + flowId: appId, + ...configsMap, + }) return { - hasNodeInspectVars, - hasSetInspectVar, - fetchInspectVarValue, - editInspectVarValue, - renameInspectVarName, - appendNodeInspectVars, - deleteInspectVar, - deleteNodeInspectorVars, - deleteAllInspectorVars, - isInspectVarEdited, - resetToLastRunVar, - invalidateSysVarValues, - resetConversationVar, - invalidateConversationVarValues, + ...apis, } } diff --git a/web/app/components/workflow-app/hooks/use-workflow-run.ts b/web/app/components/workflow-app/hooks/use-workflow-run.ts index 4c34d2ffb1..c303211715 100644 --- a/web/app/components/workflow-app/hooks/use-workflow-run.ts +++ b/web/app/components/workflow-app/hooks/use-workflow-run.ts @@ -20,7 +20,8 @@ import type { VersionHistory } from '@/types/workflow' import { noop } from 'lodash-es' import { useNodesSyncDraft } from './use-nodes-sync-draft' import { useInvalidAllLastRun } from '@/service/use-workflow' -import { useSetWorkflowVarsWithValue } from './use-fetch-workflow-inspect-vars' +import { useSetWorkflowVarsWithValue } from '../../workflow/hooks/use-fetch-workflow-inspect-vars' +import { useConfigsMap } from './use-configs-map' export const useWorkflowRun = () => { const store = useStoreApi() @@ -32,7 +33,11 @@ export const useWorkflowRun = () => { const pathname = usePathname() const appId = useAppStore.getState().appDetail?.id const invalidAllLastRun = useInvalidAllLastRun(appId as string) - const { fetchInspectVars } = useSetWorkflowVarsWithValue() + const configsMap = useConfigsMap() + const { fetchInspectVars } = useSetWorkflowVarsWithValue({ + flowId: appId as string, + ...configsMap, + }) const { handleWorkflowStarted, diff --git a/web/app/components/workflow-app/hooks/use-fetch-workflow-inspect-vars.ts b/web/app/components/workflow/hooks/use-fetch-workflow-inspect-vars.ts similarity index 85% rename from web/app/components/workflow-app/hooks/use-fetch-workflow-inspect-vars.ts rename to web/app/components/workflow/hooks/use-fetch-workflow-inspect-vars.ts index 07580c097e..27a9ea9d2d 100644 --- a/web/app/components/workflow-app/hooks/use-fetch-workflow-inspect-vars.ts +++ b/web/app/components/workflow/hooks/use-fetch-workflow-inspect-vars.ts @@ -6,12 +6,20 @@ import type { Node } from '@/app/components/workflow/types' import { fetchAllInspectVars } from '@/service/workflow' import { useInvalidateConversationVarValues, useInvalidateSysVarValues } from '@/service/use-workflow' import { useNodesInteractionsWithoutSync } from '@/app/components/workflow/hooks/use-nodes-interactions-without-sync' -import { useConfigsMap } from './use-configs-map' -export const useSetWorkflowVarsWithValue = () => { +type Params = { + flowId: string + conversationVarsUrl: string + systemVarsUrl: string +} + +export const useSetWorkflowVarsWithValue = ({ + flowId, + conversationVarsUrl, + systemVarsUrl, +}: Params) => { const workflowStore = useWorkflowStore() const store = useStoreApi() - const { conversationVarsUrl, systemVarsUrl } = useConfigsMap() const invalidateConversationVarValues = useInvalidateConversationVarValues(conversationVarsUrl) const invalidateSysVarValues = useInvalidateSysVarValues(systemVarsUrl) const { handleCancelAllNodeSuccessStatus } = useNodesInteractionsWithoutSync() @@ -58,13 +66,12 @@ export const useSetWorkflowVarsWithValue = () => { }, [workflowStore, store]) const fetchInspectVars = useCallback(async () => { - const { appId } = workflowStore.getState() invalidateConversationVarValues() invalidateSysVarValues() - const data = await fetchAllInspectVars(appId) + const data = await fetchAllInspectVars(flowId) setInspectVarsToStore(data) handleCancelAllNodeSuccessStatus() // to make sure clear node output show the unset status - }, [workflowStore, invalidateConversationVarValues, invalidateSysVarValues, setInspectVarsToStore, handleCancelAllNodeSuccessStatus]) + }, [invalidateConversationVarValues, invalidateSysVarValues, flowId, setInspectVarsToStore, handleCancelAllNodeSuccessStatus]) return { fetchInspectVars, } diff --git a/web/app/components/workflow/hooks/use-inspect-vars-crud-common.ts b/web/app/components/workflow/hooks/use-inspect-vars-crud-common.ts new file mode 100644 index 0000000000..ffcfd81666 --- /dev/null +++ b/web/app/components/workflow/hooks/use-inspect-vars-crud-common.ts @@ -0,0 +1,240 @@ +import { fetchNodeInspectVars } from '@/service/workflow' +import { useWorkflowStore } from '@/app/components/workflow/store' +import type { ValueSelector } from '@/app/components/workflow/types' +import type { VarInInspect } from '@/types/workflow' +import { VarInInspectType } from '@/types/workflow' +import { + useDeleteAllInspectorVars, + useDeleteInspectVar, + useDeleteNodeInspectorVars, + useEditInspectorVar, + useInvalidateConversationVarValues, + useInvalidateSysVarValues, + useResetConversationVar, + useResetToLastRunValue, +} from '@/service/use-workflow' +import { useCallback } from 'react' +import { isConversationVar, isENV, isSystemVar } from '@/app/components/workflow/nodes/_base/components/variable/utils' +import produce from 'immer' +import type { Node } from '@/app/components/workflow/types' +import { useNodesInteractionsWithoutSync } from '@/app/components/workflow/hooks/use-nodes-interactions-without-sync' +import { useEdgesInteractionsWithoutSync } from '@/app/components/workflow/hooks/use-edges-interactions-without-sync' + +type Params = { + flowId: string + conversationVarsUrl: string + systemVarsUrl: string +} +export const useInspectVarsCrudCommon = ({ + flowId, + conversationVarsUrl, + systemVarsUrl, +}: Params) => { + const workflowStore = useWorkflowStore() + const invalidateConversationVarValues = useInvalidateConversationVarValues(conversationVarsUrl!) + const { mutateAsync: doResetConversationVar } = useResetConversationVar(flowId) + const { mutateAsync: doResetToLastRunValue } = useResetToLastRunValue(flowId) + const invalidateSysVarValues = useInvalidateSysVarValues(systemVarsUrl!) + + const { mutateAsync: doDeleteAllInspectorVars } = useDeleteAllInspectorVars(flowId) + const { mutate: doDeleteNodeInspectorVars } = useDeleteNodeInspectorVars(flowId) + const { mutate: doDeleteInspectVar } = useDeleteInspectVar(flowId) + + const { mutateAsync: doEditInspectorVar } = useEditInspectorVar(flowId) + const { handleCancelNodeSuccessStatus } = useNodesInteractionsWithoutSync() + const { handleEdgeCancelRunningStatus } = useEdgesInteractionsWithoutSync() + const getNodeInspectVars = useCallback((nodeId: string) => { + const { nodesWithInspectVars } = workflowStore.getState() + const node = nodesWithInspectVars.find(node => node.nodeId === nodeId) + return node + }, [workflowStore]) + + const getVarId = useCallback((nodeId: string, varName: string) => { + const node = getNodeInspectVars(nodeId) + if (!node) + return undefined + const varId = node.vars.find((varItem) => { + return varItem.selector[1] === varName + })?.id + return varId + }, [getNodeInspectVars]) + + const getInspectVar = useCallback((nodeId: string, name: string): VarInInspect | undefined => { + const node = getNodeInspectVars(nodeId) + if (!node) + return undefined + + const variable = node.vars.find((varItem) => { + return varItem.name === name + }) + return variable + }, [getNodeInspectVars]) + + const hasSetInspectVar = useCallback((nodeId: string, name: string, sysVars: VarInInspect[], conversationVars: VarInInspect[]) => { + const isEnv = isENV([nodeId]) + if (isEnv) // always have value + return true + const isSys = isSystemVar([nodeId]) + if (isSys) + return sysVars.some(varItem => varItem.selector?.[1]?.replace('sys.', '') === name) + const isChatVar = isConversationVar([nodeId]) + if (isChatVar) + return conversationVars.some(varItem => varItem.selector?.[1] === name) + return getInspectVar(nodeId, name) !== undefined + }, [getInspectVar]) + + const hasNodeInspectVars = useCallback((nodeId: string) => { + return !!getNodeInspectVars(nodeId) + }, [getNodeInspectVars]) + + const fetchInspectVarValue = useCallback(async (selector: ValueSelector) => { + const { + appId, + setNodeInspectVars, + } = workflowStore.getState() + const nodeId = selector[0] + const isSystemVar = nodeId === 'sys' + const isConversationVar = nodeId === 'conversation' + if (isSystemVar) { + invalidateSysVarValues() + return + } + if (isConversationVar) { + invalidateConversationVarValues() + return + } + const vars = await fetchNodeInspectVars(appId, nodeId) + setNodeInspectVars(nodeId, vars) + }, [workflowStore, invalidateSysVarValues, invalidateConversationVarValues]) + + // after last run would call this + const appendNodeInspectVars = useCallback((nodeId: string, payload: VarInInspect[], allNodes: Node[]) => { + const { + nodesWithInspectVars, + setNodesWithInspectVars, + } = workflowStore.getState() + const nodes = produce(nodesWithInspectVars, (draft) => { + const nodeInfo = allNodes.find(node => node.id === nodeId) + if (nodeInfo) { + const index = draft.findIndex(node => node.nodeId === nodeId) + if (index === -1) { + draft.unshift({ + nodeId, + nodeType: nodeInfo.data.type, + title: nodeInfo.data.title, + vars: payload, + nodePayload: nodeInfo.data, + }) + } + else { + draft[index].vars = payload + // put the node to the topAdd commentMore actions + draft.unshift(draft.splice(index, 1)[0]) + } + } + }) + setNodesWithInspectVars(nodes) + handleCancelNodeSuccessStatus(nodeId) + }, [workflowStore, handleCancelNodeSuccessStatus]) + + const hasNodeInspectVar = useCallback((nodeId: string, varId: string) => { + const { nodesWithInspectVars } = workflowStore.getState() + const targetNode = nodesWithInspectVars.find(item => item.nodeId === nodeId) + if(!targetNode || !targetNode.vars) + return false + return targetNode.vars.some(item => item.id === varId) + }, [workflowStore]) + + const deleteInspectVar = useCallback(async (nodeId: string, varId: string) => { + const { deleteInspectVar } = workflowStore.getState() + if(hasNodeInspectVar(nodeId, varId)) { + await doDeleteInspectVar(varId) + deleteInspectVar(nodeId, varId) + } + }, [doDeleteInspectVar, workflowStore, hasNodeInspectVar]) + + const resetConversationVar = useCallback(async (varId: string) => { + await doResetConversationVar(varId) + invalidateConversationVarValues() + }, [doResetConversationVar, invalidateConversationVarValues]) + + const deleteNodeInspectorVars = useCallback(async (nodeId: string) => { + const { deleteNodeInspectVars } = workflowStore.getState() + if (hasNodeInspectVars(nodeId)) { + await doDeleteNodeInspectorVars(nodeId) + deleteNodeInspectVars(nodeId) + } + }, [doDeleteNodeInspectorVars, workflowStore, hasNodeInspectVars]) + + const deleteAllInspectorVars = useCallback(async () => { + const { deleteAllInspectVars } = workflowStore.getState() + await doDeleteAllInspectorVars() + await invalidateConversationVarValues() + await invalidateSysVarValues() + deleteAllInspectVars() + handleEdgeCancelRunningStatus() + }, [doDeleteAllInspectorVars, invalidateConversationVarValues, invalidateSysVarValues, workflowStore, handleEdgeCancelRunningStatus]) + + const editInspectVarValue = useCallback(async (nodeId: string, varId: string, value: any) => { + const { setInspectVarValue } = workflowStore.getState() + await doEditInspectorVar({ + varId, + value, + }) + setInspectVarValue(nodeId, varId, value) + if (nodeId === VarInInspectType.conversation) + invalidateConversationVarValues() + if (nodeId === VarInInspectType.system) + invalidateSysVarValues() + }, [doEditInspectorVar, invalidateConversationVarValues, invalidateSysVarValues, workflowStore]) + + const renameInspectVarName = useCallback(async (nodeId: string, oldName: string, newName: string) => { + const { renameInspectVarName } = workflowStore.getState() + const varId = getVarId(nodeId, oldName) + if (!varId) + return + + const newSelector = [nodeId, newName] + await doEditInspectorVar({ + varId, + name: newName, + }) + renameInspectVarName(nodeId, varId, newSelector) + }, [doEditInspectorVar, getVarId, workflowStore]) + + const isInspectVarEdited = useCallback((nodeId: string, name: string) => { + const inspectVar = getInspectVar(nodeId, name) + if (!inspectVar) + return false + + return inspectVar.edited + }, [getInspectVar]) + + const resetToLastRunVar = useCallback(async (nodeId: string, varId: string) => { + const { resetToLastRunVar } = workflowStore.getState() + const isSysVar = nodeId === 'sys' + const data = await doResetToLastRunValue(varId) + + if(isSysVar) + invalidateSysVarValues() + else + resetToLastRunVar(nodeId, varId, data.value) + }, [doResetToLastRunValue, invalidateSysVarValues, workflowStore]) + + return { + hasNodeInspectVars, + hasSetInspectVar, + fetchInspectVarValue, + editInspectVarValue, + renameInspectVarName, + appendNodeInspectVars, + deleteInspectVar, + deleteNodeInspectorVars, + deleteAllInspectorVars, + isInspectVarEdited, + resetToLastRunVar, + invalidateSysVarValues, + resetConversationVar, + invalidateConversationVarValues, + } +} From 2d3c5b3b7c3365a5e037e85dbbb9ced174a51862 Mon Sep 17 00:00:00 2001 From: HyaCinth <88471803+HyaCiovo@users.noreply.github.com> Date: Fri, 11 Jul 2025 17:52:16 +0800 Subject: [PATCH 289/329] fix(emoji-picker): Adjust the style of the emoji picker (#22161) (#22231) --- web/app/components/base/emoji-picker/Inner.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/web/app/components/base/emoji-picker/Inner.tsx b/web/app/components/base/emoji-picker/Inner.tsx index 8d05967f33..6fc4b67181 100644 --- a/web/app/components/base/emoji-picker/Inner.tsx +++ b/web/app/components/base/emoji-picker/Inner.tsx @@ -101,7 +101,7 @@ const EmojiPickerInner: FC = ({
-
+
{isSearching && <>

Search

@@ -170,7 +170,7 @@ const EmojiPickerInner: FC = ({ 'flex h-8 w-8 items-center justify-center rounded-lg p-1', ) } style={{ background: color }}> - {selectedEmoji !== '' && } + {selectedEmoji !== '' && }
})} From 76d21743fd9f164bbca1e97f224246750b5e83d3 Mon Sep 17 00:00:00 2001 From: HyaCinth <88471803+HyaCiovo@users.noreply.github.com> Date: Fri, 11 Jul 2025 17:54:09 +0800 Subject: [PATCH 290/329] fix(web): Optimize AppInfo Component Layout (#22212) (#22218) --- web/app/components/app-sidebar/app-info.tsx | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/web/app/components/app-sidebar/app-info.tsx b/web/app/components/app-sidebar/app-info.tsx index c28cc20df5..3817ebf5a4 100644 --- a/web/app/components/app-sidebar/app-info.tsx +++ b/web/app/components/app-sidebar/app-info.tsx @@ -308,13 +308,11 @@ const AppInfo = ({ expand, onlyShowDetail = false, openState = false, onDetailEx operations={operations} />
-
- -
+