From cf8ccb763d23c8a000def3808b9e05e2b934e5b8 Mon Sep 17 00:00:00 2001 From: Nixieboluo Date: Thu, 3 Sep 2026 14:33:47 +0800 Subject: [PATCH 01/15] refactor(workflow): remove tool desc from basic nodes Signed-off-by: Nixieboluo --- .../workflow/migration/legacy/structure.ts | 1 - packages/global/core/workflow/runtime/type.ts | 1 - .../global/core/workflow/runtime/utils.ts | 1 - packages/global/core/workflow/type/node.ts | 1 - packages/global/openapi/core/workflow/node.ts | 3 --- .../core/workflow/migration/schema.test.ts | 27 +++++++++++++++++++ .../test/core/workflow/runtime/utils.test.ts | 2 -- .../service/core/app/tool/utils/client.ts | 2 -- packages/service/core/app/utils.ts | 1 - .../dispatch/ai/agent/sub/tool/utils.ts | 7 +---- .../ai/toolcall/hooks/useToolCatalog.ts | 2 +- .../ai/toolcall/hooks/useToolNodeList.ts | 1 - .../workflow/dispatch/ai/toolcall/type.ts | 1 - packages/service/core/workflow/utils/index.ts | 2 -- .../dispatch/ai/toolcall/toolCall.test.ts | 23 +++++++++++++++- .../test/core/workflow/utils/index.test.ts | 5 +--- .../app/detail/Edit/SimpleApp/utils.ts | 1 - .../Flow/nodes/render/NodeCard.tsx | 1 - .../app/detail/WorkflowComponents/utils.ts | 1 - projects/app/src/pages/api/core/app/create.ts | 11 ++++++-- .../detail/WorkflowComponents/utils.test.ts | 3 +-- 21 files changed, 62 insertions(+), 35 deletions(-) diff --git a/packages/global/core/workflow/migration/legacy/structure.ts b/packages/global/core/workflow/migration/legacy/structure.ts index ef23e07954e1..9d6267dffc76 100644 --- a/packages/global/core/workflow/migration/legacy/structure.ts +++ b/packages/global/core/workflow/migration/legacy/structure.ts @@ -19,7 +19,6 @@ const optionalNodeFields = [ 'avatar', 'avatarLinear', 'intro', - 'toolDescription', 'version', 'versionLabel', 'pluginId', diff --git a/packages/global/core/workflow/runtime/type.ts b/packages/global/core/workflow/runtime/type.ts index 26873c09ba76..4a522582a3ca 100644 --- a/packages/global/core/workflow/runtime/type.ts +++ b/packages/global/core/workflow/runtime/type.ts @@ -18,7 +18,6 @@ export type RuntimeNodeItemType = { name: StoreNodeItemType['name']; avatar?: StoreNodeItemType['avatar']; intro?: StoreNodeItemType['intro']; - toolDescription?: StoreNodeItemType['toolDescription']; flowNodeType: StoreNodeItemType['flowNodeType']; showStatus?: StoreNodeItemType['showStatus']; isEntry?: boolean; diff --git a/packages/global/core/workflow/runtime/utils.ts b/packages/global/core/workflow/runtime/utils.ts index 0ec19d2beb43..e64dedac0f09 100644 --- a/packages/global/core/workflow/runtime/utils.ts +++ b/packages/global/core/workflow/runtime/utils.ts @@ -277,7 +277,6 @@ export const storeNodes2RuntimeNodes = ( name: node.name, avatar: node.avatar, intro: node.intro, - toolDescription: node.toolDescription, flowNodeType: node.flowNodeType, showStatus: node.showStatus, isEntry: entryNodeIds.includes(node.nodeId), diff --git a/packages/global/core/workflow/type/node.ts b/packages/global/core/workflow/type/node.ts index 64d62a50f666..30ef9d763b0c 100644 --- a/packages/global/core/workflow/type/node.ts +++ b/packages/global/core/workflow/type/node.ts @@ -179,7 +179,6 @@ export const FlowNodeCommonTypeSchema = z.object({ colorSchema: z.enum(NodeColorSchemaEnum).optional(), // color schema name: z.string(), // name intro: z.string().optional(), // template list intro - toolDescription: z.string().optional(), // tool description showStatus: BoolSchema.optional(), // chatting response step status version: z.string().optional(), // version diff --git a/packages/global/openapi/core/workflow/node.ts b/packages/global/openapi/core/workflow/node.ts index 5013c4632a62..904b9c9e9e79 100644 --- a/packages/global/openapi/core/workflow/node.ts +++ b/packages/global/openapi/core/workflow/node.ts @@ -130,9 +130,6 @@ export const OpenAPIStoreNodeItemTypeSchema = StoreNodeItemTypeSchema.omit({ intro: z.string().optional().meta({ description: '节点简介' }), - toolDescription: z.string().optional().meta({ - description: '节点作为工具被调用时的能力说明' - }), showStatus: BoolSchema.optional().meta({ description: '对话运行时是否展示该节点执行状态' }), diff --git a/packages/global/test/core/workflow/migration/schema.test.ts b/packages/global/test/core/workflow/migration/schema.test.ts index 3e5499d88529..385e6dd7fba6 100644 --- a/packages/global/test/core/workflow/migration/schema.test.ts +++ b/packages/global/test/core/workflow/migration/schema.test.ts @@ -82,6 +82,33 @@ describe('workflow migration boundary', () => { expect(result.nodes[0].inputs[1]).not.toHaveProperty('isToolParam'); }); + it('drops legacy node descriptions while keeping input descriptions', async () => { + const result = await migrateWorkflowToCurrent({ + nodes: [ + { + nodeId: 'chat-1', + flowNodeType: 'chatNode', + name: 'Chat', + intro: 'Node intro', + toolDescription: 'Legacy node description', + inputs: [ + { + key: 'query', + label: 'Query', + renderTypeList: [FlowNodeInputTypeEnum.input], + toolDescription: 'Input description' + } + ], + outputs: [] + } + ] + }); + + expect(result.nodes[0]).toMatchObject({ intro: 'Node intro' }); + expect(result.nodes[0]).not.toHaveProperty('toolDescription'); + expect(result.nodes[0].inputs[0].toolDescription).toBe('Input description'); + }); + it('keeps legacy file inputs manual', async () => { const result = await migrateWorkflowToCurrent({ nodes: [ diff --git a/packages/global/test/core/workflow/runtime/utils.test.ts b/packages/global/test/core/workflow/runtime/utils.test.ts index a77bca6151ad..fa25a10752a3 100644 --- a/packages/global/test/core/workflow/runtime/utils.test.ts +++ b/packages/global/test/core/workflow/runtime/utils.test.ts @@ -1177,7 +1177,6 @@ describe('storeNodes2RuntimeNodes', () => { name: 'Test Node', avatar: 'avatar.png', intro: 'Test intro', - toolDescription: 'Tool desc', flowNodeType: FlowNodeTypeEnum.chatNode, showStatus: true, inputs: [{ key: 'input1', label: '', renderTypeList: [], value: 'val1' }], @@ -1200,7 +1199,6 @@ describe('storeNodes2RuntimeNodes', () => { name: 'Test Node', avatar: 'avatar.png', intro: 'Test intro', - toolDescription: 'Tool desc', flowNodeType: FlowNodeTypeEnum.chatNode, showStatus: true, isEntry: true, diff --git a/packages/service/core/app/tool/utils/client.ts b/packages/service/core/app/tool/utils/client.ts index a6bfc6365d08..a117fdc66493 100644 --- a/packages/service/core/app/tool/utils/client.ts +++ b/packages/service/core/app/tool/utils/client.ts @@ -184,7 +184,6 @@ export async function getClientSystemToolPreviewNode({ avatar: toolDetail.avatar, name: toolDetail.name, intro: toolDetail.intro, - toolDescription: toolDetail.toolDescription, courseUrl: toolDetail.courseUrl, readmeUrl: toolDetail.readmeUrl, userGuide: toolDetail.userGuide ?? undefined, @@ -506,7 +505,6 @@ export async function getClientToolPreviewNode({ avatar: app.avatar, name: parseI18nString(app.name, lang), intro: parseI18nString(app.intro, lang), - toolDescription: app.toolDescription, courseUrl: app.courseUrl, userGuide: app.userGuide, showStatus: true, diff --git a/packages/service/core/app/utils.ts b/packages/service/core/app/utils.ts index acb129ce18af..6b2a8809eed3 100644 --- a/packages/service/core/app/utils.ts +++ b/packages/service/core/app/utils.ts @@ -258,7 +258,6 @@ export async function rewriteAppWorkflowToDetail({ node.hasSystemSecret = preview.hasSystemSecret; node.toolConfig = preview.toolConfig; - node.toolDescription = preview.toolDescription; // Latest version if (!node.version) { diff --git a/packages/service/core/workflow/dispatch/ai/agent/sub/tool/utils.ts b/packages/service/core/workflow/dispatch/ai/agent/sub/tool/utils.ts index d5016ba7e674..069b755dfa6d 100644 --- a/packages/service/core/workflow/dispatch/ai/agent/sub/tool/utils.ts +++ b/packages/service/core/workflow/dispatch/ai/agent/sub/tool/utils.ts @@ -212,7 +212,6 @@ export const getAgentRuntimeTools = async ({ avatar: toolDetail.avatar, name: toolDetail.name, intro: toolDetail.intro, - toolDescription: toolDetail.toolDescription, version: versionId ?? '', inputs, outputs: appendErrorOutput(schemaOutputs), @@ -473,7 +472,6 @@ export const getAgentRuntimeTools = async ({ toolId, inputs, name, - toolDescription, intro, jsonSchema, fixedInputBindings @@ -481,12 +479,11 @@ export const getAgentRuntimeTools = async ({ toolId: string; inputs: FlowNodeInputItemType[]; name: string; - toolDescription?: string; intro?: string; jsonSchema?: JSONSchemaInputType; fixedInputBindings?: Record; }) => { - const description = [name, toolDescription || intro].filter(Boolean).join(': '); + const description = [name, intro].filter(Boolean).join(': '); // 仅数字开头的工具名需要补前缀,避免破坏 runtime 使用原始 tool id 反查工具。 const formatToolId = /^\d/.test(toolId) ? `t${toolId}` : toolId; @@ -689,7 +686,6 @@ export const getAgentRuntimeTools = async ({ toolId: runtimeId, inputs, name: child.name, - toolDescription: child.toolDescription, intro: child.intro, jsonSchema: child.jsonSchema, fixedInputBindings: filterToolConfiguredParams({ params: configuredParams, inputs }) @@ -793,7 +789,6 @@ export const getAgentRuntimeTools = async ({ toolId: runtimeToolId, inputs, name: toolNode.name, - toolDescription: toolNode.toolDescription, intro: toolNode.intro, jsonSchema: toolNode.jsonSchema, fixedInputBindings: filterToolConfiguredParams({ params: configuredParams, inputs }) diff --git a/packages/service/core/workflow/dispatch/ai/toolcall/hooks/useToolCatalog.ts b/packages/service/core/workflow/dispatch/ai/toolcall/hooks/useToolCatalog.ts index bf8e9cca6a82..767f51c1d958 100644 --- a/packages/service/core/workflow/dispatch/ai/toolcall/hooks/useToolCatalog.ts +++ b/packages/service/core/workflow/dispatch/ai/toolcall/hooks/useToolCatalog.ts @@ -18,7 +18,7 @@ export const createToolSchema = (item: ToolNodeItemType): ChatCompletionTool => compileToolRuntime({ toolId: item.nodeId, name: item.name, - description: item.toolDescription || item.intro, + description: item.intro, inputs: item.inputs, jsonSchema: item.jsonSchema }).modelTool; diff --git a/packages/service/core/workflow/dispatch/ai/toolcall/hooks/useToolNodeList.ts b/packages/service/core/workflow/dispatch/ai/toolcall/hooks/useToolNodeList.ts index 0f6bf044c229..0ca496d8f5e4 100644 --- a/packages/service/core/workflow/dispatch/ai/toolcall/hooks/useToolNodeList.ts +++ b/packages/service/core/workflow/dispatch/ai/toolcall/hooks/useToolNodeList.ts @@ -49,7 +49,6 @@ export const useToolNodeList = ({ flowNodeType: tool.flowNodeType, avatar: tool.avatar, intro: tool.intro, - toolDescription: tool.toolDescription, jsonSchema, inputs }; diff --git a/packages/service/core/workflow/dispatch/ai/toolcall/type.ts b/packages/service/core/workflow/dispatch/ai/toolcall/type.ts index ceddedb3bdce..289a3dfa1aad 100644 --- a/packages/service/core/workflow/dispatch/ai/toolcall/type.ts +++ b/packages/service/core/workflow/dispatch/ai/toolcall/type.ts @@ -48,7 +48,6 @@ export type ToolNodeItemType = { name: RuntimeNodeItemType['name']; avatar?: RuntimeNodeItemType['avatar']; intro?: RuntimeNodeItemType['intro']; - toolDescription?: RuntimeNodeItemType['toolDescription']; flowNodeType: RuntimeNodeItemType['flowNodeType']; jsonSchema?: JSONSchemaInputType; diff --git a/packages/service/core/workflow/utils/index.ts b/packages/service/core/workflow/utils/index.ts index a893bd0c9e78..6b70f9be7069 100644 --- a/packages/service/core/workflow/utils/index.ts +++ b/packages/service/core/workflow/utils/index.ts @@ -99,7 +99,6 @@ export async function getSystemToolRunTimeNodeFromSystemToolset({ const pluginId = `${systemToolId}/${child.id}`; const intro = selectedTool.description || child.description; - const toolDescription = selectedTool.description || child.toolDescription || child.description; const childInputs = jsonSchema2NodeInput({ jsonSchema: child.inputSchema, schemaType: 'systemTool' @@ -116,7 +115,6 @@ export async function getSystemToolRunTimeNodeFromSystemToolset({ nodeId: `${toolSetNode.nodeId}${child.id}`, version: runtimeVersion, jsonSchema: child.inputSchema, - toolDescription, toolConfig: { systemTool: { toolId: pluginId, diff --git a/packages/service/test/core/workflow/dispatch/ai/toolcall/toolCall.test.ts b/packages/service/test/core/workflow/dispatch/ai/toolcall/toolCall.test.ts index cbf805c01937..a68665bd88a6 100644 --- a/packages/service/test/core/workflow/dispatch/ai/toolcall/toolCall.test.ts +++ b/packages/service/test/core/workflow/dispatch/ai/toolcall/toolCall.test.ts @@ -3,6 +3,7 @@ import { SANDBOX_SHELL_TOOL_NAME } from '@fastgpt/global/core/ai/sandbox/tools'; import { NodeInputKeyEnum } from '@fastgpt/global/core/workflow/constants'; import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant'; import { AgentUsageModuleName } from '@fastgpt/service/core/ai/llm/agentLoop/interface'; +import { createToolSchema } from '@fastgpt/service/core/workflow/dispatch/ai/toolcall/hooks/useToolCatalog'; import { runToolCall as runToolCallWithoutContext } from '@fastgpt/service/core/workflow/dispatch/ai/toolcall/toolCall'; import { runWithContext } from '@fastgpt/service/core/workflow/utils/context'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; @@ -155,6 +156,26 @@ const createLoopResult = ({ requestIds: ['req_main'] }); +describe('createToolSchema', () => { + it('uses node intro as the function description', () => { + const tool = createToolSchema({ + nodeId: 'search', + name: 'Search', + intro: 'Search data', + flowNodeType: FlowNodeTypeEnum.tool, + inputs: [] + }); + + expect(tool).toMatchObject({ + type: 'function', + function: { + name: 'search', + description: 'Search: Search data' + } + }); + }); +}); + describe('runToolCall compression node responses', () => { beforeEach(() => { vi.clearAllMocks(); @@ -288,7 +309,7 @@ describe('runToolCall compression node responses', () => { name: 'Search', flowNodeType: FlowNodeTypeEnum.tool, avatar: 'tool-avatar', - toolDescription: 'Search data', + intro: 'Search data', inputs: [] } ] diff --git a/packages/service/test/core/workflow/utils/index.test.ts b/packages/service/test/core/workflow/utils/index.test.ts index 2b4b5c671cdf..43121ab8fdf9 100644 --- a/packages/service/test/core/workflow/utils/index.test.ts +++ b/packages/service/test/core/workflow/utils/index.test.ts @@ -255,7 +255,6 @@ describe('getSystemToolRunTimeNodeFromSystemToolset', () => { expect(result[0].nodeId).toBe('node-1child-1'); expect(result[0].name).toBe('Custom Name'); expect(result[0].intro).toBe('Custom Desc'); - expect(result[0].toolDescription).toBe('Custom Desc'); expect(result[0].toolConfig).toEqual({ systemTool: { toolId: 'systemTool-toolset-1/child-1' } }); @@ -317,8 +316,7 @@ describe('getSystemToolRunTimeNodeFromSystemToolset', () => { { id: 'child-1', name: 'Original Name', - description: 'Original Intro', - toolDescription: 'Original Tool Description' + description: 'Original Intro' } ]); @@ -330,7 +328,6 @@ describe('getSystemToolRunTimeNodeFromSystemToolset', () => { expect(result).toHaveLength(1); expect(result[0].name).toBe('Original Name'); expect(result[0].intro).toBe('Original Intro'); - expect(result[0].toolDescription).toBe('Original Tool Description'); }); it('should pass systemInputConfig value to child tool inputs', async () => { diff --git a/projects/app/src/pageComponents/app/detail/Edit/SimpleApp/utils.ts b/projects/app/src/pageComponents/app/detail/Edit/SimpleApp/utils.ts index ba7cc684d699..4570c7041455 100644 --- a/projects/app/src/pageComponents/app/detail/Edit/SimpleApp/utils.ts +++ b/projects/app/src/pageComponents/app/detail/Edit/SimpleApp/utils.ts @@ -664,7 +664,6 @@ export function form2AppWorkflow( source: tool.source, name: tool.name, intro: tool.intro, - toolDescription: tool.toolDescription, avatar: tool.avatar, flowNodeType: tool.flowNodeType, showStatus: tool.showStatus, diff --git a/projects/app/src/pageComponents/app/detail/WorkflowComponents/Flow/nodes/render/NodeCard.tsx b/projects/app/src/pageComponents/app/detail/WorkflowComponents/Flow/nodes/render/NodeCard.tsx index a0de1ccadaa6..205c4d29b89f 100644 --- a/projects/app/src/pageComponents/app/detail/WorkflowComponents/Flow/nodes/render/NodeCard.tsx +++ b/projects/app/src/pageComponents/app/detail/WorkflowComponents/Flow/nodes/render/NodeCard.tsx @@ -919,7 +919,6 @@ const MenuRender = React.memo(function MenuRender({ pluginId: node.data.pluginId }), intro: node.data.intro, - toolDescription: node.data.toolDescription, showStatus: node.data.showStatus, version: node.data.version, diff --git a/projects/app/src/pageComponents/app/detail/WorkflowComponents/utils.ts b/projects/app/src/pageComponents/app/detail/WorkflowComponents/utils.ts index 32be053bec92..ab62f39d0f2f 100644 --- a/projects/app/src/pageComponents/app/detail/WorkflowComponents/utils.ts +++ b/projects/app/src/pageComponents/app/detail/WorkflowComponents/utils.ts @@ -93,7 +93,6 @@ export const uiWorkflow2StoreWorkflow = ({ parentNodeId: item.data.parentNodeId, name: item.data.name, intro: item.data.intro, - toolDescription: item.data.toolDescription, avatar: item.data.avatar, flowNodeType: item.data.flowNodeType, showStatus: item.data.showStatus, diff --git a/projects/app/src/pages/api/core/app/create.ts b/projects/app/src/pages/api/core/app/create.ts index bd5046ded38d..81c5130e0bcf 100644 --- a/projects/app/src/pages/api/core/app/create.ts +++ b/projects/app/src/pages/api/core/app/create.ts @@ -190,6 +190,13 @@ export const onCreateApp = async ({ }); } + // 工具集节点可能已编码 JSON Schema;只清理旧节点字段,保留嵌套 schema 的存储格式。 + const storageNodes = storageModules?.map((node) => { + const storageNode = { ...node } as typeof node & { toolDescription?: unknown }; + delete storageNode.toolDescription; + return storageNode; + }); + const create = async (session: ClientSession) => { const resourceRefs = extractAppResourceRefsFromNodes(normalizedWorkflow.nodes); const _avatar = await (async () => { @@ -220,7 +227,7 @@ export const onCreateApp = async ({ intro, teamId, tmbId, - modules: storageModules ?? normalizedWorkflow.nodes, + modules: storageNodes ?? normalizedWorkflow.nodes, edges: normalizedWorkflow.edges, chatConfig: normalizedWorkflow.chatConfig, type, @@ -241,7 +248,7 @@ export const onCreateApp = async ({ { tmbId, appId, - nodes: storageModules ?? normalizedWorkflow.nodes, + nodes: storageNodes ?? normalizedWorkflow.nodes, edges: normalizedWorkflow.edges, chatConfig: normalizedWorkflow.chatConfig, versionName: name, diff --git a/projects/app/test/pageComponents/app/detail/WorkflowComponents/utils.test.ts b/projects/app/test/pageComponents/app/detail/WorkflowComponents/utils.test.ts index 09761f2c3ca8..1ec90e750421 100644 --- a/projects/app/test/pageComponents/app/detail/WorkflowComponents/utils.test.ts +++ b/projects/app/test/pageComponents/app/detail/WorkflowComponents/utils.test.ts @@ -33,7 +33,6 @@ describe('WorkflowComponents utils', () => { parentNodeId: 'parent1', name: 'Node 1', intro: 'Intro 1', - toolDescription: 'Tool desc', avatar: 'avatar1', flowNodeType: FlowNodeTypeEnum.userInput, showStatus: true, @@ -78,11 +77,11 @@ describe('WorkflowComponents utils', () => { parentNodeId: 'parent1', name: 'Node 1', intro: 'Intro 1', - toolDescription: 'Tool desc', avatar: 'avatar1', flowNodeType: FlowNodeTypeEnum.userInput, position: { x: 100, y: 100 } }); + expect(result.nodes[0]).not.toHaveProperty('toolDescription'); expect(result.edges).toHaveLength(1); expect(result.edges[0]).toMatchObject({ From 9cb463f097a69972585650a3b2714d4f9cf1291d Mon Sep 17 00:00:00 2001 From: Nixieboluo Date: Thu, 3 Sep 2026 14:34:17 +0800 Subject: [PATCH 02/15] refactor(workflow): remove tool desc from system tools Signed-off-by: Nixieboluo --- .../global/core/app/tool/systemTool/codec.ts | 13 +----- .../core/app/tool/systemTool/type/base.ts | 2 - packages/global/core/plugin/tool/type.ts | 1 - packages/global/openapi/core/app/tool/api.ts | 6 +-- .../core/app/tool/systemTool/type.test.ts | 2 +- .../core/app/tool/systemToolConfig.test.ts | 19 ++++++++- .../app/tool/systemTool/systemTool.repo.ts | 18 +------- .../service/core/plugin/teamPluginPolicy.ts | 1 - .../tool/systemTool/systemTool.repo.test.ts | 12 +++--- .../app/tool/utils/clientSystemTool.test.ts | 12 +----- .../test/core/workflow/utils/index.test.ts | 3 -- .../core/app/tool/getSystemToolTemplates.ts | 42 +++++++------------ .../api/core/plugin/admin/tool/update.ts | 4 +- .../app/tool/getSystemToolTemplates.test.ts | 16 ++----- .../api/core/plugin/admin/tool/update.test.ts | 29 +++++++++++++ 15 files changed, 82 insertions(+), 98 deletions(-) diff --git a/packages/global/core/app/tool/systemTool/codec.ts b/packages/global/core/app/tool/systemTool/codec.ts index d798af6f6dc6..eb66fda42be8 100644 --- a/packages/global/core/app/tool/systemTool/codec.ts +++ b/packages/global/core/app/tool/systemTool/codec.ts @@ -43,16 +43,7 @@ export const SystemToolCodec = { }, fromDBTypeToListItemType(item: SystemPluginToolCollectionType): SystemToolListItemType { - const { - name, - avatar, - intro, - toolDescription, - version, - userGuide, - author = '', - tags - } = item.customConfig!; + const { name, avatar, intro, version, userGuide, author = '', tags } = item.customConfig!; return { id: item.pluginId, @@ -74,7 +65,6 @@ export const SystemToolCodec = { systemKeyCost: 0, // 数据库里面取出来的一定不是 toolset isToolSet: false, - toolDescription: toolDescription ?? intro ?? '', hideTags: item.hideTags ?? [], promoteTags: item.promoteTags ?? [] // TODO: 不知道谁做落了,之后再补吧 @@ -118,7 +108,6 @@ export const SystemToolCodec = { status: config?.status ?? PluginStatusEnum.Normal, systemKeyCost: config?.systemKeyCost ?? 0, tags: config?.customConfig?.tags ?? tool.tags ?? [], - toolDescription: config?.customConfig?.toolDescription ?? tool.toolDescription ?? '', version: tool.version, courseUrl: tool.tutorialUrl, hideTags: config?.hideTags ?? [], diff --git a/packages/global/core/app/tool/systemTool/type/base.ts b/packages/global/core/app/tool/systemTool/type/base.ts index 324f11417bbd..64a1cf5469f0 100644 --- a/packages/global/core/app/tool/systemTool/type/base.ts +++ b/packages/global/core/app/tool/systemTool/type/base.ts @@ -48,7 +48,6 @@ export const SystemToolListItemSchema = z.object({ intro: z.string().meta({ description: '工具的简介' }), author: z.string().meta({ description: '工具的作者' }), tags: z.array(z.string()).meta({ description: '工具的标签' }), - toolDescription: z.string().meta({ description: '给工具调用使用的工具的描述' }), userGuide: z.string().nullish().meta({ description: '工具的使用指南(markdown 纯文本)' }), readmeUrl: z.string().optional().meta({ description: '工具的 README 地址' }), @@ -79,7 +78,6 @@ export const SystemToolChildDetailSchema = z.object({ name: z.string(), status: PluginStatusSchema.meta({ description: '工具的状态' }), description: z.string().optional(), - toolDescription: z.string().optional(), icon: z.string().optional(), currentCost: z.number().meta({ description: '当前使用的费用' }), systemKeyCost: z.number().meta({ description: '系统密钥的费用' }), diff --git a/packages/global/core/plugin/tool/type.ts b/packages/global/core/plugin/tool/type.ts index 3df6965f4470..dd05b1d742b5 100644 --- a/packages/global/core/plugin/tool/type.ts +++ b/packages/global/core/plugin/tool/type.ts @@ -22,7 +22,6 @@ export const SystemPluginToolCollectionSchema = SystemToolBasicConfigSchema.exte name: z.string(), avatar: z.string().optional(), intro: z.string().optional(), - toolDescription: z.string().optional(), version: z.string(), tags: z.array(z.string()).nullish(), associatedPluginId: z.string().optional(), diff --git a/packages/global/openapi/core/app/tool/api.ts b/packages/global/openapi/core/app/tool/api.ts index f1f2c256ad89..a5e294e49a3d 100644 --- a/packages/global/openapi/core/app/tool/api.ts +++ b/packages/global/openapi/core/app/tool/api.ts @@ -47,11 +47,7 @@ export const GetToolSetChildrenResponseSchema = z.object({ }); export type GetToolSetChildrenResponseType = z.infer; -const ToolNodeTemplateListItemSchema = NodeTemplateListItemTypeSchema.extend({ - toolDescription: z.string().optional().meta({ - description: '工具调用描述' - }) -}).catchall(z.any()); +const ToolNodeTemplateListItemSchema = NodeTemplateListItemTypeSchema.catchall(z.any()); const ToolPreviewNodeResponseSchema = FlowNodeTemplateTypeSchema.omit({ // Runtime-only predicate; functions cannot be represented in OpenAPI or JSON. diff --git a/packages/global/test/core/app/tool/systemTool/type.test.ts b/packages/global/test/core/app/tool/systemTool/type.test.ts index 60254c02f5d9..8aa68dfd83e9 100644 --- a/packages/global/test/core/app/tool/systemTool/type.test.ts +++ b/packages/global/test/core/app/tool/systemTool/type.test.ts @@ -18,7 +18,6 @@ const createAdminToolDetail = () => ({ intro: 'Tool intro', author: 'FastGPT', tags: [], - toolDescription: 'Tool description', currentCost: 0, systemKeyCost: 0, hasTokenFee: false, @@ -34,6 +33,7 @@ describe('AdminSystemToolDetailSchema', () => { }); expect(result.status).toBe(PluginStatusEnum.Hidden); + expect(result).not.toHaveProperty('toolDescription'); }); it('strips input and output schemas from admin detail response', () => { diff --git a/packages/global/test/core/app/tool/systemToolConfig.test.ts b/packages/global/test/core/app/tool/systemToolConfig.test.ts index 89fdd775888e..9ffcf9b42862 100644 --- a/packages/global/test/core/app/tool/systemToolConfig.test.ts +++ b/packages/global/test/core/app/tool/systemToolConfig.test.ts @@ -1,9 +1,26 @@ import { describe, expect, it } from 'vitest'; import { SystemToolCodec } from '@fastgpt/global/core/app/tool/systemTool/codec'; import { UpdateSystemToolBodySchema } from '@fastgpt/global/openapi/core/plugin/admin/tool/api'; -import type { SystemPluginToolCollectionType } from '@fastgpt/global/core/plugin/tool/type'; +import { + SystemPluginToolCollectionSchema, + type SystemPluginToolCollectionType +} from '@fastgpt/global/core/plugin/tool/type'; describe('system tool config', () => { + it('strips legacy resource toolDescription at the config schema boundary', () => { + const result = SystemPluginToolCollectionSchema.parse({ + pluginId: 'systemTool-weather', + customConfig: { + name: 'Weather', + intro: 'Weather intro', + toolDescription: 'Legacy resource description', + version: '1.0.0' + } + }); + + expect(result.customConfig).not.toHaveProperty('toolDescription'); + }); + it('allows null secretsVal to explicitly disable system secret', () => { const result = UpdateSystemToolBodySchema.parse({ id: 'systemTool-github', diff --git a/packages/service/core/app/tool/systemTool/systemTool.repo.ts b/packages/service/core/app/tool/systemTool/systemTool.repo.ts index d55288fd8c9e..d43091bcbba9 100644 --- a/packages/service/core/app/tool/systemTool/systemTool.repo.ts +++ b/packages/service/core/app/tool/systemTool/systemTool.repo.ts @@ -60,14 +60,7 @@ type SystemToolRuntimeType = { type SystemToolDisplayChildType = Pick< SystemToolChildDetailType, - | 'id' - | 'name' - | 'status' - | 'description' - | 'toolDescription' - | 'icon' - | 'currentCost' - | 'systemKeyCost' + 'id' | 'name' | 'status' | 'description' | 'icon' | 'currentCost' | 'systemKeyCost' >; type SystemToolDisplayInfoType = Pick< @@ -82,7 +75,6 @@ type SystemToolDisplayInfoType = Pick< | 'intro' | 'author' | 'tags' - | 'toolDescription' | 'userGuide' | 'readmeUrl' | 'courseUrl' @@ -421,7 +413,6 @@ export class SystemToolRepo { id: pluginId, name: dbTool.customConfig.name, status: getVisiblePluginStatus({ status: dbTool.status, source: toolSource }), - toolDescription: dbTool.customConfig.toolDescription ?? dbTool.customConfig.intro ?? '', version: appVersion.versionId, versionLabel: appVersion.versionName, intro: dbTool.customConfig.intro ?? '', @@ -562,9 +553,6 @@ export class SystemToolRepo { tags: dbTool?.customConfig?.tags ?? tool.tags ?? [], source: tool.source, userGuide: dbTool?.customConfig?.userGuide, - toolDescription: - dbTool?.customConfig?.toolDescription ?? - (childPluginId ? child!.toolDescription : tool.toolDescription), // courseUrl: dbTool?.customConfig?.courseUrl ?? tool.tutorialUrl, courseUrl: tool.tutorialUrl, readmeUrl: tool.readmeUrl, @@ -616,8 +604,6 @@ export class SystemToolRepo { intro: exactDbTool.customConfig.intro ?? '', author: exactDbTool.customConfig.author ?? global.feConfigs.systemTitle ?? '', tags: exactDbTool.customConfig.tags ?? [], - toolDescription: - exactDbTool.customConfig.toolDescription ?? exactDbTool.customConfig.intro ?? '', userGuide: exactDbTool.customConfig.userGuide, pluginOrder: exactDbTool.pluginOrder ?? 0, originCost: exactDbTool.originCost ?? 0, @@ -684,7 +670,6 @@ export class SystemToolRepo { source: isIsolatedSource ? source : undefined }), description: parseI18nString(item.description, lang), - toolDescription: childConfig?.customConfig?.toolDescription ?? item.toolDescription, icon: childIcon, currentCost: childConfig?.currentCost ?? 0, systemKeyCost: childConfig?.systemKeyCost ?? 0 @@ -702,7 +687,6 @@ export class SystemToolRepo { avatar: child.icon ?? parent.avatar, name: child.name, intro: child.description ?? '', - toolDescription: child.toolDescription ?? '', status: child.status, currentCost: child.currentCost, systemKeyCost: child.systemKeyCost diff --git a/packages/service/core/plugin/teamPluginPolicy.ts b/packages/service/core/plugin/teamPluginPolicy.ts index 68532e21b5b6..b98656b58005 100644 --- a/packages/service/core/plugin/teamPluginPolicy.ts +++ b/packages/service/core/plugin/teamPluginPolicy.ts @@ -243,7 +243,6 @@ const buildDeletedToolPlaceholder = ({ intro: '', author: '', tags: [], - toolDescription: '', userGuide: undefined, pluginOrder: 0, originCost: 0, diff --git a/packages/service/test/core/app/tool/systemTool/systemTool.repo.test.ts b/packages/service/test/core/app/tool/systemTool/systemTool.repo.test.ts index f0fc1ddf7f65..1ac995cea184 100644 --- a/packages/service/test/core/app/tool/systemTool/systemTool.repo.test.ts +++ b/packages/service/test/core/app/tool/systemTool/systemTool.repo.test.ts @@ -157,6 +157,7 @@ describe('SystemToolRepo.getSystemToolList', () => { 'systemTool-low-order-single-match', 'systemTool-second-order-single-match' ]); + expect(tools.some((tool) => 'toolDescription' in tool)).toBe(false); }); it('keeps plugin order sorting when no tags are provided', async () => { @@ -445,6 +446,7 @@ describe('SystemToolRepo.getSystemToolDetail', () => { expect(tool).not.toHaveProperty('inputs'); expect(tool).not.toHaveProperty('outputs'); expect(tool).not.toHaveProperty('secrets'); + expect(tool).not.toHaveProperty('toolDescription'); }); it('uses the parent tool secret for toolset child details', async () => { @@ -798,7 +800,6 @@ describe('SystemToolRepo.getSystemToolDisplayInfo', () => { name: 'Workflow Tool', avatar: 'workflow.svg', intro: 'Workflow intro', - toolDescription: 'Workflow description', version: 'workflow-version', currentCost: 3, systemKeyCost: 4, @@ -807,6 +808,7 @@ describe('SystemToolRepo.getSystemToolDisplayInfo', () => { expect(tool).not.toHaveProperty('inputSchema'); expect(tool).not.toHaveProperty('outputSchema'); expect(tool).not.toHaveProperty('secretSchema'); + expect(tool).not.toHaveProperty('toolDescription'); expect(mocks.findAppById).not.toHaveBeenCalled(); expect(mocks.getAppLatestVersion).not.toHaveBeenCalled(); expect(mocks.getTool).not.toHaveBeenCalled(); @@ -875,13 +877,13 @@ describe('SystemToolRepo.getSystemToolDisplayInfo', () => { name: 'Forecast', intro: 'Forecast intro', avatar: 'forecast.svg', - toolDescription: 'Configured forecast', currentCost: 2, systemKeyCost: 1 }); expect(tool).not.toHaveProperty('inputSchema'); expect(tool).not.toHaveProperty('outputSchema'); expect(tool).not.toHaveProperty('secretSchema'); + expect(tool).not.toHaveProperty('toolDescription'); expect(mocks.getTool).not.toHaveBeenCalled(); }); @@ -935,9 +937,9 @@ describe('SystemToolRepo.getSystemToolDisplayInfo', () => { id: 'forecast', name: 'Forecast', description: 'Forecast intro', - toolDescription: 'Forecast tool', icon: 'forecast.svg' }); + expect(tool.children?.[0]).not.toHaveProperty('toolDescription'); expect(tool.children?.[0]).not.toHaveProperty('inputSchema'); expect(tool.children?.[0]).not.toHaveProperty('outputSchema'); expect(mocks.getTool).not.toHaveBeenCalled(); @@ -1040,9 +1042,9 @@ describe('SystemToolRepo.getSystemToolDisplayInfo', () => { source: 'debug:tmbId:tmb-1', status: PluginStatusEnum.Normal, currentCost: 3, - systemKeyCost: 2, - toolDescription: 'Configured forecast' + systemKeyCost: 2 }); + expect(tool).not.toHaveProperty('toolDescription'); }); it('keeps parent toolset display lightweight when list omits child icons', async () => { diff --git a/packages/service/test/core/app/tool/utils/clientSystemTool.test.ts b/packages/service/test/core/app/tool/utils/clientSystemTool.test.ts index f7f1d3d213a3..e13ffed6f99a 100644 --- a/packages/service/test/core/app/tool/utils/clientSystemTool.test.ts +++ b/packages/service/test/core/app/tool/utils/clientSystemTool.test.ts @@ -45,7 +45,7 @@ describe('getClientSystemToolPreviewNode', () => { intro: 'Weather query', author: 'FastGPT', tags: [], - toolDescription: 'Weather query', + toolDescription: 'Legacy resource description', currentCost: 0, systemKeyCost: 1, hasTokenFee: false, @@ -94,6 +94,7 @@ describe('getClientSystemToolPreviewNode', () => { ] }); expect(result.inputs[1]?.key).toBe('city'); + expect(result).not.toHaveProperty('toolDescription'); expect(result.version).toBe('1.0.0'); expect(result.versionLabel).toBe('1.0.0'); }); @@ -110,7 +111,6 @@ describe('getClientSystemToolPreviewNode', () => { intro: 'Weather query', author: 'FastGPT', tags: [], - toolDescription: 'Weather query', currentCost: 0, systemKeyCost: 0, hasTokenFee: false, @@ -150,7 +150,6 @@ describe('getClientSystemToolPreviewNode', () => { intro: 'Workflow tool', author: 'FastGPT', tags: [], - toolDescription: 'Workflow tool', currentCost: 0, systemKeyCost: 0, hasTokenFee: false, @@ -203,7 +202,6 @@ describe('getClientSystemToolPreviewNode', () => { intro: 'Workflow tool', author: 'FastGPT', tags: [], - toolDescription: 'Workflow tool', currentCost: 0, systemKeyCost: 0, hasTokenFee: false, @@ -255,7 +253,6 @@ describe('getClientSystemToolPreviewNode', () => { intro: 'Weather query', author: 'FastGPT', tags: [], - toolDescription: 'Weather query', currentCost: 0, systemKeyCost: 0, hasTokenFee: false, @@ -284,7 +281,6 @@ describe('getClientSystemToolPreviewNode', () => { intro: 'Weather query', author: 'FastGPT', tags: [], - toolDescription: 'Weather query', currentCost: 0, systemKeyCost: 0, hasTokenFee: false, @@ -319,7 +315,6 @@ describe('getClientSystemToolPreviewNode', () => { intro: 'Weather query', author: 'FastGPT', tags: [], - toolDescription: 'Weather query', currentCost: 0, systemKeyCost: 0, hasTokenFee: false, @@ -359,7 +354,6 @@ describe('getClientSystemToolPreviewNode', () => { intro: 'Weather query', author: 'FastGPT', tags: [], - toolDescription: 'Weather query', currentCost: 0, systemKeyCost: 0, hasTokenFee: false, @@ -404,7 +398,6 @@ describe('getClientSystemToolPreviewNode', () => { intro: 'Search tools', author: 'FastGPT', tags: [], - toolDescription: 'Search tools', currentCost: 0, systemKeyCost: 0, hasTokenFee: false, @@ -445,7 +438,6 @@ describe('getClientSystemToolPreviewNode', () => { intro: 'Workflow tool intro', author: 'FastGPT', tags: [], - toolDescription: 'Run workflow tool', currentCost: 1, systemKeyCost: 0, hasTokenFee: true, diff --git a/packages/service/test/core/workflow/utils/index.test.ts b/packages/service/test/core/workflow/utils/index.test.ts index 43121ab8fdf9..05457eff8fcb 100644 --- a/packages/service/test/core/workflow/utils/index.test.ts +++ b/packages/service/test/core/workflow/utils/index.test.ts @@ -183,7 +183,6 @@ describe('getSystemToolRunTimeNodeFromSystemToolset', () => { id: string; name?: string; description?: string; - toolDescription?: string; inputSchema?: any; outputSchema?: any; }> = [ @@ -191,7 +190,6 @@ describe('getSystemToolRunTimeNodeFromSystemToolset', () => { id: 'child-1', name: 'Original', description: 'Original Desc', - toolDescription: 'Original Tool Desc', inputSchema: childInputSchema, outputSchema: childOutputSchema } @@ -227,7 +225,6 @@ describe('getSystemToolRunTimeNodeFromSystemToolset', () => { id: 'child-1', name: 'Original', description: 'Original Desc', - toolDescription: 'Original Tool Desc', inputSchema: childInputSchema, outputSchema: childOutputSchema }, diff --git a/projects/app/src/pages/api/core/app/tool/getSystemToolTemplates.ts b/projects/app/src/pages/api/core/app/tool/getSystemToolTemplates.ts index 90ec4a67e697..dec7cdd5be9a 100644 --- a/projects/app/src/pages/api/core/app/tool/getSystemToolTemplates.ts +++ b/projects/app/src/pages/api/core/app/tool/getSystemToolTemplates.ts @@ -57,7 +57,7 @@ export async function handler( pluginId: getRawPluginIdFromSystemToolId(parentId) }); } - const parentSource = await getQuerySource({ source, teamId, tmbId }); + const parentSource = await getQuerySource({ source, tmbId }); const parent = await systemToolRepo.getSystemToolDisplayInfoWithChildIcons({ pluginId: parentId, lang, @@ -71,7 +71,7 @@ export async function handler( parent.children ?.filter((child) => isSelectableToolStatus(child.status)) .map((child) => ({ - ...parent, + ...omitLegacyToolDescription(parent), templateType: FlowNodeTemplateTypeEnum.tools, isTool: true, // templateType: tool.isToolSet @@ -80,7 +80,6 @@ export async function handler( flowNodeType: FlowNodeTypeEnum.tool, name: child.name, intro: child.description, - toolDescription: child.toolDescription, id: `${parentId}/${child.id}`, source: isTeamPluginSource(source) ? source : parent.source, avatar: child.icon ?? parent.avatar, @@ -121,7 +120,7 @@ export async function handler( return true; }) .map((tool) => ({ - ...tool, + ...omitLegacyToolDescription(tool), templateType: FlowNodeTemplateTypeEnum.tools, isTool: true, flowNodeType: tool.isToolSet ? FlowNodeTypeEnum.toolSet : FlowNodeTypeEnum.tool, @@ -129,7 +128,6 @@ export async function handler( name: tool.name, intro: tool.intro, instructions: tool.userGuide ?? '', - toolDescription: tool.toolDescription, tags: tool.tags })) .filter((item) => filterTemplateBySearchKey(item, searchRegex)); @@ -150,15 +148,7 @@ async function getActiveDebugSource({ tmbId, source }: { tmbId: string; source?: } } -async function getQuerySource({ - source, - teamId, - tmbId -}: { - source?: string; - teamId: string; - tmbId: string; -}) { +async function getQuerySource({ source, tmbId }: { source?: string; tmbId: string }) { if (isTeamPluginSource(source)) return source; if (isDebugToolSource(source)) { return getActiveDebugSource({ tmbId, source }); @@ -189,19 +179,17 @@ function filterTemplatesBySearchKey( return templates.filter((item) => filterTemplateBySearchKey(item, searchRegex)); } -function filterTemplateBySearchKey( - template: NodeTemplateListItemType & { - toolDescription?: string; - }, - searchRegex?: RegExp -) { +function filterTemplateBySearchKey(template: NodeTemplateListItemType, searchRegex?: RegExp) { if (!searchRegex) return true; - return [ - template.name, - template.intro, - template.instructions, - template.toolDescription, - ...(template.tags ?? []) - ].some((text) => searchRegex.test(String(text ?? ''))); + return [template.name, template.intro, template.instructions, ...(template.tags ?? [])].some( + (text) => searchRegex.test(String(text ?? '')) + ); +} + +/** API 边界裁剪历史系统工具资源字段,兼容旧 provider 或数据库对象。 */ +function omitLegacyToolDescription(value: T): T { + const result = { ...value } as T & { toolDescription?: unknown }; + delete result.toolDescription; + return result; } diff --git a/projects/app/src/pages/api/core/plugin/admin/tool/update.ts b/projects/app/src/pages/api/core/plugin/admin/tool/update.ts index 7cf255abcbd4..29a8111ca168 100644 --- a/projects/app/src/pages/api/core/plugin/admin/tool/update.ts +++ b/projects/app/src/pages/api/core/plugin/admin/tool/update.ts @@ -84,7 +84,9 @@ export async function handler( await mongoSessionRun(async (session) => { // 构建 customConfig,保留现有配置并添加/更新 tags - const existingCustomConfig = plugin?.customConfig || {}; + const existingCustomConfig = Object.fromEntries( + Object.entries(plugin?.customConfig || {}).filter(([key]) => key !== 'toolDescription') + ); const newCustomConfig = updateFields.tags ? { ...existingCustomConfig, tags: updateFields.tags } : existingCustomConfig; diff --git a/projects/app/test/api/core/app/tool/getSystemToolTemplates.test.ts b/projects/app/test/api/core/app/tool/getSystemToolTemplates.test.ts index 9330ef538e98..6c087928d8a9 100644 --- a/projects/app/test/api/core/app/tool/getSystemToolTemplates.test.ts +++ b/projects/app/test/api/core/app/tool/getSystemToolTemplates.test.ts @@ -84,7 +84,7 @@ describe('get system tool templates handler', () => { id: 'weather', name: 'Weather', intro: 'Forecast lookup', - toolDescription: 'Get weather', + toolDescription: 'Legacy resource description', isToolSet: false, status: PluginStatusEnum.Normal, tags: ['life'] @@ -93,7 +93,6 @@ describe('get system tool templates handler', () => { id: 'math', name: 'Math', intro: 'Calculator', - toolDescription: 'Compute numbers', isToolSet: false, status: PluginStatusEnum.Normal, tags: ['calc'] @@ -102,7 +101,6 @@ describe('get system tool templates handler', () => { id: 'hidden-weather', name: 'Hidden Weather', intro: 'Forecast lookup', - toolDescription: 'Get weather', isToolSet: false, status: PluginStatusEnum.Normal, tags: ['life'], @@ -122,6 +120,7 @@ describe('get system tool templates handler', () => { isTool: true, flowNodeType: FlowNodeTypeEnum.tool }); + expect(result[0]).not.toHaveProperty('toolDescription'); expect(mocks.getSystemToolList).toHaveBeenCalledWith({ lang: 'zh', op: 'or', @@ -136,7 +135,6 @@ describe('get system tool templates handler', () => { id: 'toolset', name: 'Toolset', intro: '', - toolDescription: '', isToolSet: true, status: PluginStatusEnum.Normal, tags: [] @@ -168,7 +166,6 @@ describe('get system tool templates handler', () => { source: 'debug:tmbId:tmb-1', name: 'Debug Tool', intro: '', - toolDescription: '', isToolSet: false, status: PluginStatusEnum.Normal, tags: [] @@ -178,7 +175,6 @@ describe('get system tool templates handler', () => { source: 'system', name: 'System Tool', intro: '', - toolDescription: '', isToolSet: false, status: PluginStatusEnum.Normal, tags: [] @@ -255,7 +251,7 @@ describe('get system tool templates handler', () => { name: 'A+B Tool', status: PluginStatusEnum.Normal, description: 'Exact plus', - toolDescription: 'Use literal plus', + toolDescription: 'Legacy child resource description', icon: 'plus-icon', currentCost: 2, systemKeyCost: 0.5 @@ -265,7 +261,6 @@ describe('get system tool templates handler', () => { name: 'AxxB Tool', status: PluginStatusEnum.Normal, description: 'Would match an unescaped regex', - toolDescription: 'No literal plus', currentCost: 3, systemKeyCost: 1 } @@ -289,6 +284,7 @@ describe('get system tool templates handler', () => { isTool: true, flowNodeType: FlowNodeTypeEnum.tool }); + expect(result[0]).not.toHaveProperty('toolDescription'); expect(mocks.getSystemToolDisplayInfoWithChildIcons).toHaveBeenCalledWith({ pluginId: 'toolset', lang: 'zh', @@ -348,7 +344,6 @@ describe('get system tool templates handler', () => { id: 'normal-tool', name: 'Normal Tool', intro: '', - toolDescription: '', isToolSet: false, status: PluginStatusEnum.Normal, tags: [] @@ -357,7 +352,6 @@ describe('get system tool templates handler', () => { id: 'hidden-tool', name: 'Hidden Tool', intro: '', - toolDescription: '', isToolSet: false, status: PluginStatusEnum.Hidden, tags: [] @@ -366,7 +360,6 @@ describe('get system tool templates handler', () => { id: 'soon-offline-tool', name: 'Soon Offline Tool', intro: '', - toolDescription: '', isToolSet: false, status: PluginStatusEnum.SoonOffline, tags: [] @@ -375,7 +368,6 @@ describe('get system tool templates handler', () => { id: 'offline-tool', name: 'Offline Tool', intro: '', - toolDescription: '', isToolSet: false, status: PluginStatusEnum.Offline, tags: [] diff --git a/projects/app/test/api/core/plugin/admin/tool/update.test.ts b/projects/app/test/api/core/plugin/admin/tool/update.test.ts index 463bbe603f7f..37f3346c97ed 100644 --- a/projects/app/test/api/core/plugin/admin/tool/update.test.ts +++ b/projects/app/test/api/core/plugin/admin/tool/update.test.ts @@ -97,6 +97,35 @@ describe('admin system tool update handler', () => { ); }); + it('保存系统工具配置时丢弃历史资源级 toolDescription', async () => { + mocks.findOne.mockResolvedValue({ + pluginId: 'systemTool-weather', + customConfig: { + name: 'Weather', + intro: 'Weather intro', + toolDescription: 'Legacy resource description', + version: '1.0.0' + } + }); + + await handler( + { + body: { + id: 'systemTool-weather', + tags: ['weather'] + } + } as any, + {} as any + ); + + expect(mocks.updateOne.mock.calls[0][1].customConfig).toEqual({ + name: 'Weather', + intro: 'Weather intro', + version: '1.0.0', + tags: ['weather'] + }); + }); + it('为新建的子工具写入与父工具一致的密钥', async () => { const secretsVal = { apiKey: 'plain-value' }; From 1fb27334288156ca0003b61a94e783365949e558 Mon Sep 17 00:00:00 2001 From: Nixieboluo Date: Thu, 3 Sep 2026 15:18:31 +0800 Subject: [PATCH 03/15] fix(workflow): backfill MCP HTTP tool intro Preserve saved intro while backfilling existing tool nodes from remote descriptions. --- .../service/core/app/tool/utils/client.ts | 6 +- packages/service/core/app/utils.ts | 9 +++ .../core/workflow/dispatch/utils/index.ts | 10 ++- .../test/core/app/tool/utils/client.test.ts | 39 ++++++++++ .../test/core/workflow/dispatch/utils.test.ts | 51 ++++++++++++ .../app/rewriteAppWorkflowToDetail.test.ts | 78 +++++++++++++++++++ 6 files changed, 190 insertions(+), 3 deletions(-) diff --git a/packages/service/core/app/tool/utils/client.ts b/packages/service/core/app/tool/utils/client.ts index a117fdc66493..333d397656dc 100644 --- a/packages/service/core/app/tool/utils/client.ts +++ b/packages/service/core/app/tool/utils/client.ts @@ -495,6 +495,10 @@ export async function getClientToolPreviewNode({ forceDefaultMode: true, allowUserChatInputAgentGenerated: true }); + const intro = + idSource === AppToolSourceEnum.mcp || idSource === AppToolSourceEnum.http + ? app.workflow.nodes.find((node) => node.flowNodeType === FlowNodeTypeEnum.tool)?.intro + : app.intro; return { id: getNanoid(), @@ -504,7 +508,7 @@ export async function getClientToolPreviewNode({ flowNodeType, avatar: app.avatar, name: parseI18nString(app.name, lang), - intro: parseI18nString(app.intro, lang), + intro: parseI18nString(intro, lang), courseUrl: app.courseUrl, userGuide: app.userGuide, showStatus: true, diff --git a/packages/service/core/app/utils.ts b/packages/service/core/app/utils.ts index 6b2a8809eed3..eda48d021e73 100644 --- a/packages/service/core/app/utils.ts +++ b/packages/service/core/app/utils.ts @@ -3,6 +3,7 @@ import { getDefaultEmbeddingModelData } from '../ai/model'; import { desensitizeSystemModel } from '../ai/config/utils'; import { getDatasetEmbeddingModel } from '../dataset/model'; import { DatasetTypeEnum, DatasetTypeMap } from '@fastgpt/global/core/dataset/constants'; +import { AppToolSourceEnum } from '@fastgpt/global/core/app/tool/constants'; import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant'; import { NodeInputKeyEnum } from '@fastgpt/global/core/workflow/constants'; import type { StoreNodeItemType } from '@fastgpt/global/core/workflow/type/node'; @@ -257,6 +258,14 @@ export async function rewriteAppWorkflowToDetail({ node.hasTokenFee = preview.hasTokenFee; node.hasSystemSecret = preview.hasSystemSecret; + const { source } = splitCombineToolId(node.pluginId); + if ( + (source === AppToolSourceEnum.mcp || source === AppToolSourceEnum.http) && + !node.intro?.trim() + ) { + node.intro = preview.intro; + } + node.toolConfig = preview.toolConfig; // Latest version diff --git a/packages/service/core/workflow/dispatch/utils/index.ts b/packages/service/core/workflow/dispatch/utils/index.ts index 8d7e7b23697c..fc51efb65b2c 100644 --- a/packages/service/core/workflow/dispatch/utils/index.ts +++ b/packages/service/core/workflow/dispatch/utils/index.ts @@ -747,7 +747,10 @@ export const rewriteRuntimeWorkFlow = async ({ const toolRaw = toolList.find((tool) => tool.name === parseResult.toolName); if (!toolRaw) return; node.jsonSchema = toolRaw.inputSchema; - node.intro = toolRaw.description; + // 为已存在的 MCP 工具节点提供描述回退。 + if (!node.intro?.trim()) { + node.intro = toolRaw.description; + } mergeToolNodeInputs({ node, jsonSchema: toolRaw.inputSchema, schemaType: 'mcp' }); }); }; @@ -788,7 +791,10 @@ export const rewriteRuntimeWorkFlow = async ({ if (!toolRaw) return; const { inputSchema, requestSchema } = getHTTPToolRuntimeSchemas(toolRaw); node.jsonSchema = requestSchema; - node.intro = toolRaw.description; + // 为已存在的 HTTP 工具节点提供描述回退。 + if (!node.intro?.trim()) { + node.intro = toolRaw.description; + } mergeToolNodeInputs({ node, jsonSchema: inputSchema, diff --git a/packages/service/test/core/app/tool/utils/client.test.ts b/packages/service/test/core/app/tool/utils/client.test.ts index 9e3fb82f5718..2005461fd296 100644 --- a/packages/service/test/core/app/tool/utils/client.test.ts +++ b/packages/service/test/core/app/tool/utils/client.test.ts @@ -281,6 +281,7 @@ describe('getClientToolPreviewNode', () => { expect(result.toolConfig?.httpTool).toEqual({ toolId: 'http-507f1f77bcf86cd799439011/search' }); + expect(result.intro).toBe('Search tool'); expect(result.inputs[0]?.key).toBe('q'); expect((result as any).jsonSchema).toBeUndefined(); expect(getRuntimeSchemaFieldPaths(result)).toEqual([]); @@ -430,6 +431,44 @@ describe('getClientToolPreviewNode', () => { expect(getRuntimeSchemaFieldPaths(result)).toEqual([]); }); + it('uses the MCP tool description for a standalone tool preview', async () => { + mocks.findById.mockReturnValueOnce({ + lean: vi.fn().mockResolvedValue({ + _id: '507f1f77bcf86cd799439013', + teamId: '507f1f77bcf86cd799439014', + type: AppTypeEnum.mcpToolSet, + name: 'MCP Tools', + avatar: 'mcp.svg' + }) + }); + mocks.getAppVersionById.mockResolvedValueOnce({ + nodes: [ + { + toolConfig: { + mcpToolSet: { + toolList: [ + { + name: 'search', + description: 'MCP search tool', + inputSchema: { type: 'object', properties: {} } + } + ] + } + } + } + ], + edges: [], + chatConfig: {} + }); + + const result = await getClientToolPreviewNode({ + appId: 'mcp-507f1f77bcf86cd799439013/search', + lang: 'en' + }); + + expect(result.intro).toBe('MCP search tool'); + }); + it('applies defaultToAgentGenerated over a workflow plugin input selection', async () => { const appId = '507f1f77bcf86cd799439011'; mocks.findById.mockReturnValueOnce({ diff --git a/packages/service/test/core/workflow/dispatch/utils.test.ts b/packages/service/test/core/workflow/dispatch/utils.test.ts index 131d8dd7c776..ed7877189b6c 100644 --- a/packages/service/test/core/workflow/dispatch/utils.test.ts +++ b/packages/service/test/core/workflow/dispatch/utils.test.ts @@ -1347,6 +1347,57 @@ describe('rewriteRuntimeWorkFlow', () => { }); }); + it('should preserve saved intro for standalone MCP and HTTP tool nodes', async () => { + const mcpToolNode = makeNode('mcp1', FlowNodeTypeEnum.tool, { + intro: 'Saved MCP intro', + toolConfig: { mcpTool: { toolId: 'mcp-toolset-1/toolA' } } + } as any); + const httpToolNode = makeNode('http1', FlowNodeTypeEnum.tool, { + intro: 'Saved HTTP intro', + toolConfig: { httpTool: { toolId: 'http-toolset-1/toolB' } } + } as any); + setupFindByIdMap({ + 'toolset-1': { + _id: 'toolset-1', + modules: [ + { + toolConfig: { + mcpToolSet: { + url: 'https://mcp.example.com', + toolList: [ + { + name: 'toolA', + description: 'Remote MCP intro', + inputSchema: { type: 'object', properties: {} } + } + ] + }, + httpToolSet: { + toolList: [ + { + name: 'toolB', + description: 'Remote HTTP intro', + inputSchema: { type: 'object', properties: {} }, + requestSchema: { type: 'object', properties: {} } + } + ] + } + } + } + ] + } + }); + + await rewriteRuntimeWorkFlow({ + teamId: 'team1', + nodes: [mcpToolNode, httpToolNode], + edges: [] + }); + + expect(mcpToolNode.intro).toBe('Saved MCP intro'); + expect(httpToolNode.intro).toBe('Saved HTTP intro'); + }); + it('should use the remote default only when a saved tool input has no selection', async () => { const mcpToolNode = makeNode('mcp1', FlowNodeTypeEnum.tool, { toolConfig: { mcpTool: { toolId: 'mcp-toolset-1/toolA' } }, diff --git a/projects/app/test/service/core/app/rewriteAppWorkflowToDetail.test.ts b/projects/app/test/service/core/app/rewriteAppWorkflowToDetail.test.ts index 33c4d2faa240..c433426a029f 100644 --- a/projects/app/test/service/core/app/rewriteAppWorkflowToDetail.test.ts +++ b/projects/app/test/service/core/app/rewriteAppWorkflowToDetail.test.ts @@ -710,6 +710,84 @@ describe('rewriteAppWorkflowToDetail - agent skills', () => { }); }); + it.each([ + ['mcp-app-1/search', 'MCP search description'], + ['http-app-1/search', 'HTTP search description'] + ])('补齐 %s 工具节点的空 intro', async (pluginId, description) => { + getClientToolPreviewNodeMock.mockResolvedValue({ + id: pluginId, + flowNodeType: FlowNodeTypeEnum.tool, + name: 'Search Tool', + avatar: 'tool-avatar', + intro: description, + inputs: [], + outputs: [], + version: '', + isLatestVersion: true + }); + authAppByTmbIdMock.mockResolvedValue({}); + + for (const originalIntro of [undefined, '', ' ']) { + const nodes = [ + { + nodeId: 'tool', + flowNodeType: FlowNodeTypeEnum.tool, + pluginId, + intro: originalIntro, + inputs: [], + outputs: [] + } as StoreNodeItemType + ]; + + await rewriteAppWorkflowToDetail({ + nodes, + teamId: 'team-1', + ownerTmbId: 'tmb-1', + isRoot: false + }); + + expect(nodes[0].intro).toBe(description); + } + }); + + it.each([ + ['mcp-app-1/search', 'Saved MCP description'], + ['http-app-1/search', 'Saved HTTP description'] + ])('保留 %s 工具节点已有 intro', async (pluginId, originalIntro) => { + getClientToolPreviewNodeMock.mockResolvedValue({ + id: pluginId, + flowNodeType: FlowNodeTypeEnum.tool, + name: 'Search Tool', + avatar: 'tool-avatar', + intro: 'Remote description', + inputs: [], + outputs: [], + version: '', + isLatestVersion: true + }); + authAppByTmbIdMock.mockResolvedValue({}); + + const nodes = [ + { + nodeId: 'tool', + flowNodeType: FlowNodeTypeEnum.tool, + pluginId, + intro: originalIntro, + inputs: [], + outputs: [] + } as StoreNodeItemType + ]; + + await rewriteAppWorkflowToDetail({ + nodes, + teamId: 'team-1', + ownerTmbId: 'tmb-1', + isRoot: false + }); + + expect(nodes[0].intro).toBe(originalIntro); + }); + it('刷新最新工具节点时保留 agentGenerated 推荐并显式保存手动类型', async () => { getClientToolPreviewNodeMock.mockResolvedValue({ id: 'mcp-app-1/search', From 6e62ae713df7756ff46623ffaedf41ace09a0bc8 Mon Sep 17 00:00:00 2001 From: Nixieboluo Date: Thu, 3 Sep 2026 16:28:44 +0800 Subject: [PATCH 04/15] test(workflow): remove stale descriptions --- .../core/app/tool/systemTool/type.test.ts | 1 - .../core/app/tool/systemToolConfig.test.ts | 19 +------- .../global/test/core/app/tool/type.test.ts | 5 +- .../core/workflow/migration/schema.test.ts | 4 +- .../tool/systemTool/systemTool.repo.test.ts | 46 +++---------------- .../test/core/app/tool/utils/client.test.ts | 1 - .../app/tool/utils/clientSystemTool.test.ts | 2 - .../dispatch/ai/agent/sub/tool/utils.test.ts | 13 ------ .../ai/toolcall/hooks/useToolCatalog.test.ts | 4 +- .../ai/toolcall/hooks/useToolNodeList.test.ts | 2 - .../ai/toolcall/nodeToolResponses.test.ts | 1 - .../dispatch/ai/toolcall/toolCall.test.ts | 3 -- .../createToolCallToolProvider.test.ts | 1 - .../app/tool/getSystemToolTemplates.test.ts | 4 -- .../api/core/plugin/admin/tool/update.test.ts | 3 +- .../api/core/plugin/team/tool/list.test.ts | 6 --- .../detail/WorkflowComponents/utils.test.ts | 1 - 17 files changed, 12 insertions(+), 104 deletions(-) diff --git a/packages/global/test/core/app/tool/systemTool/type.test.ts b/packages/global/test/core/app/tool/systemTool/type.test.ts index 8aa68dfd83e9..6434a41bb9b1 100644 --- a/packages/global/test/core/app/tool/systemTool/type.test.ts +++ b/packages/global/test/core/app/tool/systemTool/type.test.ts @@ -33,7 +33,6 @@ describe('AdminSystemToolDetailSchema', () => { }); expect(result.status).toBe(PluginStatusEnum.Hidden); - expect(result).not.toHaveProperty('toolDescription'); }); it('strips input and output schemas from admin detail response', () => { diff --git a/packages/global/test/core/app/tool/systemToolConfig.test.ts b/packages/global/test/core/app/tool/systemToolConfig.test.ts index 9ffcf9b42862..104ea2499879 100644 --- a/packages/global/test/core/app/tool/systemToolConfig.test.ts +++ b/packages/global/test/core/app/tool/systemToolConfig.test.ts @@ -1,26 +1,9 @@ import { describe, expect, it } from 'vitest'; import { SystemToolCodec } from '@fastgpt/global/core/app/tool/systemTool/codec'; import { UpdateSystemToolBodySchema } from '@fastgpt/global/openapi/core/plugin/admin/tool/api'; -import { - SystemPluginToolCollectionSchema, - type SystemPluginToolCollectionType -} from '@fastgpt/global/core/plugin/tool/type'; +import { type SystemPluginToolCollectionType } from '@fastgpt/global/core/plugin/tool/type'; describe('system tool config', () => { - it('strips legacy resource toolDescription at the config schema boundary', () => { - const result = SystemPluginToolCollectionSchema.parse({ - pluginId: 'systemTool-weather', - customConfig: { - name: 'Weather', - intro: 'Weather intro', - toolDescription: 'Legacy resource description', - version: '1.0.0' - } - }); - - expect(result.customConfig).not.toHaveProperty('toolDescription'); - }); - it('allows null secretsVal to explicitly disable system secret', () => { const result = UpdateSystemToolBodySchema.parse({ id: 'systemTool-github', diff --git a/packages/global/test/core/app/tool/type.test.ts b/packages/global/test/core/app/tool/type.test.ts index b79ac1c1966e..627df74fd6bb 100644 --- a/packages/global/test/core/app/tool/type.test.ts +++ b/packages/global/test/core/app/tool/type.test.ts @@ -13,7 +13,7 @@ describe('AgentToolSchema', () => { expect(result.inputs).toEqual([{ key: 'query', mode: AgentToolInputModeEnum.agentGenerated }]); }); - it('rejects a historical workflow input snapshot', () => { + it('rejects a non-sparse workflow input snapshot', () => { expect(() => AgentToolSchema.parse({ id: 'systemTool-search', @@ -21,8 +21,7 @@ describe('AgentToolSchema', () => { { key: 'query', renderTypeList: ['input', 'agentGenerated'], - selectedType: 'agentGenerated', - toolDescription: 'Search query' + selectedType: 'agentGenerated' } ], config: {} diff --git a/packages/global/test/core/workflow/migration/schema.test.ts b/packages/global/test/core/workflow/migration/schema.test.ts index 385e6dd7fba6..13a8923c7426 100644 --- a/packages/global/test/core/workflow/migration/schema.test.ts +++ b/packages/global/test/core/workflow/migration/schema.test.ts @@ -82,7 +82,7 @@ describe('workflow migration boundary', () => { expect(result.nodes[0].inputs[1]).not.toHaveProperty('isToolParam'); }); - it('drops legacy node descriptions while keeping input descriptions', async () => { + it('keeps node and input descriptions', async () => { const result = await migrateWorkflowToCurrent({ nodes: [ { @@ -90,7 +90,6 @@ describe('workflow migration boundary', () => { flowNodeType: 'chatNode', name: 'Chat', intro: 'Node intro', - toolDescription: 'Legacy node description', inputs: [ { key: 'query', @@ -105,7 +104,6 @@ describe('workflow migration boundary', () => { }); expect(result.nodes[0]).toMatchObject({ intro: 'Node intro' }); - expect(result.nodes[0]).not.toHaveProperty('toolDescription'); expect(result.nodes[0].inputs[0].toolDescription).toBe('Input description'); }); diff --git a/packages/service/test/core/app/tool/systemTool/systemTool.repo.test.ts b/packages/service/test/core/app/tool/systemTool/systemTool.repo.test.ts index 1ac995cea184..8645c65df9e9 100644 --- a/packages/service/test/core/app/tool/systemTool/systemTool.repo.test.ts +++ b/packages/service/test/core/app/tool/systemTool/systemTool.repo.test.ts @@ -86,8 +86,7 @@ const createPluginTool = ({ version: '1.0.0', etag: `${pluginId}-etag`, icon: `${pluginId}.svg`, - tags, - toolDescription: `${name} description` + tags }); const createToolConfig = ({ @@ -157,7 +156,6 @@ describe('SystemToolRepo.getSystemToolList', () => { 'systemTool-low-order-single-match', 'systemTool-second-order-single-match' ]); - expect(tools.some((tool) => 'toolDescription' in tool)).toBe(false); }); it('keeps plugin order sorting when no tags are provided', async () => { @@ -415,7 +413,6 @@ describe('SystemToolRepo.getSystemToolDetail', () => { version: '1.0.0', icon: 'weather.svg', tags: [], - toolDescription: 'Weather tool', inputSchema, outputSchema, secretSchema, @@ -424,7 +421,6 @@ describe('SystemToolRepo.getSystemToolDetail', () => { id: 'forecast', name: { en: 'Forecast' }, description: { en: 'Forecast intro' }, - toolDescription: 'Forecast tool', inputSchema, outputSchema } @@ -446,7 +442,6 @@ describe('SystemToolRepo.getSystemToolDetail', () => { expect(tool).not.toHaveProperty('inputs'); expect(tool).not.toHaveProperty('outputs'); expect(tool).not.toHaveProperty('secrets'); - expect(tool).not.toHaveProperty('toolDescription'); }); it('uses the parent tool secret for toolset child details', async () => { @@ -474,7 +469,6 @@ describe('SystemToolRepo.getSystemToolDetail', () => { version: '1.0.0', icon: 'weather.svg', tags: [], - toolDescription: 'Weather tool', hasSecret: true, secretSchema: { type: 'object', @@ -484,8 +478,7 @@ describe('SystemToolRepo.getSystemToolDetail', () => { { id: 'forecast', name: { en: 'Forecast' }, - description: { en: 'Forecast intro' }, - toolDescription: 'Forecast tool' + description: { en: 'Forecast intro' } } ] }); @@ -514,7 +507,6 @@ describe('SystemToolRepo.getSystemToolDetail', () => { version: '1.0.0', icon: 'weather.svg', tags: [], - toolDescription: 'Weather tool', hasSecret: true, secretSchema: { type: 'object', @@ -546,7 +538,6 @@ describe('SystemToolRepo.getSystemToolDetail', () => { version: '1.0.0', icon: 'weather.svg', tags: [], - toolDescription: 'Weather tool', hasSecret: true, secretSchema: { type: 'object', @@ -584,7 +575,6 @@ describe('SystemToolRepo.getSystemToolDetail', () => { version: '1.0.0', icon: 'weather.svg', tags: [], - toolDescription: 'Weather tool', hasSecret: true, secretSchema: { type: 'object', @@ -622,7 +612,6 @@ describe('SystemToolRepo.getSystemToolDetail', () => { version: '1.0.0', icon: 'weather.svg', tags: [], - toolDescription: 'Weather tool', hasSecret: true, secretSchema: { type: 'object', @@ -659,7 +648,6 @@ describe('SystemToolRepo.getSystemToolDetail', () => { version: '0.0.1', icon: 'perplexity.svg', tags: [], - toolDescription: 'Perplexity tool', inputSchema: null, outputSchema: null, secretSchema: null, @@ -668,7 +656,6 @@ describe('SystemToolRepo.getSystemToolDetail', () => { id: 'search', name: { en: 'Search' }, description: { en: 'Search intro' }, - toolDescription: 'Search tool', inputSchema: null, outputSchema: null } @@ -782,7 +769,6 @@ describe('SystemToolRepo.getSystemToolDisplayInfo', () => { name: 'Workflow Tool', avatar: 'workflow.svg', intro: 'Workflow intro', - toolDescription: 'Workflow description', version: 'workflow-version', tags: ['workflow'], associatedPluginId: 'app-id', @@ -808,7 +794,6 @@ describe('SystemToolRepo.getSystemToolDisplayInfo', () => { expect(tool).not.toHaveProperty('inputSchema'); expect(tool).not.toHaveProperty('outputSchema'); expect(tool).not.toHaveProperty('secretSchema'); - expect(tool).not.toHaveProperty('toolDescription'); expect(mocks.findAppById).not.toHaveBeenCalled(); expect(mocks.getAppLatestVersion).not.toHaveBeenCalled(); expect(mocks.getTool).not.toHaveBeenCalled(); @@ -840,14 +825,12 @@ describe('SystemToolRepo.getSystemToolDisplayInfo', () => { version: '1.0.0', icon: 'weather.svg', tags: ['life'], - toolDescription: 'Weather tool', hasSecret: false, children: [ { id: 'forecast', name: { en: 'Forecast' }, description: { en: 'Forecast intro' }, - toolDescription: 'Forecast tool', icon: 'forecast.svg', inputSchema, outputSchema @@ -862,8 +845,7 @@ describe('SystemToolRepo.getSystemToolDisplayInfo', () => { systemKeyCost: 1, customConfig: { name: 'Forecast', - version: '1.0.0', - toolDescription: 'Configured forecast' + version: '1.0.0' } } ]); @@ -883,7 +865,6 @@ describe('SystemToolRepo.getSystemToolDisplayInfo', () => { expect(tool).not.toHaveProperty('inputSchema'); expect(tool).not.toHaveProperty('outputSchema'); expect(tool).not.toHaveProperty('secretSchema'); - expect(tool).not.toHaveProperty('toolDescription'); expect(mocks.getTool).not.toHaveBeenCalled(); }); @@ -912,14 +893,12 @@ describe('SystemToolRepo.getSystemToolDisplayInfo', () => { version: '1.0.0', icon: 'weather.svg', tags: ['life'], - toolDescription: 'Weather tool', hasSecret: false, children: [ { id: 'forecast', name: { en: 'Forecast' }, description: { en: 'Forecast intro' }, - toolDescription: 'Forecast tool', icon: 'forecast.svg', inputSchema, outputSchema @@ -939,7 +918,6 @@ describe('SystemToolRepo.getSystemToolDisplayInfo', () => { description: 'Forecast intro', icon: 'forecast.svg' }); - expect(tool.children?.[0]).not.toHaveProperty('toolDescription'); expect(tool.children?.[0]).not.toHaveProperty('inputSchema'); expect(tool.children?.[0]).not.toHaveProperty('outputSchema'); expect(mocks.getTool).not.toHaveBeenCalled(); @@ -970,7 +948,6 @@ describe('SystemToolRepo.getSystemToolDisplayInfo', () => { version: '1.0.0', icon: 'weather.svg', tags: ['life'], - toolDescription: 'Weather tool', hasSecret: false } ]); @@ -995,9 +972,7 @@ describe('SystemToolRepo.getSystemToolDisplayInfo', () => { status: PluginStatusEnum.Offline, currentCost: 3, systemKeyCost: 2, - customConfig: { - toolDescription: 'Configured forecast' - } + customConfig: {} }; mocks.findSystemTools.mockResolvedValueOnce([childConfig]).mockResolvedValueOnce([ @@ -1018,14 +993,12 @@ describe('SystemToolRepo.getSystemToolDisplayInfo', () => { version: '1.0.0', icon: 'weather.svg', tags: ['life'], - toolDescription: 'Weather tool', hasSecret: false, children: [ { id: 'forecast', name: { en: 'Forecast' }, description: { en: 'Forecast intro' }, - toolDescription: 'Forecast tool', icon: 'forecast.svg' } ] @@ -1044,7 +1017,6 @@ describe('SystemToolRepo.getSystemToolDisplayInfo', () => { currentCost: 3, systemKeyCost: 2 }); - expect(tool).not.toHaveProperty('toolDescription'); }); it('keeps parent toolset display lightweight when list omits child icons', async () => { @@ -1059,14 +1031,12 @@ describe('SystemToolRepo.getSystemToolDisplayInfo', () => { version: '1.0.0', icon: 'weather.svg', tags: ['life'], - toolDescription: 'Weather tool', hasSecret: false, children: [ { id: 'forecast', name: { en: 'Forecast' }, - description: { en: 'Forecast intro' }, - toolDescription: 'Forecast tool' + description: { en: 'Forecast intro' } } ] } @@ -1111,14 +1081,12 @@ describe('SystemToolRepo.getSystemToolDisplayInfo', () => { version: '1.0.0', icon: 'weather.svg', tags: ['life'], - toolDescription: 'Weather tool', hasSecret: false, children: [ { id: 'forecast', name: { en: 'Forecast' }, - description: { en: 'Forecast intro' }, - toolDescription: 'Forecast tool' + description: { en: 'Forecast intro' } } ] } @@ -1132,14 +1100,12 @@ describe('SystemToolRepo.getSystemToolDisplayInfo', () => { version: '1.0.0', icon: 'weather.svg', tags: ['life'], - toolDescription: 'Weather tool', hasSecret: false, children: [ { id: 'forecast', name: { en: 'Forecast' }, description: { en: 'Forecast intro' }, - toolDescription: 'Forecast tool', icon: 'forecast.svg', inputSchema, outputSchema diff --git a/packages/service/test/core/app/tool/utils/client.test.ts b/packages/service/test/core/app/tool/utils/client.test.ts index 2005461fd296..5d304acd26be 100644 --- a/packages/service/test/core/app/tool/utils/client.test.ts +++ b/packages/service/test/core/app/tool/utils/client.test.ts @@ -203,7 +203,6 @@ describe('getClientToolPreviewNode', () => { intro: 'Weather query', author: 'FastGPT', tags: [], - toolDescription: 'Weather query', currentCost: 0, systemKeyCost: 0, hasTokenFee: false, diff --git a/packages/service/test/core/app/tool/utils/clientSystemTool.test.ts b/packages/service/test/core/app/tool/utils/clientSystemTool.test.ts index e13ffed6f99a..46e24cbbfc06 100644 --- a/packages/service/test/core/app/tool/utils/clientSystemTool.test.ts +++ b/packages/service/test/core/app/tool/utils/clientSystemTool.test.ts @@ -45,7 +45,6 @@ describe('getClientSystemToolPreviewNode', () => { intro: 'Weather query', author: 'FastGPT', tags: [], - toolDescription: 'Legacy resource description', currentCost: 0, systemKeyCost: 1, hasTokenFee: false, @@ -94,7 +93,6 @@ describe('getClientSystemToolPreviewNode', () => { ] }); expect(result.inputs[1]?.key).toBe('city'); - expect(result).not.toHaveProperty('toolDescription'); expect(result.version).toBe('1.0.0'); expect(result.versionLabel).toBe('1.0.0'); }); diff --git a/packages/service/test/core/workflow/dispatch/ai/agent/sub/tool/utils.test.ts b/packages/service/test/core/workflow/dispatch/ai/agent/sub/tool/utils.test.ts index 708b60c19c04..6f75e1009111 100644 --- a/packages/service/test/core/workflow/dispatch/ai/agent/sub/tool/utils.test.ts +++ b/packages/service/test/core/workflow/dispatch/ai/agent/sub/tool/utils.test.ts @@ -995,7 +995,6 @@ describe('getAgentRuntimeTools schema loading', () => { name: '秘塔搜索', avatar: 'metaso.png', intro: '搜索工具集', - toolDescription: '搜索工具集', status: 'active', source: 'system', isToolSet: true, @@ -1038,7 +1037,6 @@ describe('getAgentRuntimeTools schema loading', () => { name: '热榜工具', avatar: 'hot-list.png', intro: '获取热榜信息,支持36氪、知乎、微博、掘金、头条等多个平台', - toolDescription: '获取热榜信息', status: 'active', source: 'system', isToolSet: false, @@ -1072,7 +1070,6 @@ describe('getAgentRuntimeTools schema loading', () => { name: 'Search', avatar: 'search.png', intro: 'Search tool', - toolDescription: 'Search tool', status: 'active', source: 'system', isToolSet: false, @@ -1147,7 +1144,6 @@ describe('getAgentRuntimeTools schema loading', () => { name: 'Legacy tool', avatar: 'legacy.png', intro: 'Legacy tool', - toolDescription: 'Legacy tool', status: 'active', source: 'system', isToolSet: false, @@ -1188,7 +1184,6 @@ describe('getAgentRuntimeTools schema loading', () => { name: 'Workflow Tool', avatar: 'workflow.png', intro: 'Workflow tool', - toolDescription: 'Workflow tool', status: 'active', source: 'system', isToolSet: false, @@ -1226,7 +1221,6 @@ describe('getAgentRuntimeTools schema loading', () => { name: 'Workflow Tool', avatar: 'workflow.png', intro: 'Workflow tool', - toolDescription: 'Workflow tool', status: 'active', source: 'system', isToolSet: false, @@ -1289,7 +1283,6 @@ describe('getAgentRuntimeTools schema loading', () => { name: 'Workflow Tool', avatar: 'workflow.png', intro: 'Workflow tool', - toolDescription: 'Workflow tool', status: 'active', source: 'system', isToolSet: false, @@ -1374,7 +1367,6 @@ describe('getAgentRuntimeTools schema loading', () => { name: 'Workflow Tool', avatar: 'workflow.png', intro: 'Workflow tool', - toolDescription: 'Workflow tool', status: 'active', source: 'system', isToolSet: false, @@ -1420,7 +1412,6 @@ describe('getAgentRuntimeTools schema loading', () => { name: 'Workflow Tool', avatar: 'workflow.png', intro: 'Workflow tool', - toolDescription: 'Workflow tool', status: 'active', source: 'system', isToolSet: false, @@ -1487,7 +1478,6 @@ describe('getAgentRuntimeTools schema loading', () => { name: '热榜工具', avatar: 'hot-list.png', intro: '获取热榜信息', - toolDescription: '获取热榜信息', status: 'active', source: 'debug:tmbId:tmb-1', isToolSet: false, @@ -1540,7 +1530,6 @@ describe('getAgentRuntimeTools schema loading', () => { name: 'Weather', avatar: 'weather.png', intro: 'Weather', - toolDescription: 'Weather', status: 'active', source, isToolSet: false, @@ -1588,7 +1577,6 @@ describe('getAgentRuntimeTools schema loading', () => { name: 'Weather', avatar: 'weather.png', intro: 'Weather', - toolDescription: 'Weather', status: 'active', source, isToolSet: false, @@ -1625,7 +1613,6 @@ describe('getAgentRuntimeTools schema loading', () => { name: '热榜工具', avatar: 'hot-list.png', intro: '获取热榜信息,支持36氪、知乎、微博、掘金、头条等多个平台', - toolDescription: '获取热榜信息', status: 'active', source: 'system', isToolSet: false, diff --git a/packages/service/test/core/workflow/dispatch/ai/toolcall/hooks/useToolCatalog.test.ts b/packages/service/test/core/workflow/dispatch/ai/toolcall/hooks/useToolCatalog.test.ts index fa97947bfdda..7480b1c6af12 100644 --- a/packages/service/test/core/workflow/dispatch/ai/toolcall/hooks/useToolCatalog.test.ts +++ b/packages/service/test/core/workflow/dispatch/ai/toolcall/hooks/useToolCatalog.test.ts @@ -35,7 +35,6 @@ const createToolNode = (overrides: Record = {}) => name: 'Search', avatar: 'tool-avatar', intro: 'Search intro', - toolDescription: 'Search data', inputs: [], ...overrides }) as any; @@ -79,7 +78,6 @@ describe('useToolCatalog', () => { createToolNode({ nodeId: 'weather', name: 'Weather', - toolDescription: '', intro: 'Weather intro', inputs: [ { @@ -119,7 +117,7 @@ describe('useToolCatalog', () => { type: 'function', function: { name: 'search', - description: 'Search: Search data', + description: 'Search: Search intro', parameters: explicitSchema } }, diff --git a/packages/service/test/core/workflow/dispatch/ai/toolcall/hooks/useToolNodeList.test.ts b/packages/service/test/core/workflow/dispatch/ai/toolcall/hooks/useToolNodeList.test.ts index 0114c83d8c96..7e2956fd6a65 100644 --- a/packages/service/test/core/workflow/dispatch/ai/toolcall/hooks/useToolNodeList.test.ts +++ b/packages/service/test/core/workflow/dispatch/ai/toolcall/hooks/useToolNodeList.test.ts @@ -13,7 +13,6 @@ const createToolNode = (overrides: Record = {}) => flowNodeType: FlowNodeTypeEnum.tool, avatar: 'tool-avatar', intro: 'Search intro', - toolDescription: 'Search data', inputs: [], ...overrides }) as any; @@ -83,7 +82,6 @@ describe('useToolNodeList', () => { flowNodeType: FlowNodeTypeEnum.tool, avatar: 'tool-avatar', intro: 'Search intro', - toolDescription: 'Search data', jsonSchema: inputSchema, inputs: expect.arrayContaining([ expect.objectContaining({ diff --git a/packages/service/test/core/workflow/dispatch/ai/toolcall/nodeToolResponses.test.ts b/packages/service/test/core/workflow/dispatch/ai/toolcall/nodeToolResponses.test.ts index e6169969fefc..defaa007f81b 100644 --- a/packages/service/test/core/workflow/dispatch/ai/toolcall/nodeToolResponses.test.ts +++ b/packages/service/test/core/workflow/dispatch/ai/toolcall/nodeToolResponses.test.ts @@ -222,7 +222,6 @@ describe('newly connectable nodes as tools', () => { avatar: '', flowNodeType: FlowNodeTypeEnum.code, intro: '', - toolDescription: 'run code', inputs: codeNode.inputs } as any ], diff --git a/packages/service/test/core/workflow/dispatch/ai/toolcall/toolCall.test.ts b/packages/service/test/core/workflow/dispatch/ai/toolcall/toolCall.test.ts index a68665bd88a6..a8c919496780 100644 --- a/packages/service/test/core/workflow/dispatch/ai/toolcall/toolCall.test.ts +++ b/packages/service/test/core/workflow/dispatch/ai/toolcall/toolCall.test.ts @@ -544,7 +544,6 @@ describe('runToolCall compression node responses', () => { name: 'Search', flowNodeType: FlowNodeTypeEnum.tool, avatar: 'tool-avatar', - toolDescription: 'Search data', inputs: [] } ] @@ -807,7 +806,6 @@ describe('runToolCall compression node responses', () => { name: 'Dataset search', flowNodeType: FlowNodeTypeEnum.datasetSearchNode, avatar: 'dataset-avatar', - toolDescription: 'Search dataset', inputs: [] } ] @@ -884,7 +882,6 @@ describe('runToolCall compression node responses', () => { name: 'Search', flowNodeType: FlowNodeTypeEnum.tool, avatar: 'tool-avatar', - toolDescription: 'Search data', inputs: [] } ] diff --git a/packages/service/test/core/workflow/dispatch/ai/toolcall/toolProvider/createToolCallToolProvider.test.ts b/packages/service/test/core/workflow/dispatch/ai/toolcall/toolProvider/createToolCallToolProvider.test.ts index aba154132568..8d275aec84ad 100644 --- a/packages/service/test/core/workflow/dispatch/ai/toolcall/toolProvider/createToolCallToolProvider.test.ts +++ b/packages/service/test/core/workflow/dispatch/ai/toolcall/toolProvider/createToolCallToolProvider.test.ts @@ -23,7 +23,6 @@ const createToolNode = (overrides: Record = {}) => avatar: 'tool-avatar', flowNodeType: FlowNodeTypeEnum.tool, intro: 'Search intro', - toolDescription: 'Search data', inputs: [ { key: 'q', diff --git a/projects/app/test/api/core/app/tool/getSystemToolTemplates.test.ts b/projects/app/test/api/core/app/tool/getSystemToolTemplates.test.ts index 6c087928d8a9..6333c388fd19 100644 --- a/projects/app/test/api/core/app/tool/getSystemToolTemplates.test.ts +++ b/projects/app/test/api/core/app/tool/getSystemToolTemplates.test.ts @@ -84,7 +84,6 @@ describe('get system tool templates handler', () => { id: 'weather', name: 'Weather', intro: 'Forecast lookup', - toolDescription: 'Legacy resource description', isToolSet: false, status: PluginStatusEnum.Normal, tags: ['life'] @@ -120,7 +119,6 @@ describe('get system tool templates handler', () => { isTool: true, flowNodeType: FlowNodeTypeEnum.tool }); - expect(result[0]).not.toHaveProperty('toolDescription'); expect(mocks.getSystemToolList).toHaveBeenCalledWith({ lang: 'zh', op: 'or', @@ -251,7 +249,6 @@ describe('get system tool templates handler', () => { name: 'A+B Tool', status: PluginStatusEnum.Normal, description: 'Exact plus', - toolDescription: 'Legacy child resource description', icon: 'plus-icon', currentCost: 2, systemKeyCost: 0.5 @@ -284,7 +281,6 @@ describe('get system tool templates handler', () => { isTool: true, flowNodeType: FlowNodeTypeEnum.tool }); - expect(result[0]).not.toHaveProperty('toolDescription'); expect(mocks.getSystemToolDisplayInfoWithChildIcons).toHaveBeenCalledWith({ pluginId: 'toolset', lang: 'zh', diff --git a/projects/app/test/api/core/plugin/admin/tool/update.test.ts b/projects/app/test/api/core/plugin/admin/tool/update.test.ts index 37f3346c97ed..ef048c2e98e3 100644 --- a/projects/app/test/api/core/plugin/admin/tool/update.test.ts +++ b/projects/app/test/api/core/plugin/admin/tool/update.test.ts @@ -97,13 +97,12 @@ describe('admin system tool update handler', () => { ); }); - it('保存系统工具配置时丢弃历史资源级 toolDescription', async () => { + it('保存系统工具配置', async () => { mocks.findOne.mockResolvedValue({ pluginId: 'systemTool-weather', customConfig: { name: 'Weather', intro: 'Weather intro', - toolDescription: 'Legacy resource description', version: '1.0.0' } }); diff --git a/projects/app/test/api/core/plugin/team/tool/list.test.ts b/projects/app/test/api/core/plugin/team/tool/list.test.ts index 55b2ebe27a3e..fb41a9544acd 100644 --- a/projects/app/test/api/core/plugin/team/tool/list.test.ts +++ b/projects/app/test/api/core/plugin/team/tool/list.test.ts @@ -80,7 +80,6 @@ describe('team system plugin list handler', () => { intro: '', author: '', tags: [], - toolDescription: '', currentCost: 0, systemKeyCost: 0, hasTokenFee: false, @@ -145,7 +144,6 @@ describe('team system plugin list handler', () => { intro: '', author: '', tags: [], - toolDescription: '', currentCost: 0, systemKeyCost: 0, hasTokenFee: false, @@ -162,7 +160,6 @@ describe('team system plugin list handler', () => { intro: '', author: '', tags: [], - toolDescription: '', currentCost: 0, systemKeyCost: 0, hasTokenFee: false, @@ -225,7 +222,6 @@ describe('team system plugin list handler', () => { intro: '', author: '', tags: [], - toolDescription: '', currentCost: 0, systemKeyCost: 0, hasTokenFee: false, @@ -242,7 +238,6 @@ describe('team system plugin list handler', () => { intro: '', author: '', tags: [], - toolDescription: '', currentCost: 0, systemKeyCost: 0, hasTokenFee: false, @@ -259,7 +254,6 @@ describe('team system plugin list handler', () => { intro: '', author: '', tags: [], - toolDescription: '', currentCost: 0, systemKeyCost: 0, hasTokenFee: false, diff --git a/projects/app/test/pageComponents/app/detail/WorkflowComponents/utils.test.ts b/projects/app/test/pageComponents/app/detail/WorkflowComponents/utils.test.ts index 1ec90e750421..442bc113e8fe 100644 --- a/projects/app/test/pageComponents/app/detail/WorkflowComponents/utils.test.ts +++ b/projects/app/test/pageComponents/app/detail/WorkflowComponents/utils.test.ts @@ -81,7 +81,6 @@ describe('WorkflowComponents utils', () => { flowNodeType: FlowNodeTypeEnum.userInput, position: { x: 100, y: 100 } }); - expect(result.nodes[0]).not.toHaveProperty('toolDescription'); expect(result.edges).toHaveLength(1); expect(result.edges[0]).toMatchObject({ From b4491dd003a9c3ed4f7bbd2fb3746dce7077046c Mon Sep 17 00:00:00 2001 From: Nixieboluo Date: Thu, 3 Sep 2026 17:44:07 +0800 Subject: [PATCH 05/15] feat(workflow): special placeholder for toolset Signed-off-by: Nixieboluo --- packages/web/i18n/en/app.json | 1 + packages/web/i18n/ko-KR/app.json | 1 + packages/web/i18n/zh-CN/app.json | 1 + packages/web/i18n/zh-Hant/app.json | 1 + .../Flow/nodes/render/NodeCard.tsx | 13 ++++++++++--- 5 files changed, 14 insertions(+), 3 deletions(-) diff --git a/packages/web/i18n/en/app.json b/packages/web/i18n/en/app.json index 5a6807460af2..c174178d5ebe 100644 --- a/packages/web/i18n/en/app.json +++ b/packages/web/i18n/en/app.json @@ -421,6 +421,7 @@ "tool_params_description_tips": "Describe what the parameter does. When used in Tool calling, this description helps the model provide the correct value.", "tool_run_free": "This tool runs without points consumption", "tool_select": "Select tool", + "toolset_intro_placeholder": "The toolset description does not affect tool calling. If needed, edit the tool description directly.", "tool_soon_offset_tips": "This tool will be offline in the future. For the sake of your business stability, please replace it as soon as possible.", "tool_tip": "When run as a Tool, use this field as the Tool response?", "tool_type_tools": "tool", diff --git a/packages/web/i18n/ko-KR/app.json b/packages/web/i18n/ko-KR/app.json index d77a853a6821..18744352dc7d 100644 --- a/packages/web/i18n/ko-KR/app.json +++ b/packages/web/i18n/ko-KR/app.json @@ -421,6 +421,7 @@ "tool_params_description_tips": "파라미터 기능에 대한 설명이며, 도구 호출 파라미터로 사용될 경우 모델의 도구 호출 효과에 영향을 미칩니다.", "tool_run_free": "이 도구는 포인트 소비 없이 실행됩니다", "tool_select": "도구 선택", + "toolset_intro_placeholder": "도구 모음 설명은 도구 호출에 영향을 주지 않습니다. 필요한 경우 도구 설명을 직접 편집하세요.", "tool_soon_offset_tips": "이 도구는 향후 오프라인 처리될 예정입니다. 비즈니스 안정성을 위해 가능한 한 빨리 교체해 주세요.", "tool_tip": "도구로 실행될 때, 이 필드를 도구 응답 결과로 사용할지 여부", "tool_type_tools": "도구", diff --git a/packages/web/i18n/zh-CN/app.json b/packages/web/i18n/zh-CN/app.json index dc4d5be86a25..88250f52cb82 100644 --- a/packages/web/i18n/zh-CN/app.json +++ b/packages/web/i18n/zh-CN/app.json @@ -421,6 +421,7 @@ "tool_params_description_tips": "参数功能的描述,若作为工具调用参数,影响模型工具调用效果", "tool_run_free": "该工具运行无积分消耗", "tool_select": "选择工具", + "toolset_intro_placeholder": "工具集描述不影响工具调用效果。如需要,请直接编辑工具描述", "tool_soon_offset_tips": "该工具将在后续下线,为了您的业务稳定,请尽快替换", "tool_tip": "作为工具执行时,该字段是否作为工具响应结果", "tool_type_tools": "工具", diff --git a/packages/web/i18n/zh-Hant/app.json b/packages/web/i18n/zh-Hant/app.json index 684e8a1a6f76..8f4306ed9856 100644 --- a/packages/web/i18n/zh-Hant/app.json +++ b/packages/web/i18n/zh-Hant/app.json @@ -421,6 +421,7 @@ "tool_params_description_tips": "參數功能的描述,若作為工具調用參數,影響模型工具調用效果", "tool_run_free": "該工具運行無積分消耗", "tool_select": "選擇工具", + "toolset_intro_placeholder": "工具集描述不影響工具呼叫效果。如需要,請直接編輯工具描述", "tool_soon_offset_tips": "該工具將在後續下線,為了您的業務穩定,請盡快替換", "tool_tip": "作為工具執行時,該字段是否作為工具響應結果", "tool_type_tools": "工具", diff --git a/projects/app/src/pageComponents/app/detail/WorkflowComponents/Flow/nodes/render/NodeCard.tsx b/projects/app/src/pageComponents/app/detail/WorkflowComponents/Flow/nodes/render/NodeCard.tsx index 205c4d29b89f..5293707fac92 100644 --- a/projects/app/src/pageComponents/app/detail/WorkflowComponents/Flow/nodes/render/NodeCard.tsx +++ b/projects/app/src/pageComponents/app/detail/WorkflowComponents/Flow/nodes/render/NodeCard.tsx @@ -134,6 +134,7 @@ const NodeCard = (props: Props) => { inputs, rtDoms, pluginId, + flowNodeType, colorSchema } = props; @@ -455,7 +456,7 @@ const NodeCard = (props: Props) => { - + )} @@ -702,13 +703,19 @@ NodeTitleSection.displayName = 'NodeTitleSection'; // 节点介绍组件 const NodeIntro = React.memo(function NodeIntro({ nodeId, - intro = '' + intro = '', + flowNodeType }: { nodeId: string; intro?: string; + flowNodeType: FlowNodeTypeEnum; }) { const { t } = useTranslation(); const onChangeNode = useContextSelector(WorkflowActionsContext, (v) => v.onChangeNode); + const placeholder = + flowNodeType === FlowNodeTypeEnum.toolSet + ? t('app:toolset_intro_placeholder') + : t('app:node_not_intro'); const handleSave = useCallback( (newVal: string) => { @@ -733,7 +740,7 @@ const NodeIntro = React.memo(function NodeIntro({ onSave={handleSave} type={'textarea'} maxLength={500} - placeholder={t('app:node_not_intro')} + placeholder={placeholder} fontSize={'sm'} lineHeight={'short'} color={'myGray.500'} From fb1608b0663e95bdcde46812043ff9987b94817c Mon Sep 17 00:00:00 2001 From: Nixieboluo Date: Thu, 3 Sep 2026 17:56:14 +0800 Subject: [PATCH 06/15] chore: catch up pro Signed-off-by: Nixieboluo --- pro | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pro b/pro index 9cca902631f1..f4eff23329e5 160000 --- a/pro +++ b/pro @@ -1 +1 @@ -Subproject commit 9cca902631f116f9d0d861957f8a7f8487a83957 +Subproject commit f4eff23329e574cde512b24fd74ef848abe27274 From 6fcd1fbf9efa9838bbb88beaaf73813ae4691176 Mon Sep 17 00:00:00 2001 From: Nixieboluo Date: Tue, 8 Sep 2026 14:36:19 +0800 Subject: [PATCH 07/15] feat(workflow): editable tool desc in toolsets Signed-off-by: Nixieboluo --- packages/global/core/app/tool/utils.ts | 84 +++++++++++++++- .../global/test/core/app/tool/utils.test.ts | 88 +++++++++++++++++ packages/service/core/app/utils.ts | 6 +- .../core/workflow/dispatch/utils/index.ts | 25 ++++- packages/service/core/workflow/utils/index.ts | 8 +- .../test/core/workflow/dispatch/utils.test.ts | 59 ++++++++++++ .../test/core/workflow/utils/index.test.ts | 21 ++++ packages/web/i18n/en/app.json | 2 +- packages/web/i18n/ko-KR/app.json | 2 +- packages/web/i18n/zh-CN/app.json | 2 +- packages/web/i18n/zh-Hant/app.json | 2 +- .../Flow/nodes/NodeToolSet.tsx | 33 ++++++- .../Flow/nodes/components/ToolSetList.tsx | 96 +++++++++++++++---- .../Flow/nodes/render/InlineEdit.tsx | 20 +++- .../Flow/nodes/render/NodeCard.tsx | 28 ++++-- .../app/rewriteAppWorkflowToDetail.test.ts | 56 +++++++++++ 16 files changed, 494 insertions(+), 38 deletions(-) diff --git a/packages/global/core/app/tool/utils.ts b/packages/global/core/app/tool/utils.ts index 11e40fe2e409..952dce58b7ef 100644 --- a/packages/global/core/app/tool/utils.ts +++ b/packages/global/core/app/tool/utils.ts @@ -1,7 +1,7 @@ import { AppToolSourceEnum } from '../tool/constants'; import { NodeInputKeyEnum } from '../../workflow/constants'; import { FlowNodeTypeEnum } from '../../workflow/node/constant'; -import type { StoreNodeItemType } from '../../workflow/type/node'; +import type { NodeToolConfigType, StoreNodeItemType } from '../../workflow/type/node'; import type { SelectedToolItemType } from '../formEdit/type'; /** @@ -225,3 +225,85 @@ export const getToolNameCandidates = (toolName?: string) => { return candidates; }; + +/** 返回工具集子工具的有效描述;空白保存值继续使用工具定义默认值。 */ +export const getToolSetChildDescription = ( + savedDescription?: string, + definitionDescription = '' +) => (savedDescription?.trim() ? savedDescription : definitionDescription); + +/** + * 将当前节点保存的子工具描述合并到新模板配置。 + * + * 系统工具集用 `toolId` 匹配,MCP/HTTP 工具集用 `name` 匹配;只保留非空描述, + * 以便新增工具和历史空值继续使用新模板的工具定义描述。 + */ +export const mergeToolSetChildDescriptions = ({ + savedToolConfig, + templateToolConfig +}: { + savedToolConfig?: NodeToolConfigType; + templateToolConfig?: NodeToolConfigType; +}) => { + if (!templateToolConfig) return templateToolConfig; + + const mergeToolList = ({ + templateList, + savedList, + getKey + }: { + templateList: T[]; + savedList: T[] | undefined; + getKey: (tool: T) => string; + }) => { + const savedDescriptionMap = new Map(savedList?.map((tool) => [getKey(tool), tool.description])); + + return templateList.map((tool) => { + const description = getToolSetChildDescription( + savedDescriptionMap.get(getKey(tool)), + tool.description + ); + return description === tool.description ? tool : { ...tool, description }; + }); + }; + + return { + ...templateToolConfig, + ...(templateToolConfig.systemToolSet + ? { + systemToolSet: { + ...templateToolConfig.systemToolSet, + toolList: mergeToolList({ + templateList: templateToolConfig.systemToolSet.toolList, + savedList: savedToolConfig?.systemToolSet?.toolList, + getKey: (tool) => tool.toolId + }) + } + } + : {}), + ...(templateToolConfig.mcpToolSet + ? { + mcpToolSet: { + ...templateToolConfig.mcpToolSet, + toolList: mergeToolList({ + templateList: templateToolConfig.mcpToolSet.toolList, + savedList: savedToolConfig?.mcpToolSet?.toolList, + getKey: (tool) => tool.name + }) + } + } + : {}), + ...(templateToolConfig.httpToolSet + ? { + httpToolSet: { + ...templateToolConfig.httpToolSet, + toolList: mergeToolList({ + templateList: templateToolConfig.httpToolSet.toolList, + savedList: savedToolConfig?.httpToolSet?.toolList, + getKey: (tool) => tool.name + }) + } + } + : {}) + }; +}; diff --git a/packages/global/test/core/app/tool/utils.test.ts b/packages/global/test/core/app/tool/utils.test.ts index 83954f342287..470550664f4d 100644 --- a/packages/global/test/core/app/tool/utils.test.ts +++ b/packages/global/test/core/app/tool/utils.test.ts @@ -4,9 +4,11 @@ import { getToolIdentityKey, getToolNameCandidates, getToolRawId, + getToolSetChildDescription, hasDebugToolInNodes, hasDebugToolInSelectedTools, isTeamPluginSource, + mergeToolSetChildDescriptions, parseDebugToolSource, parseTeamPluginSource, parseToolsetToolId, @@ -17,6 +19,92 @@ import { import { AppToolSourceEnum } from '@fastgpt/global/core/app/tool/constants'; import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant'; +describe('tool set child descriptions', () => { + it('uses a definition description when the saved description is blank', () => { + expect(getToolSetChildDescription(' ', 'Definition description')).toBe( + 'Definition description' + ); + expect(getToolSetChildDescription(undefined, 'Definition description')).toBe( + 'Definition description' + ); + expect(getToolSetChildDescription(' Custom description ', 'Definition description')).toBe( + ' Custom description ' + ); + }); + + it('preserves saved system descriptions by tool id and external descriptions by name', () => { + const savedToolConfig = { + systemToolSet: { + toolId: 'system-tool-set', + toolList: [ + { toolId: 'search', name: 'Old search', description: 'Custom system description' }, + { toolId: 'removed', name: 'Removed', description: 'Removed description' } + ] + }, + mcpToolSet: { + url: 'https://example.com/mcp', + toolList: [ + { name: 'search', description: 'Custom MCP description' }, + { name: 'blank', description: ' ' } + ] + }, + httpToolSet: { + toolList: [ + { name: 'search', description: 'Custom HTTP description' }, + { name: 'blank', description: '' } + ] + } + } as any; + const templateToolConfig = { + systemToolSet: { + toolId: 'system-tool-set', + toolList: [ + { toolId: 'search', name: 'New search', description: 'System default' }, + { toolId: 'new', name: 'New', description: 'New system default' } + ] + }, + mcpToolSet: { + url: 'https://example.com/mcp', + toolList: [ + { name: 'search', description: 'MCP default' }, + { name: 'blank', description: 'Blank MCP default' }, + { name: 'new', description: 'New MCP default' } + ] + }, + httpToolSet: { + toolList: [ + { name: 'search', description: 'HTTP default' }, + { name: 'blank', description: 'Blank HTTP default' }, + { name: 'new', description: 'New HTTP default' } + ] + } + } as any; + + expect(mergeToolSetChildDescriptions({ savedToolConfig, templateToolConfig })).toMatchObject({ + systemToolSet: { + toolList: [ + { toolId: 'search', description: 'Custom system description' }, + { toolId: 'new', description: 'New system default' } + ] + }, + mcpToolSet: { + toolList: [ + { name: 'search', description: 'Custom MCP description' }, + { name: 'blank', description: 'Blank MCP default' }, + { name: 'new', description: 'New MCP default' } + ] + }, + httpToolSet: { + toolList: [ + { name: 'search', description: 'Custom HTTP description' }, + { name: 'blank', description: 'Blank HTTP default' }, + { name: 'new', description: 'New HTTP default' } + ] + } + }); + }); +}); + describe('shouldUseLegacyToolDescriptionFallback', () => { it('only enables fallback for workflow, system and commercial tools', () => { expect( diff --git a/packages/service/core/app/utils.ts b/packages/service/core/app/utils.ts index eda48d021e73..e765b3e510c8 100644 --- a/packages/service/core/app/utils.ts +++ b/packages/service/core/app/utils.ts @@ -21,6 +21,7 @@ import { ReadPermissionVal } from '@fastgpt/global/support/permission/constant'; import { getErrText } from '@fastgpt/global/common/error/utils'; import { isSystemOrCommercialToolId, + mergeToolSetChildDescriptions, splitCombineToolId } from '@fastgpt/global/core/app/tool/utils'; import { AgentToolInputModeEnum } from '@fastgpt/global/core/app/tool/constants'; @@ -266,7 +267,10 @@ export async function rewriteAppWorkflowToDetail({ node.intro = preview.intro; } - node.toolConfig = preview.toolConfig; + node.toolConfig = mergeToolSetChildDescriptions({ + savedToolConfig: node.toolConfig, + templateToolConfig: preview.toolConfig + }); // Latest version if (!node.version) { diff --git a/packages/service/core/workflow/dispatch/utils/index.ts b/packages/service/core/workflow/dispatch/utils/index.ts index fc51efb65b2c..1f85b029d654 100644 --- a/packages/service/core/workflow/dispatch/utils/index.ts +++ b/packages/service/core/workflow/dispatch/utils/index.ts @@ -42,6 +42,7 @@ import { import { jsonSchema2NodeInput } from '@fastgpt/global/core/app/jsonschema'; import { authAppByTmbId } from '../../../../support/permission/app/auth'; import { ReadPermissionVal } from '@fastgpt/global/support/permission/constant'; +import { getToolSetChildDescription } from '@fastgpt/global/core/app/tool/utils'; /** * 创建 runtime nodeResponse 的轻量汇总对象。 @@ -656,14 +657,24 @@ export const rewriteRuntimeWorkFlow = async ({ if (!app) continue; const toolList = (await getMCPChildren(app)) as RuntimeMcpTool[]; + const savedDescriptionMap = new Map( + (mcpToolsetVal.toolList ?? []).map((tool) => [tool.name, tool.description]) + ); toolList.forEach((tool, index) => { + const runtimeTool = { + ...tool, + description: getToolSetChildDescription( + savedDescriptionMap.get(tool.name), + tool.description + ) + }; const newToolNode = initToolSetChildNode( getMCPToolRuntimeNode({ nodeId: `${toolSetNode.nodeId}${index}`, toolSetId, toolsetName: toolSetNode.name, avatar: toolSetNode.avatar, - tool + tool: runtimeTool, }) ); nodes.push(newToolNode); @@ -680,10 +691,20 @@ export const rewriteRuntimeWorkFlow = async ({ const toolList = await getHTTPToolList(app); + const savedDescriptionMap = new Map( + (httpToolsetVal.toolList ?? []).map((tool) => [tool.name, tool.description]) + ); toolList.forEach((tool: HttpToolConfigType, index: number) => { + const runtimeTool = { + ...tool, + description: getToolSetChildDescription( + savedDescriptionMap.get(tool.name), + tool.description + ) + }; const newToolNode = initToolSetChildNode( getHTTPToolRuntimeNode({ - tool, + tool: runtimeTool, nodeId: `${toolSetNode.nodeId}${index}`, avatar: toolSetNode.avatar, toolSetId, diff --git a/packages/service/core/workflow/utils/index.ts b/packages/service/core/workflow/utils/index.ts index 6b70f9be7069..aaea17be1611 100644 --- a/packages/service/core/workflow/utils/index.ts +++ b/packages/service/core/workflow/utils/index.ts @@ -6,7 +6,11 @@ import { NodeInputKeyEnum } from '@fastgpt/global/core/workflow/constants'; import type { localeType } from '@fastgpt/global/common/i18n/type'; import { SystemToolRepo } from '../../app/tool/systemTool/systemTool.repo'; import { jsonSchema2NodeInput, jsonSchema2NodeOutput } from '@fastgpt/global/core/app/jsonschema'; -import { isDebugToolSource, isTeamPluginSource } from '@fastgpt/global/core/app/tool/utils'; +import { + getToolSetChildDescription, + isDebugToolSource, + isTeamPluginSource +} from '@fastgpt/global/core/app/tool/utils'; import { assertTeamPluginSourceAccess, getRawPluginIdFromSystemToolId @@ -98,7 +102,7 @@ export async function getSystemToolRunTimeNodeFromSystemToolset({ if (!child) return []; const pluginId = `${systemToolId}/${child.id}`; - const intro = selectedTool.description || child.description; + const intro = getToolSetChildDescription(selectedTool.description, child.description); const childInputs = jsonSchema2NodeInput({ jsonSchema: child.inputSchema, schemaType: 'systemTool' diff --git a/packages/service/test/core/workflow/dispatch/utils.test.ts b/packages/service/test/core/workflow/dispatch/utils.test.ts index ed7877189b6c..ff36ab4a542a 100644 --- a/packages/service/test/core/workflow/dispatch/utils.test.ts +++ b/packages/service/test/core/workflow/dispatch/utils.test.ts @@ -1055,6 +1055,7 @@ describe('rewriteRuntimeWorkFlow', () => { expect(nodes.find((n) => n.nodeId === 'ts2')).toBeUndefined(); expect(nodes.find((n) => n.nodeId === 'ts20')).toMatchObject({ + intro: 'desc', toolConfig: { mcpTool: { toolId: 'mcp-mcp-app-1/tool1' @@ -1209,10 +1210,68 @@ describe('rewriteRuntimeWorkFlow', () => { ]) ); expect(nodes.find((n) => n.nodeId === 'ts41')).toBeDefined(); + expect(nodes.find((n) => n.nodeId === 'ts40')?.intro).toBe('desc1'); + expect(nodes.find((n) => n.nodeId === 'ts41')?.intro).toBe('desc2'); expect(edges.filter((e) => e.target === 'ts40' || e.target === 'ts41').length).toBe(2); } ); + it('should prefer saved MCP and HTTP toolSet child descriptions', async () => { + const mcpToolSetNode = makeNode('mcpToolSet', FlowNodeTypeEnum.toolSet, { + pluginId: 'mcp-app-1', + name: 'MCP ToolSet', + toolConfig: { + mcpToolSet: { + toolList: [{ name: 'search', description: 'Saved MCP description' }] + } + } + } as any); + const httpToolSetNode = makeNode('httpToolSet', FlowNodeTypeEnum.toolSet, { + pluginId: 'http-app-1', + name: 'HTTP ToolSet', + toolConfig: { + httpToolSet: { + toolList: [{ name: 'search', description: 'Saved HTTP description' }] + } + } + } as any); + const nodes = [mcpToolSetNode, httpToolSetNode]; + const edges: RuntimeEdgeItemType[] = []; + + mockMongoAppFindOne.mockImplementation(({ _id }: { _id: string }) => ({ + lean: vi.fn().mockResolvedValue( + _id === 'mcp-app-1' + ? { + _id, + modules: [ + { toolConfig: { mcpToolSet: { url: 'https://mcp.example.com', toolList: [] } } } + ] + } + : { _id } + ) + })); + mockGetMCPChildren.mockResolvedValue([ + { name: 'search', description: 'Default MCP description', inputSchema: {} } + ]); + mockGetHTTPToolList.mockResolvedValue([ + { + name: 'search', + description: 'Default HTTP description', + path: '/search', + method: 'GET' + } + ]); + + await rewriteRuntimeWorkFlow({ teamId: 'team1', nodes, edges }); + + expect(nodes.find((node) => node.nodeId === 'mcpToolSet0')?.intro).toBe( + 'Saved MCP description' + ); + expect(nodes.find((node) => node.nodeId === 'httpToolSet0')?.intro).toBe( + 'Saved HTTP description' + ); + }); + // Helper: route MongoApp.find responses by the toolsetId it queries, since // parseMcpTool and parseHttpTool may both hit MongoApp.find in parallel. const setupFindByIdMap = (idToDoc: Record) => { diff --git a/packages/service/test/core/workflow/utils/index.test.ts b/packages/service/test/core/workflow/utils/index.test.ts index 05457eff8fcb..f94bbea73e01 100644 --- a/packages/service/test/core/workflow/utils/index.test.ts +++ b/packages/service/test/core/workflow/utils/index.test.ts @@ -327,6 +327,27 @@ describe('getSystemToolRunTimeNodeFromSystemToolset', () => { expect(result[0].intro).toBe('Original Intro'); }); + it('should use the child definition description when the saved description is blank', async () => { + const toolSetNode = makeToolSetNode({ + toolList: [{ toolId: 'child-1', name: 'Search', description: ' ' }] + }); + + mockToolDetail([ + { + id: 'child-1', + name: 'Search', + description: 'Definition description' + } + ]); + + const result = await getSystemToolRunTimeNodeFromSystemToolset({ + toolSetNode, + lang: 'en' + }); + + expect(result[0].intro).toBe('Definition description'); + }); + it('should pass systemInputConfig value to child tool inputs', async () => { const toolSetNode = makeToolSetNode({ toolList: [{ toolId: 'child-1', name: 'Tool1', description: 'Desc1' }], diff --git a/packages/web/i18n/en/app.json b/packages/web/i18n/en/app.json index c174178d5ebe..e26fbe26dcfd 100644 --- a/packages/web/i18n/en/app.json +++ b/packages/web/i18n/en/app.json @@ -421,7 +421,7 @@ "tool_params_description_tips": "Describe what the parameter does. When used in Tool calling, this description helps the model provide the correct value.", "tool_run_free": "This tool runs without points consumption", "tool_select": "Select tool", - "toolset_intro_placeholder": "The toolset description does not affect tool calling. If needed, edit the tool description directly.", + "toolset_intro_tips": "The toolset description does not affect tool calling. If needed, edit the tool description directly.", "tool_soon_offset_tips": "This tool will be offline in the future. For the sake of your business stability, please replace it as soon as possible.", "tool_tip": "When run as a Tool, use this field as the Tool response?", "tool_type_tools": "tool", diff --git a/packages/web/i18n/ko-KR/app.json b/packages/web/i18n/ko-KR/app.json index 18744352dc7d..71d1d6f7dc56 100644 --- a/packages/web/i18n/ko-KR/app.json +++ b/packages/web/i18n/ko-KR/app.json @@ -421,7 +421,7 @@ "tool_params_description_tips": "파라미터 기능에 대한 설명이며, 도구 호출 파라미터로 사용될 경우 모델의 도구 호출 효과에 영향을 미칩니다.", "tool_run_free": "이 도구는 포인트 소비 없이 실행됩니다", "tool_select": "도구 선택", - "toolset_intro_placeholder": "도구 모음 설명은 도구 호출에 영향을 주지 않습니다. 필요한 경우 도구 설명을 직접 편집하세요.", + "toolset_intro_tips": "도구 모음 설명은 도구 호출에 영향을 주지 않습니다. 필요한 경우 도구 설명을 직접 편집하세요.", "tool_soon_offset_tips": "이 도구는 향후 오프라인 처리될 예정입니다. 비즈니스 안정성을 위해 가능한 한 빨리 교체해 주세요.", "tool_tip": "도구로 실행될 때, 이 필드를 도구 응답 결과로 사용할지 여부", "tool_type_tools": "도구", diff --git a/packages/web/i18n/zh-CN/app.json b/packages/web/i18n/zh-CN/app.json index 88250f52cb82..e03ced5b63f3 100644 --- a/packages/web/i18n/zh-CN/app.json +++ b/packages/web/i18n/zh-CN/app.json @@ -421,7 +421,7 @@ "tool_params_description_tips": "参数功能的描述,若作为工具调用参数,影响模型工具调用效果", "tool_run_free": "该工具运行无积分消耗", "tool_select": "选择工具", - "toolset_intro_placeholder": "工具集描述不影响工具调用效果。如需要,请直接编辑工具描述", + "toolset_intro_tips": "工具集描述不影响工具调用效果。如需要,请直接编辑工具描述", "tool_soon_offset_tips": "该工具将在后续下线,为了您的业务稳定,请尽快替换", "tool_tip": "作为工具执行时,该字段是否作为工具响应结果", "tool_type_tools": "工具", diff --git a/packages/web/i18n/zh-Hant/app.json b/packages/web/i18n/zh-Hant/app.json index 8f4306ed9856..a60dcd7235c7 100644 --- a/packages/web/i18n/zh-Hant/app.json +++ b/packages/web/i18n/zh-Hant/app.json @@ -421,7 +421,7 @@ "tool_params_description_tips": "參數功能的描述,若作為工具調用參數,影響模型工具調用效果", "tool_run_free": "該工具運行無積分消耗", "tool_select": "選擇工具", - "toolset_intro_placeholder": "工具集描述不影響工具呼叫效果。如需要,請直接編輯工具描述", + "toolset_intro_tips": "工具集描述不影響工具呼叫效果。如需要,請直接編輯工具描述", "tool_soon_offset_tips": "該工具將在後續下線,為了您的業務穩定,請盡快替換", "tool_tip": "作為工具執行時,該字段是否作為工具響應結果", "tool_type_tools": "工具", diff --git a/projects/app/src/pageComponents/app/detail/WorkflowComponents/Flow/nodes/NodeToolSet.tsx b/projects/app/src/pageComponents/app/detail/WorkflowComponents/Flow/nodes/NodeToolSet.tsx index 558c36ee0319..e5da09e3ab1c 100644 --- a/projects/app/src/pageComponents/app/detail/WorkflowComponents/Flow/nodes/NodeToolSet.tsx +++ b/projects/app/src/pageComponents/app/detail/WorkflowComponents/Flow/nodes/NodeToolSet.tsx @@ -1,21 +1,52 @@ import { type FlowNodeItemType } from '@fastgpt/global/core/workflow/type/node'; -import React from 'react'; +import React, { useCallback } from 'react'; import { type NodeProps } from 'reactflow'; import NodeCard from './render/NodeCard'; import Container from '../components/Container'; import IOTitle from '../components/IOTitle'; import ToolSetList, { getNodeToolSetList } from './components/ToolSetList'; import { useTranslation } from 'next-i18next'; +import { useContextSelector } from 'use-context-selector'; +import { WorkflowActionsContext } from '../../context/workflowActionsContext'; const NodeToolSet = ({ data, selected }: NodeProps) => { const { t } = useTranslation(); const toolList = getNodeToolSetList(data); + const onChangeNode = useContextSelector(WorkflowActionsContext, (v) => v.onChangeNode); + const onSaveDescription = useCallback( + (index: number, description: string) => { + const toolSetKey = (['mcpToolSet', 'httpToolSet', 'systemToolSet'] as const).find( + (key) => data.toolConfig?.[key] + ); + if (!toolSetKey || !data.toolConfig) return; + + const toolSet = data.toolConfig[toolSetKey]; + if (!toolSet) return; + + onChangeNode({ + nodeId: data.nodeId, + type: 'attr', + key: 'toolConfig', + value: { + ...data.toolConfig, + [toolSetKey]: { + ...toolSet, + toolList: toolSet.toolList.map((tool, toolIndex) => + toolIndex === index ? { ...tool, description } : tool + ) + } + } + }); + }, + [data.nodeId, data.toolConfig, onChangeNode] + ); return ( } /> diff --git a/projects/app/src/pageComponents/app/detail/WorkflowComponents/Flow/nodes/components/ToolSetList.tsx b/projects/app/src/pageComponents/app/detail/WorkflowComponents/Flow/nodes/components/ToolSetList.tsx index 06da54a0a3c5..58f190c77685 100644 --- a/projects/app/src/pageComponents/app/detail/WorkflowComponents/Flow/nodes/components/ToolSetList.tsx +++ b/projects/app/src/pageComponents/app/detail/WorkflowComponents/Flow/nodes/components/ToolSetList.tsx @@ -1,7 +1,8 @@ -import React from 'react'; -import { Box, Flex } from '@chakra-ui/react'; +import React, { useCallback, useRef, useState } from 'react'; +import { Box, Flex, Textarea } from '@chakra-ui/react'; import { useTranslation } from 'next-i18next'; import type { FlowNodeTemplateType } from '@fastgpt/global/core/workflow/type/node'; +import MyIconButton from '@fastgpt/web/components/common/Icon/button'; type ToolSetListItemType = { name: string; @@ -21,12 +22,32 @@ export const getNodeToolSetList = (tool: Pick void; }) => { const { t } = useTranslation(); + const [editingIndex, setEditingIndex] = useState(); + const [editingDescription, setEditingDescription] = useState(''); + const isCancellingRef = useRef(false); + + const handleSave = useCallback(() => { + if (editingIndex === undefined) return; + if (isCancellingRef.current) { + isCancellingRef.current = false; + return; + } + + onSaveDescription(editingIndex, editingDescription); + setEditingIndex(undefined); + }, [editingDescription, editingIndex, onSaveDescription]); + const handleCancel = useCallback(() => { + isCancellingRef.current = true; + setEditingIndex(undefined); + }, []); return ( <> @@ -37,14 +58,20 @@ const ToolSetList = ({ key={`${tool.name}-${index}`} borderBottom={'1px solid'} borderColor={'myGray.200'} - alignItems={'center'} py={2} px={3} > - + {index + 1 < 10 ? `0${index + 1}` : index + 1} - + {tool.name} - - {tool.description || t('app:tools_no_description')} - + + {editingIndex === index ? ( +