diff --git a/document/data/doc-last-modified.json b/document/data/doc-last-modified.json index b8bd56f492f6..e37ad3e1942b 100644 --- a/document/data/doc-last-modified.json +++ b/document/data/doc-last-modified.json @@ -151,16 +151,16 @@ "content/guide/workspace/team/invitation_link.mdx": "2026-05-07T15:06:40+08:00", "content/guide/workspace/team/team_roles_permissions.en.mdx": "2026-08-15T23:40:17+08:00", "content/guide/workspace/team/team_roles_permissions.mdx": "2026-05-07T15:06:40+08:00", - "content/openapi/app.en.mdx": "2026-09-10T11:39:40+08:00", - "content/openapi/app.mdx": "2026-09-10T11:39:40+08:00", - "content/openapi/chat.en.mdx": "2026-09-10T11:39:40+08:00", - "content/openapi/chat.mdx": "2026-09-10T11:39:40+08:00", - "content/openapi/dataset.en.mdx": "2026-09-10T11:39:40+08:00", - "content/openapi/dataset.mdx": "2026-09-10T11:39:40+08:00", + "content/openapi/app.en.mdx": "2026-08-15T23:40:17+08:00", + "content/openapi/app.mdx": "2026-05-29T19:31:16+08:00", + "content/openapi/chat.en.mdx": "2026-08-15T23:40:17+08:00", + "content/openapi/chat.mdx": "2026-07-08T22:37:19+08:00", + "content/openapi/dataset.en.mdx": "2026-08-15T23:40:17+08:00", + "content/openapi/dataset.mdx": "2026-08-11T20:41:16+08:00", "content/openapi/index.en.mdx": "2026-04-26T21:08:47+08:00", "content/openapi/index.mdx": "2026-04-26T21:08:47+08:00", - "content/openapi/intro.en.mdx": "2026-09-10T11:39:40+08:00", - "content/openapi/intro.mdx": "2026-09-10T11:39:40+08:00", + "content/openapi/intro.en.mdx": "2026-08-15T23:40:17+08:00", + "content/openapi/intro.mdx": "2026-08-25T13:41:03+08:00", "content/plugin/index.en.mdx": "2026-06-04T16:10:15+08:00", "content/plugin/index.mdx": "2026-06-04T16:10:15+08:00", "content/plugin/intro.en.mdx": "2026-08-18T18:05:49+08:00", @@ -171,8 +171,8 @@ "content/plugin/system-tool-development.mdx": "2026-07-02T11:54:55+08:00", "content/plugin/team-installation.en.mdx": "2026-09-02T20:13:11+08:00", "content/plugin/team-installation.mdx": "2026-09-02T20:13:11+08:00", - "content/self-host/config/env.en.mdx": "2026-09-09T22:57:06+08:00", - "content/self-host/config/env.mdx": "2026-09-09T22:57:06+08:00", + "content/self-host/config/env.en.mdx": "2026-09-03T21:50:14+08:00", + "content/self-host/config/env.mdx": "2026-09-03T21:50:14+08:00", "content/self-host/config/model/intro.en.mdx": "2026-08-24T17:55:53+08:00", "content/self-host/config/model/intro.mdx": "2026-08-24T17:55:53+08:00", "content/self-host/config/model/minimax.en.mdx": "2026-08-15T23:40:17+08:00", @@ -201,12 +201,12 @@ "content/self-host/custom-models/marker.mdx": "2026-04-26T21:08:47+08:00", "content/self-host/custom-models/mineru.en.mdx": "2026-08-15T23:40:17+08:00", "content/self-host/custom-models/mineru.mdx": "2026-04-26T21:08:47+08:00", - "content/self-host/custom-models/ollama.en.mdx": "2026-09-09T22:57:06+08:00", - "content/self-host/custom-models/ollama.mdx": "2026-09-09T22:57:06+08:00", + "content/self-host/custom-models/ollama.en.mdx": "2026-08-24T17:55:53+08:00", + "content/self-host/custom-models/ollama.mdx": "2026-08-24T17:55:53+08:00", "content/self-host/custom-models/xinference.en.mdx": "2026-08-15T23:40:17+08:00", "content/self-host/custom-models/xinference.mdx": "2026-04-26T21:08:47+08:00", - "content/self-host/deploy/docker.en.mdx": "2026-09-07T23:20:55+08:00", - "content/self-host/deploy/docker.mdx": "2026-09-07T23:20:55+08:00", + "content/self-host/deploy/docker.en.mdx": "2026-09-07T00:55:30+08:00", + "content/self-host/deploy/docker.mdx": "2026-09-07T00:55:30+08:00", "content/self-host/deploy/sealos.en.mdx": "2026-08-24T10:27:24+08:00", "content/self-host/deploy/sealos.mdx": "2026-08-24T10:27:24+08:00", "content/self-host/design/dataset.en.mdx": "2026-08-15T23:40:17+08:00", @@ -355,8 +355,8 @@ "content/self-host/upgrading/4-16/4161.mdx": "2026-08-24T10:06:28+08:00", "content/self-host/upgrading/4-16/4162.en.mdx": "2026-09-03T21:50:14+08:00", "content/self-host/upgrading/4-16/4162.mdx": "2026-09-03T21:50:14+08:00", - "content/self-host/upgrading/4-17/4170.en.mdx": "2026-09-09T22:57:06+08:00", - "content/self-host/upgrading/4-17/4170.mdx": "2026-09-09T22:57:06+08:00", + "content/self-host/upgrading/4-17/4170.en.mdx": "2026-09-07T20:09:57+08:00", + "content/self-host/upgrading/4-17/4170.mdx": "2026-09-07T20:09:57+08:00", "content/self-host/upgrading/outdated/40.en.mdx": "2026-08-16T23:16:43+08:00", "content/self-host/upgrading/outdated/40.mdx": "2026-08-16T23:16:43+08:00", "content/self-host/upgrading/outdated/41.en.mdx": "2026-08-16T23:16:43+08:00", 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..ee8192f06e12 100644 --- a/packages/global/core/app/tool/systemTool/type/base.ts +++ b/packages/global/core/app/tool/systemTool/type/base.ts @@ -48,7 +48,10 @@ 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: '给工具调用使用的工具的描述' }), + toolDescription: z.string().optional().meta({ + description: '已废弃:节点作为工具被调用时的能力说明', + deprecated: true + }), userGuide: z.string().nullish().meta({ description: '工具的使用指南(markdown 纯文本)' }), readmeUrl: z.string().optional().meta({ description: '工具的 README 地址' }), @@ -79,7 +82,10 @@ export const SystemToolChildDetailSchema = z.object({ name: z.string(), status: PluginStatusSchema.meta({ description: '工具的状态' }), description: z.string().optional(), - toolDescription: z.string().optional(), + toolDescription: z.string().optional().meta({ + description: '已废弃:节点作为工具被调用时的能力说明', + deprecated: true + }), icon: z.string().optional(), currentCost: z.number().meta({ description: '当前使用的费用' }), systemKeyCost: z.number().meta({ description: '系统密钥的费用' }), diff --git a/packages/global/core/app/tool/utils.ts b/packages/global/core/app/tool/utils.ts index 11e40fe2e409..cb1386f72249 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 | null | false | 0, + definitionDescription = '' +) => (savedDescription === '' ? savedDescription : 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 + }) + } + } + : {}) + } as NodeToolConfigType; +}; diff --git a/packages/global/core/plugin/tool/type.ts b/packages/global/core/plugin/tool/type.ts index 3df6965f4470..1dc1faeb5480 100644 --- a/packages/global/core/plugin/tool/type.ts +++ b/packages/global/core/plugin/tool/type.ts @@ -22,7 +22,10 @@ export const SystemPluginToolCollectionSchema = SystemToolBasicConfigSchema.exte name: z.string(), avatar: z.string().optional(), intro: z.string().optional(), - toolDescription: z.string().optional(), + toolDescription: z.string().optional().meta({ + description: '已废弃:节点作为工具被调用时的能力说明', + deprecated: true + }), version: z.string(), tags: z.array(z.string()).nullish(), associatedPluginId: z.string().optional(), diff --git a/packages/global/core/workflow/runtime/type.ts b/packages/global/core/workflow/runtime/type.ts index 26873c09ba76..3159a35a437d 100644 --- a/packages/global/core/workflow/runtime/type.ts +++ b/packages/global/core/workflow/runtime/type.ts @@ -18,6 +18,7 @@ export type RuntimeNodeItemType = { name: StoreNodeItemType['name']; avatar?: StoreNodeItemType['avatar']; intro?: StoreNodeItemType['intro']; + /** @deprecated Unused now in favor of `intro` in node data. */ toolDescription?: StoreNodeItemType['toolDescription']; flowNodeType: StoreNodeItemType['flowNodeType']; showStatus?: StoreNodeItemType['showStatus']; 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/index.ts b/packages/global/core/workflow/type/index.ts index 36c62c3db858..53bcfc1f0087 100644 --- a/packages/global/core/workflow/type/index.ts +++ b/packages/global/core/workflow/type/index.ts @@ -21,7 +21,10 @@ export const WorkflowTemplateTypeSchema = z.object({ avatar: z.string().optional(), name: z.union([I18nStringSchema, z.string()]), intro: z.union([I18nStringSchema, z.string()]).optional(), - toolDescription: z.string().optional(), + toolDescription: z.string().optional().meta({ + description: '已废弃:节点作为工具被调用时的能力说明', + deprecated: true + }), author: z.string().optional(), courseUrl: z.string().optional(), diff --git a/packages/global/core/workflow/type/node.ts b/packages/global/core/workflow/type/node.ts index 64d62a50f666..3660e9e4a659 100644 --- a/packages/global/core/workflow/type/node.ts +++ b/packages/global/core/workflow/type/node.ts @@ -179,7 +179,10 @@ 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 + toolDescription: z.string().optional().meta({ + description: '已废弃:节点作为工具被调用时的能力说明', + deprecated: true + }), showStatus: BoolSchema.optional(), // chatting response step status version: z.string().optional(), // version @@ -282,6 +285,10 @@ export const NodeTemplateListItemTypeSchema = z.object({ avatar: z.string().optional(), name: z.string(), intro: z.string().optional(), // template list intro + toolDescription: z.string().optional().meta({ + description: '已废弃:节点作为工具被调用时的能力说明', + deprecated: true + }), isTool: BoolSchema.optional(), hasToolInput: BoolSchema.optional(), diff --git a/packages/global/openapi/core/app/tool/api.ts b/packages/global/openapi/core/app/tool/api.ts index 5c09fb611d13..94abba8a4234 100644 --- a/packages/global/openapi/core/app/tool/api.ts +++ b/packages/global/openapi/core/app/tool/api.ts @@ -49,8 +49,9 @@ export const GetToolSetChildrenResponseSchema = z.object({ export type GetToolSetChildrenResponseType = z.infer; const ToolNodeTemplateListItemSchema = NodeTemplateListItemTypeSchema.extend({ - toolDescription: z.string().optional().meta({ - description: '工具调用描述' + toolDescription: NodeTemplateListItemTypeSchema.shape.toolDescription.meta({ + description: '已废弃:节点作为工具被调用时的能力说明', + deprecated: true }) }).catchall(z.any()); diff --git a/packages/global/openapi/core/workflow/node.ts b/packages/global/openapi/core/workflow/node.ts index 5013c4632a62..7701e1cc4743 100644 --- a/packages/global/openapi/core/workflow/node.ts +++ b/packages/global/openapi/core/workflow/node.ts @@ -130,8 +130,9 @@ export const OpenAPIStoreNodeItemTypeSchema = StoreNodeItemTypeSchema.omit({ intro: z.string().optional().meta({ description: '节点简介' }), - toolDescription: z.string().optional().meta({ - description: '节点作为工具被调用时的能力说明' + toolDescription: StoreNodeItemTypeSchema.shape.toolDescription.meta({ + description: '已废弃:节点作为工具被调用时的能力说明', + deprecated: true }), showStatus: BoolSchema.optional().meta({ description: '对话运行时是否展示该节点执行状态' 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..6434a41bb9b1 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, diff --git a/packages/global/test/core/app/tool/systemToolConfig.test.ts b/packages/global/test/core/app/tool/systemToolConfig.test.ts index 89fdd775888e..104ea2499879 100644 --- a/packages/global/test/core/app/tool/systemToolConfig.test.ts +++ b/packages/global/test/core/app/tool/systemToolConfig.test.ts @@ -1,7 +1,7 @@ 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 { type SystemPluginToolCollectionType } from '@fastgpt/global/core/plugin/tool/type'; describe('system tool config', () => { it('allows null secretsVal to explicitly disable system secret', () => { 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/app/tool/utils.test.ts b/packages/global/test/core/app/tool/utils.test.ts index 83954f342287..0a227434e44b 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,98 @@ 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 definitions for falsy values except an explicitly empty description', () => { + expect(getToolSetChildDescription(undefined, 'Definition description')).toBe( + 'Definition description' + ); + expect(getToolSetChildDescription(null, 'Definition description')).toBe( + 'Definition description' + ); + expect(getToolSetChildDescription(false, 'Definition description')).toBe( + 'Definition description' + ); + expect(getToolSetChildDescription(0, 'Definition description')).toBe('Definition description'); + expect(getToolSetChildDescription('', 'Definition description')).toBe(''); + expect(getToolSetChildDescription(' ', 'Definition description')).toBe(' '); + 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: ' ' }, + { name: 'new', description: 'New MCP default' } + ] + }, + httpToolSet: { + toolList: [ + { name: 'search', description: 'Custom HTTP description' }, + { name: 'blank', description: '' }, + { name: 'new', description: 'New HTTP default' } + ] + } + }); + }); +}); + describe('shouldUseLegacyToolDescriptionFallback', () => { it('only enables fallback for workflow, system and commercial tools', () => { expect( diff --git a/packages/global/test/core/workflow/migration/schema.test.ts b/packages/global/test/core/workflow/migration/schema.test.ts index 58e24e7e3789..b983f5034611 100644 --- a/packages/global/test/core/workflow/migration/schema.test.ts +++ b/packages/global/test/core/workflow/migration/schema.test.ts @@ -117,6 +117,48 @@ describe('workflow migration boundary', () => { expect(result.nodes[0].inputs[1]).not.toHaveProperty('isToolParam'); }); + it('keeps node and input descriptions', async () => { + const result = await migrateWorkflowToCurrent({ + nodes: [ + { + nodeId: 'chat-1', + flowNodeType: 'chatNode', + name: 'Chat', + intro: 'Node intro', + inputs: [ + { + key: 'query', + label: 'Query', + renderTypeList: [FlowNodeInputTypeEnum.input], + toolDescription: 'Input description' + } + ], + outputs: [] + } + ] + }); + + expect(result.nodes[0]).toMatchObject({ intro: 'Node intro' }); + expect(result.nodes[0].inputs[0].toolDescription).toBe('Input description'); + }); + + it('cleans null legacy node tool descriptions', async () => { + const result = await migrateWorkflowToCurrent({ + nodes: [ + { + nodeId: 'chat-1', + flowNodeType: 'chatNode', + name: 'Chat', + toolDescription: null, + inputs: [], + outputs: [] + } + ] + }); + + expect(result.nodes[0]).not.toHaveProperty('toolDescription'); + }); + 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/systemTool/systemTool.repo.ts b/packages/service/core/app/tool/systemTool/systemTool.repo.ts index d55288fd8c9e..5dc6d7c058c9 100644 --- a/packages/service/core/app/tool/systemTool/systemTool.repo.ts +++ b/packages/service/core/app/tool/systemTool/systemTool.repo.ts @@ -421,7 +421,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 +561,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 +612,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 +678,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 +695,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/app/tool/utils/client.ts b/packages/service/core/app/tool/utils/client.ts index a6bfc6365d08..333d397656dc 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, @@ -496,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(), @@ -505,8 +508,7 @@ export async function getClientToolPreviewNode({ flowNodeType, avatar: app.avatar, name: parseI18nString(app.name, lang), - intro: parseI18nString(app.intro, lang), - toolDescription: app.toolDescription, + 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 aaa36096f128..041fd067b43d 100644 --- a/packages/service/core/app/utils.ts +++ b/packages/service/core/app/utils.ts @@ -2,6 +2,7 @@ import { getDatasetModelReference } from '../dataset/model'; import { MongoDataset } from '../dataset/schema'; 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'; @@ -20,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'; @@ -279,8 +281,19 @@ export async function rewriteAppWorkflowToDetail({ node.hasTokenFee = preview.hasTokenFee; node.hasSystemSecret = preview.hasSystemSecret; - node.toolConfig = preview.toolConfig; - node.toolDescription = preview.toolDescription; + const { source } = splitCombineToolId(toolId); + if ( + (source === AppToolSourceEnum.mcp || source === AppToolSourceEnum.http) && + node.intro !== '' && + !node.intro + ) { + node.intro = preview.intro; + } + + node.toolConfig = mergeToolSetChildDescriptions({ + savedToolConfig: node.toolConfig, + templateToolConfig: preview.toolConfig + }); // Latest version if (!node.version) { 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/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/agent/type.ts b/packages/service/core/workflow/dispatch/ai/agent/type.ts index 276f5bd59e34..8367cf936191 100644 --- a/packages/service/core/workflow/dispatch/ai/agent/type.ts +++ b/packages/service/core/workflow/dispatch/ai/agent/type.ts @@ -23,7 +23,10 @@ export const SubAppRuntimeSchema = z.object({ id: z.string(), name: z.string(), avatar: z.string().optional(), - toolDescription: z.string().optional(), + toolDescription: z.string().optional().meta({ + description: '已废弃:节点作为工具被调用时的能力说明', + deprecated: true + }), version: z.string().optional(), toolConfig: NodeToolConfigTypeSchema.optional(), inputs: z.custom().optional(), @@ -35,5 +38,6 @@ export type SubAppRuntimeType = z.infer; export type GetSubAppInfoFnType = (id: string) => { name: string; avatar: string; + /** @deprecated Unused now in favor of `intro` in node data. */ toolDescription: string; }; diff --git a/packages/service/core/workflow/dispatch/ai/agentLoopCore/domain/toolInfo.ts b/packages/service/core/workflow/dispatch/ai/agentLoopCore/domain/toolInfo.ts index 4c5bacf06fa3..765dfb125e61 100644 --- a/packages/service/core/workflow/dispatch/ai/agentLoopCore/domain/toolInfo.ts +++ b/packages/service/core/workflow/dispatch/ai/agentLoopCore/domain/toolInfo.ts @@ -3,6 +3,7 @@ import type { localeType } from '@fastgpt/global/common/i18n/type'; export type AgentLoopCoreToolDisplayInfo = { name: string; avatar?: string; + /** @deprecated Unused now in favor of `intro` in node data. */ toolDescription?: string; }; 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..53c54d894a2a 100644 --- a/packages/service/core/workflow/dispatch/ai/toolcall/type.ts +++ b/packages/service/core/workflow/dispatch/ai/toolcall/type.ts @@ -48,6 +48,7 @@ export type ToolNodeItemType = { name: RuntimeNodeItemType['name']; avatar?: RuntimeNodeItemType['avatar']; intro?: RuntimeNodeItemType['intro']; + /** @deprecated Unused now in favor of `intro` in node data. */ toolDescription?: RuntimeNodeItemType['toolDescription']; flowNodeType: RuntimeNodeItemType['flowNodeType']; diff --git a/packages/service/core/workflow/dispatch/utils/index.ts b/packages/service/core/workflow/dispatch/utils/index.ts index 8d7e7b23697c..2225797c8a0b 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, @@ -747,7 +768,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; + // 空字符串代表用户主动清空,历史缺失值才使用工具定义描述。 + if (node.intro !== '' && !node.intro) { + node.intro = toolRaw.description; + } mergeToolNodeInputs({ node, jsonSchema: toolRaw.inputSchema, schemaType: 'mcp' }); }); }; @@ -788,7 +812,10 @@ export const rewriteRuntimeWorkFlow = async ({ if (!toolRaw) return; const { inputSchema, requestSchema } = getHTTPToolRuntimeSchemas(toolRaw); node.jsonSchema = requestSchema; - node.intro = toolRaw.description; + // 空字符串代表用户主动清空,历史缺失值才使用工具定义描述。 + if (node.intro !== '' && !node.intro) { + node.intro = toolRaw.description; + } mergeToolNodeInputs({ node, jsonSchema: inputSchema, diff --git a/packages/service/core/workflow/utils/index.ts b/packages/service/core/workflow/utils/index.ts index a893bd0c9e78..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,8 +102,7 @@ export async function getSystemToolRunTimeNodeFromSystemToolset({ if (!child) return []; const pluginId = `${systemToolId}/${child.id}`; - const intro = selectedTool.description || child.description; - const toolDescription = selectedTool.description || child.toolDescription || child.description; + const intro = getToolSetChildDescription(selectedTool.description, child.description); const childInputs = jsonSchema2NodeInput({ jsonSchema: child.inputSchema, schemaType: 'systemTool' @@ -116,7 +119,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/app/tool/systemTool/systemTool.repo.test.ts b/packages/service/test/core/app/tool/systemTool/systemTool.repo.test.ts index f0fc1ddf7f65..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 = ({ @@ -414,7 +413,6 @@ describe('SystemToolRepo.getSystemToolDetail', () => { version: '1.0.0', icon: 'weather.svg', tags: [], - toolDescription: 'Weather tool', inputSchema, outputSchema, secretSchema, @@ -423,7 +421,6 @@ describe('SystemToolRepo.getSystemToolDetail', () => { id: 'forecast', name: { en: 'Forecast' }, description: { en: 'Forecast intro' }, - toolDescription: 'Forecast tool', inputSchema, outputSchema } @@ -472,7 +469,6 @@ describe('SystemToolRepo.getSystemToolDetail', () => { version: '1.0.0', icon: 'weather.svg', tags: [], - toolDescription: 'Weather tool', hasSecret: true, secretSchema: { type: 'object', @@ -482,8 +478,7 @@ describe('SystemToolRepo.getSystemToolDetail', () => { { id: 'forecast', name: { en: 'Forecast' }, - description: { en: 'Forecast intro' }, - toolDescription: 'Forecast tool' + description: { en: 'Forecast intro' } } ] }); @@ -512,7 +507,6 @@ describe('SystemToolRepo.getSystemToolDetail', () => { version: '1.0.0', icon: 'weather.svg', tags: [], - toolDescription: 'Weather tool', hasSecret: true, secretSchema: { type: 'object', @@ -544,7 +538,6 @@ describe('SystemToolRepo.getSystemToolDetail', () => { version: '1.0.0', icon: 'weather.svg', tags: [], - toolDescription: 'Weather tool', hasSecret: true, secretSchema: { type: 'object', @@ -582,7 +575,6 @@ describe('SystemToolRepo.getSystemToolDetail', () => { version: '1.0.0', icon: 'weather.svg', tags: [], - toolDescription: 'Weather tool', hasSecret: true, secretSchema: { type: 'object', @@ -620,7 +612,6 @@ describe('SystemToolRepo.getSystemToolDetail', () => { version: '1.0.0', icon: 'weather.svg', tags: [], - toolDescription: 'Weather tool', hasSecret: true, secretSchema: { type: 'object', @@ -657,7 +648,6 @@ describe('SystemToolRepo.getSystemToolDetail', () => { version: '0.0.1', icon: 'perplexity.svg', tags: [], - toolDescription: 'Perplexity tool', inputSchema: null, outputSchema: null, secretSchema: null, @@ -666,7 +656,6 @@ describe('SystemToolRepo.getSystemToolDetail', () => { id: 'search', name: { en: 'Search' }, description: { en: 'Search intro' }, - toolDescription: 'Search tool', inputSchema: null, outputSchema: null } @@ -780,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', @@ -798,7 +786,6 @@ describe('SystemToolRepo.getSystemToolDisplayInfo', () => { name: 'Workflow Tool', avatar: 'workflow.svg', intro: 'Workflow intro', - toolDescription: 'Workflow description', version: 'workflow-version', currentCost: 3, systemKeyCost: 4, @@ -838,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 @@ -860,8 +845,7 @@ describe('SystemToolRepo.getSystemToolDisplayInfo', () => { systemKeyCost: 1, customConfig: { name: 'Forecast', - version: '1.0.0', - toolDescription: 'Configured forecast' + version: '1.0.0' } } ]); @@ -875,7 +859,6 @@ describe('SystemToolRepo.getSystemToolDisplayInfo', () => { name: 'Forecast', intro: 'Forecast intro', avatar: 'forecast.svg', - toolDescription: 'Configured forecast', currentCost: 2, systemKeyCost: 1 }); @@ -910,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 @@ -935,7 +916,6 @@ describe('SystemToolRepo.getSystemToolDisplayInfo', () => { id: 'forecast', name: 'Forecast', description: 'Forecast intro', - toolDescription: 'Forecast tool', icon: 'forecast.svg' }); expect(tool.children?.[0]).not.toHaveProperty('inputSchema'); @@ -968,7 +948,6 @@ describe('SystemToolRepo.getSystemToolDisplayInfo', () => { version: '1.0.0', icon: 'weather.svg', tags: ['life'], - toolDescription: 'Weather tool', hasSecret: false } ]); @@ -993,9 +972,7 @@ describe('SystemToolRepo.getSystemToolDisplayInfo', () => { status: PluginStatusEnum.Offline, currentCost: 3, systemKeyCost: 2, - customConfig: { - toolDescription: 'Configured forecast' - } + customConfig: {} }; mocks.findSystemTools.mockResolvedValueOnce([childConfig]).mockResolvedValueOnce([ @@ -1016,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' } ] @@ -1040,8 +1015,7 @@ describe('SystemToolRepo.getSystemToolDisplayInfo', () => { source: 'debug:tmbId:tmb-1', status: PluginStatusEnum.Normal, currentCost: 3, - systemKeyCost: 2, - toolDescription: 'Configured forecast' + systemKeyCost: 2 }); }); @@ -1057,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' } } ] } @@ -1109,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' } } ] } @@ -1130,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 9e3fb82f5718..55fbf95643ce 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, @@ -281,6 +280,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 +430,41 @@ 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', + modules: [ + { + toolConfig: { + mcpToolSet: { + url: 'https://mcp.example.com', + toolList: [ + { + name: 'search', + description: 'MCP search tool', + inputSchema: { type: 'object', properties: {} } + } + ] + } + } + } + ] + }) + }); + + 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/app/tool/utils/clientSystemTool.test.ts b/packages/service/test/core/app/tool/utils/clientSystemTool.test.ts index f7f1d3d213a3..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: 'Weather query', currentCost: 0, systemKeyCost: 1, hasTokenFee: false, @@ -110,7 +109,6 @@ describe('getClientSystemToolPreviewNode', () => { intro: 'Weather query', author: 'FastGPT', tags: [], - toolDescription: 'Weather query', currentCost: 0, systemKeyCost: 0, hasTokenFee: false, @@ -150,7 +148,6 @@ describe('getClientSystemToolPreviewNode', () => { intro: 'Workflow tool', author: 'FastGPT', tags: [], - toolDescription: 'Workflow tool', currentCost: 0, systemKeyCost: 0, hasTokenFee: false, @@ -203,7 +200,6 @@ describe('getClientSystemToolPreviewNode', () => { intro: 'Workflow tool', author: 'FastGPT', tags: [], - toolDescription: 'Workflow tool', currentCost: 0, systemKeyCost: 0, hasTokenFee: false, @@ -255,7 +251,6 @@ describe('getClientSystemToolPreviewNode', () => { intro: 'Weather query', author: 'FastGPT', tags: [], - toolDescription: 'Weather query', currentCost: 0, systemKeyCost: 0, hasTokenFee: false, @@ -284,7 +279,6 @@ describe('getClientSystemToolPreviewNode', () => { intro: 'Weather query', author: 'FastGPT', tags: [], - toolDescription: 'Weather query', currentCost: 0, systemKeyCost: 0, hasTokenFee: false, @@ -319,7 +313,6 @@ describe('getClientSystemToolPreviewNode', () => { intro: 'Weather query', author: 'FastGPT', tags: [], - toolDescription: 'Weather query', currentCost: 0, systemKeyCost: 0, hasTokenFee: false, @@ -359,7 +352,6 @@ describe('getClientSystemToolPreviewNode', () => { intro: 'Weather query', author: 'FastGPT', tags: [], - toolDescription: 'Weather query', currentCost: 0, systemKeyCost: 0, hasTokenFee: false, @@ -404,7 +396,6 @@ describe('getClientSystemToolPreviewNode', () => { intro: 'Search tools', author: 'FastGPT', tags: [], - toolDescription: 'Search tools', currentCost: 0, systemKeyCost: 0, hasTokenFee: false, @@ -445,7 +436,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/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 cbf805c01937..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 @@ -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: [] } ] @@ -523,7 +544,6 @@ describe('runToolCall compression node responses', () => { name: 'Search', flowNodeType: FlowNodeTypeEnum.tool, avatar: 'tool-avatar', - toolDescription: 'Search data', inputs: [] } ] @@ -786,7 +806,6 @@ describe('runToolCall compression node responses', () => { name: 'Dataset search', flowNodeType: FlowNodeTypeEnum.datasetSearchNode, avatar: 'dataset-avatar', - toolDescription: 'Search dataset', inputs: [] } ] @@ -863,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/packages/service/test/core/workflow/dispatch/utils.test.ts b/packages/service/test/core/workflow/dispatch/utils.test.ts index 131d8dd7c776..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) => { @@ -1347,6 +1406,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/packages/service/test/core/workflow/utils/index.test.ts b/packages/service/test/core/workflow/utils/index.test.ts index 2b4b5c671cdf..b86837f854f8 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 }, @@ -255,7 +252,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' } }); @@ -308,7 +304,7 @@ describe('getSystemToolRunTimeNodeFromSystemToolset', () => { expect(result[0].jsonSchema).toBe(inputSchema); }); - it('should use child name and descriptions when selected config is empty', async () => { + it('should use child name and preserve an explicitly empty description', async () => { const toolSetNode = makeToolSetNode({ toolList: [{ toolId: 'child-1', name: '', description: '' }] }); @@ -317,8 +313,7 @@ describe('getSystemToolRunTimeNodeFromSystemToolset', () => { { id: 'child-1', name: 'Original Name', - description: 'Original Intro', - toolDescription: 'Original Tool Description' + description: 'Original Intro' } ]); @@ -329,8 +324,28 @@ 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'); + expect(result[0].intro).toBe(''); + }); + + it('should preserve whitespace in the saved description', 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(' '); }); it('should pass systemInputConfig value to child tool inputs', async () => { diff --git a/packages/web/i18n/en/app.json b/packages/web/i18n/en/app.json index 5a6807460af2..e26fbe26dcfd 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_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 d77a853a6821..71d1d6f7dc56 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_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 dc4d5be86a25..e03ced5b63f3 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_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 684e8a1a6f76..a60dcd7235c7 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_tips": "工具集描述不影響工具呼叫效果。如需要,請直接編輯工具描述", "tool_soon_offset_tips": "該工具將在後續下線,為了您的業務穩定,請盡快替換", "tool_tip": "作為工具執行時,該字段是否作為工具響應結果", "tool_type_tools": "工具", diff --git a/pro b/pro index 22d610f7860c..6ef80c82b486 160000 --- a/pro +++ b/pro @@ -1 +1 @@ -Subproject commit 22d610f7860c9939092da3c63b3d38fdb0ff0b61 +Subproject commit 6ef80c82b486feab058b6a53209b87c13b1fa4eb 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/NodeToolSet.tsx b/projects/app/src/pageComponents/app/detail/WorkflowComponents/Flow/nodes/NodeToolSet.tsx index 558c36ee0319..6d079d3e56f3 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..c66eb8390f94 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 ? ( +