diff --git a/document/content/docs/self-host/upgrading/4-14/4149.mdx b/document/content/docs/self-host/upgrading/4-14/4149.mdx new file mode 100644 index 0000000000..0ca37e5245 --- /dev/null +++ b/document/content/docs/self-host/upgrading/4-14/4149.mdx @@ -0,0 +1,16 @@ +--- +title: 'V4.14.9(进行中)' +description: 'FastGPT V4.14.9 更新说明' +--- + + +## 🚀 新增内容 + + +## ⚙️ 优化 + + +## 🐛 修复 + +1. 工作流嵌套插件时,未成功保留插件运行详情。同时整理所有 tool 类型前缀。 +2. 更新 MCP toolset 后可能无法正常调用。 \ No newline at end of file diff --git a/document/content/docs/toc.mdx b/document/content/docs/toc.mdx index 2bdc27f740..25bab62742 100644 --- a/document/content/docs/toc.mdx +++ b/document/content/docs/toc.mdx @@ -120,6 +120,7 @@ description: FastGPT 文档目录 - [/docs/self-host/upgrading/4-14/4147](/docs/self-host/upgrading/4-14/4147) - [/docs/self-host/upgrading/4-14/4148](/docs/self-host/upgrading/4-14/4148) - [/docs/self-host/upgrading/4-14/41481](/docs/self-host/upgrading/4-14/41481) +- [/docs/self-host/upgrading/4-14/4149](/docs/self-host/upgrading/4-14/4149) - [/docs/self-host/upgrading/outdated/40](/docs/self-host/upgrading/outdated/40) - [/docs/self-host/upgrading/outdated/41](/docs/self-host/upgrading/outdated/41) - [/docs/self-host/upgrading/outdated/4100](/docs/self-host/upgrading/outdated/4100) diff --git a/document/data/doc-last-modified.json b/document/data/doc-last-modified.json index 819e690cd1..72a718e871 100644 --- a/document/data/doc-last-modified.json +++ b/document/data/doc-last-modified.json @@ -95,8 +95,8 @@ "document/content/docs/introduction/guide/dashboard/workflow/variable_update.mdx": "2025-07-23T21:35:03+08:00", "document/content/docs/introduction/guide/knowledge_base/RAG.en.mdx": "2026-02-26T22:14:30+08:00", "document/content/docs/introduction/guide/knowledge_base/RAG.mdx": "2025-07-23T21:35:03+08:00", - "document/content/docs/introduction/guide/knowledge_base/api_dataset.en.mdx": "2026-02-26T22:14:30+08:00", - "document/content/docs/introduction/guide/knowledge_base/api_dataset.mdx": "2025-07-23T21:35:03+08:00", + "document/content/docs/introduction/guide/knowledge_base/api_dataset.en.mdx": "2026-03-09T17:39:53+08:00", + "document/content/docs/introduction/guide/knowledge_base/api_dataset.mdx": "2026-03-09T17:39:53+08:00", "document/content/docs/introduction/guide/knowledge_base/collection_tags.en.mdx": "2026-02-26T22:14:30+08:00", "document/content/docs/introduction/guide/knowledge_base/collection_tags.mdx": "2025-08-02T19:38:37+08:00", "document/content/docs/introduction/guide/knowledge_base/dataset_engine.en.mdx": "2026-02-26T22:14:30+08:00", @@ -232,9 +232,10 @@ "document/content/docs/self-host/upgrading/4-14/4147.en.mdx": "2026-03-03T17:39:47+08:00", "document/content/docs/self-host/upgrading/4-14/4147.mdx": "2026-03-03T17:39:47+08:00", "document/content/docs/self-host/upgrading/4-14/4148.en.mdx": "2026-03-06T19:32:23+08:00", - "document/content/docs/self-host/upgrading/4-14/4148.mdx": "2026-03-09T12:04:22+08:00", + "document/content/docs/self-host/upgrading/4-14/4148.mdx": "2026-03-09T17:39:53+08:00", "document/content/docs/self-host/upgrading/4-14/41481.en.mdx": "2026-03-09T12:02:02+08:00", - "document/content/docs/self-host/upgrading/4-14/41481.mdx": "2026-03-09T14:24:27+08:00", + "document/content/docs/self-host/upgrading/4-14/41481.mdx": "2026-03-09T17:39:53+08:00", + "document/content/docs/self-host/upgrading/4-14/4149.mdx": "2026-03-11T22:47:07+08:00", "document/content/docs/self-host/upgrading/outdated/40.en.mdx": "2026-03-03T17:39:47+08:00", "document/content/docs/self-host/upgrading/outdated/40.mdx": "2026-03-03T17:39:47+08:00", "document/content/docs/self-host/upgrading/outdated/41.en.mdx": "2026-03-03T17:39:47+08:00", @@ -376,7 +377,7 @@ "document/content/docs/self-host/upgrading/upgrade-intruction.en.mdx": "2026-03-03T17:39:47+08:00", "document/content/docs/self-host/upgrading/upgrade-intruction.mdx": "2026-03-03T17:39:47+08:00", "document/content/docs/toc.en.mdx": "2026-03-09T12:02:02+08:00", - "document/content/docs/toc.mdx": "2026-03-09T12:02:02+08:00", + "document/content/docs/toc.mdx": "2026-03-11T22:47:07+08:00", "document/content/docs/use-cases/app-cases/dalle3.en.mdx": "2026-02-26T22:14:30+08:00", "document/content/docs/use-cases/app-cases/dalle3.mdx": "2025-07-23T21:35:03+08:00", "document/content/docs/use-cases/app-cases/english_essay_correction_bot.en.mdx": "2026-02-26T22:14:30+08:00", diff --git a/packages/global/common/error/code/plugin.ts b/packages/global/common/error/code/plugin.ts index 5a3b7a4582..f508bedaf8 100644 --- a/packages/global/common/error/code/plugin.ts +++ b/packages/global/common/error/code/plugin.ts @@ -9,7 +9,7 @@ export enum PluginErrEnum { const errList = [ { statusText: PluginErrEnum.unExist, - message: i18nT('common:code_error.plugin_error.not_exist') + message: i18nT('common:error.tool_not_exist') }, { statusText: PluginErrEnum.unAuth, diff --git a/packages/global/core/app/tool/httpTool/utils.ts b/packages/global/core/app/tool/httpTool/utils.ts index 651c52c2fd..468619e1bc 100644 --- a/packages/global/core/app/tool/httpTool/utils.ts +++ b/packages/global/core/app/tool/httpTool/utils.ts @@ -51,12 +51,14 @@ export const getHTTPToolRuntimeNode = ({ tool, nodeId, avatar = 'core/app/type/httpToolsFill', - toolSetId + toolSetId, + toolsetName }: { tool: Omit; nodeId: string; avatar?: string; toolSetId: string; + toolsetName: string; }): RuntimeNodeItemType => { return { nodeId, @@ -81,7 +83,7 @@ export const getHTTPToolRuntimeNode = ({ type: FlowNodeOutputTypeEnum.static } ], - name: tool.name, + name: `${toolsetName}/${tool.name}`, version: '' }; }; diff --git a/packages/global/core/app/tool/mcpTool/utils.ts b/packages/global/core/app/tool/mcpTool/utils.ts index e00dd8c909..4743db5661 100644 --- a/packages/global/core/app/tool/mcpTool/utils.ts +++ b/packages/global/core/app/tool/mcpTool/utils.ts @@ -13,15 +13,13 @@ export const getMCPToolSetRuntimeNode = ({ toolList, headerSecret, name, - avatar, - toolId + avatar }: { url: string; toolList: McpToolConfigType[]; headerSecret?: StoreSecretValueType; name?: string; avatar?: string; - toolId: string; }): RuntimeNodeItemType => { return { nodeId: getNanoid(16), @@ -32,8 +30,7 @@ export const getMCPToolSetRuntimeNode = ({ mcpToolSet: { toolList, headerSecret, - url, - toolId + url } }, inputs: [], @@ -47,12 +44,14 @@ export const getMCPToolRuntimeNode = ({ tool, avatar = 'core/app/type/mcpToolsFill', nodeId, + toolsetName, toolSetId }: { nodeId: string; tool: McpToolConfigType; - avatar?: string; toolSetId: string; + toolsetName: string; + avatar?: string; }): RuntimeNodeItemType => { return { nodeId, @@ -76,7 +75,7 @@ export const getMCPToolRuntimeNode = ({ type: FlowNodeOutputTypeEnum.static } ], - name: tool.name, + name: `${toolsetName}/${tool.name}`, version: '' }; }; diff --git a/packages/global/core/app/tool/utils.ts b/packages/global/core/app/tool/utils.ts index 531022888f..4001f42961 100644 --- a/packages/global/core/app/tool/utils.ts +++ b/packages/global/core/app/tool/utils.ts @@ -2,60 +2,81 @@ import { AppToolSourceEnum } from '../tool/constants'; /** Tool id rule: - - personal: ObjectId + - personal: ObjectId(旧版), personal-objectId(新版) - commercial: commercial-ObjectId - systemtool: systemTool-id - - mcp tool: mcp-parentId/toolName + - mcp toolset: appId + - mcp tool pluginId: mcp-appId/toolname + - http toolset: appId + - http tool pluginId: http-appId/toolname (deprecated) community: community-id */ -export function splitCombineToolId(id: string) { +export function splitCombineToolId(id: string): { + source: AppToolSourceEnum; + pluginId: string; + authAppId?: string; +} { const splitRes = id.split('-'); if (splitRes.length === 1) { // app id return { source: AppToolSourceEnum.personal, - pluginId: id + pluginId: id, + authAppId: id }; } const [source, ...rest] = id.split('-') as [AppToolSourceEnum, string | undefined]; - const pluginId = rest.join('-'); - if (!source || !pluginId) throw new Error('pluginId not found'); + const toolId = rest.join('-'); + if (!source || !toolId) throw new Error('toolId not found'); // 兼容4.10.0 之前的插件 if (source === 'community' || id === 'commercial-dalle3') { return { source: AppToolSourceEnum.systemTool, - pluginId: `${AppToolSourceEnum.systemTool}-${pluginId}` + pluginId: toolId + }; + } + + if (source === AppToolSourceEnum.systemTool) { + return { + source: AppToolSourceEnum.systemTool, + pluginId: toolId + }; + } + if (source === AppToolSourceEnum.commercial) { + return { + source: AppToolSourceEnum.commercial, + pluginId: toolId }; } // mcp-appId, mcp-appId/toolname - if (source === 'mcp') { - const [parentId, toolName] = pluginId.split('/'); + if (source === AppToolSourceEnum.mcp) { + const [parentId, toolName] = toolId.split('/'); return { source: AppToolSourceEnum.mcp, - pluginId, + pluginId: toolId, authAppId: parentId }; } - if (source === 'http') { - const [parentId, toolName] = pluginId.split('/'); + if (source === AppToolSourceEnum.http) { + const [parentId, toolName] = toolId.split('/'); return { source: AppToolSourceEnum.http, - pluginId, - parentId + pluginId: toolId, + authAppId: parentId }; } - if (source === 'personal') { + if (source === AppToolSourceEnum.personal) { return { source: AppToolSourceEnum.personal, - pluginId, - parentId: pluginId + pluginId: toolId, + authAppId: toolId }; } - return { source, pluginId: id }; + throw new Error('Invalid tool id'); } export const getToolRawId = (id: string) => { diff --git a/packages/global/core/workflow/type/node.ts b/packages/global/core/workflow/type/node.ts index 6aeb0b30d9..6039cf07fb 100644 --- a/packages/global/core/workflow/type/node.ts +++ b/packages/global/core/workflow/type/node.ts @@ -12,7 +12,6 @@ import z from 'zod'; export const NodeToolConfigTypeSchema = z.object({ mcpToolSet: z .object({ - toolId: z.string(), url: z.string(), headerSecret: StoreSecretValueTypeSchema.optional(), toolList: z.array(McpToolConfigSchema) @@ -20,7 +19,7 @@ export const NodeToolConfigTypeSchema = z.object({ .optional(), mcpTool: z .object({ - toolId: z.string() + toolId: z.string() // mcp-appId/oolname }) .optional(), systemTool: z @@ -51,7 +50,7 @@ export const NodeToolConfigTypeSchema = z.object({ .optional(), httpTool: z .object({ - toolId: z.string() + toolId: z.string() // http-appId/oolname }) .optional() }); diff --git a/packages/service/core/app/tool/controller.ts b/packages/service/core/app/tool/controller.ts index 0d0470f08f..49b7ad0cd2 100644 --- a/packages/service/core/app/tool/controller.ts +++ b/packages/service/core/app/tool/controller.ts @@ -239,10 +239,10 @@ export const getSystemToolsWithInstalled = async ({ }; export const getSystemToolByIdAndVersionId = async ( - pluginId: string, + toolId: string, versionId?: string ): Promise => { - const tool = await getSystemToolById(pluginId); + const tool = await getSystemToolById(toolId); // App type system tool if (tool.associatedPluginId) { @@ -333,10 +333,6 @@ export const getSystemToolByIdAndVersionId = async ( /* Format plugin to workflow preview node data - Persion workflow/plugin: objectId - Persion mcptoolset: objectId - Persion mcp tool: mcp-parentId/name - System tool/toolset: system-toolId */ export async function getChildAppPreviewNode({ appId, @@ -350,8 +346,7 @@ export async function getChildAppPreviewNode({ const { source, pluginId } = splitCombineToolId(appId); const app: ChildAppType = await (async () => { - // 1. App - // 2. MCP ToolSets + // App / Mcp toolset / Http toolset if (source === AppToolSourceEnum.personal) { const item = await MongoApp.findById(pluginId).lean(); if (!item) return Promise.reject(PluginErrEnum.unExist); @@ -367,17 +362,18 @@ export async function getChildAppPreviewNode({ }) : true; - if (item.type === AppTypeEnum.mcpToolSet) { + // Adapt + if (item.type === AppTypeEnum.mcpToolSet && !version.nodes[0].toolConfig) { const children = await getMCPChildren(item); version.nodes[0].toolConfig = { mcpToolSet: { - toolId: pluginId, toolList: children, url: '', headerSecret: {} } }; } + return { id: String(item._id), teamId: String(item.teamId), @@ -430,11 +426,12 @@ export async function getChildAppPreviewNode({ getMCPToolRuntimeNode({ nodeId: getNanoid(6), toolSetId: item._id, + toolsetName: item.name, avatar: item.avatar, tool: { description: tool.description, inputSchema: tool.inputSchema, - name: `${item.name}/${tool.name}` + name: tool.name } }) ], @@ -469,11 +466,12 @@ export async function getChildAppPreviewNode({ getHTTPToolRuntimeNode({ nodeId: getNanoid(6), toolSetId: item._id, + toolsetName: item.name, tool: { description: tool.description, inputSchema: tool.inputSchema, outputSchema: tool.outputSchema, - name: `${item.name}/${tool.name}` + name: tool.name }, avatar: item.avatar }) @@ -484,10 +482,9 @@ export async function getChildAppPreviewNode({ isLatestVersion: true }; } - // 1. System Tools - // 2. System Plugins configured in Pro (has associatedPluginId) + // System Tools/ Commercial system tools else { - return getSystemToolByIdAndVersionId(pluginId, versionId); + return getSystemToolByIdAndVersionId(appId, versionId); } })(); @@ -504,7 +501,7 @@ export async function getChildAppPreviewNode({ if (source === AppToolSourceEnum.systemTool) { // system Tool or Toolsets const children = app.isFolder - ? (await getSystemTools()).filter((item) => item.parentId === pluginId) + ? (await getSystemTools()).filter((item) => item.parentId === app.id) : []; return { @@ -619,74 +616,6 @@ export async function getChildAppPreviewNode({ }; } -/** - Get runtime plugin data - System plugin: plugin id - Personal plugin: Version id -*/ -export async function getChildAppRuntimeById({ - id, - versionId, - lang = 'en' -}: { - id: string; - versionId?: string; - lang?: localeType; -}): Promise { - const app = await (async () => { - const { source, pluginId } = splitCombineToolId(id); - - if (source === AppToolSourceEnum.personal) { - const item = await MongoApp.findById(pluginId).lean(); - if (!item) return Promise.reject(PluginErrEnum.unExist); - - const version = await getAppVersionById({ - appId: pluginId, - versionId, - app: item - }); - - return { - id: String(item._id), - teamId: String(item.teamId), - tmbId: String(item.tmbId), - name: item.name, - avatar: item.avatar, - intro: item.intro, - showStatus: true, - workflow: { - nodes: version.nodes, - edges: version.edges, - chatConfig: version.chatConfig - }, - templateType: FlowNodeTemplateTypeEnum.teamApp, - - originCost: 0, - currentCost: 0, - systemKeyCost: 0, - hasTokenFee: false, - pluginOrder: 0 - }; - } else { - return getSystemToolByIdAndVersionId(pluginId, versionId); - } - })(); - - return { - id: app.id, - teamId: app.teamId, - tmbId: app.tmbId, - name: parseI18nString(app.name, lang), - avatar: app.avatar || '', - showStatus: true, - currentCost: app.currentCost, - systemKeyCost: app.systemKeyCost, - nodes: app.workflow.nodes, - edges: app.workflow.edges, - hasTokenFee: app.hasTokenFee - }; -} - /* FastsGPT-tool api: */ export const refreshSystemTools = async (): Promise => { const workflowToolFormat = (item: SystemPluginToolCollectionType): AppToolTemplateItemType => { @@ -784,10 +713,10 @@ export const refreshSystemTools = async (): Promise = return concatTools; }; -export const getSystemToolById = async (id: string): Promise => { - const { pluginId } = splitCombineToolId(id); +// toolId: systemTool-id, commercial-id +export const getSystemToolById = async (toolId: string): Promise => { const tools = await getSystemTools(); - const tool = tools.find((item) => item.id === pluginId); + const tool = tools.find((item) => item.id === toolId); if (tool) { return cloneDeep(tool); } diff --git a/packages/service/core/app/tool/runtime/utils.ts b/packages/service/core/app/tool/runtime/utils.ts index 7ac7c7b39c..992fbc3bee 100644 --- a/packages/service/core/app/tool/runtime/utils.ts +++ b/packages/service/core/app/tool/runtime/utils.ts @@ -23,7 +23,12 @@ export const computedAppToolUsage = async ({ const { source } = splitCombineToolId(plugin.id); const childrenUsages = childrenUsage.reduce((sum, item) => sum + (item.totalPoints || 0), 0); - if (source !== AppToolSourceEnum.personal) { + const set = new Set([ + AppToolSourceEnum.commercial, + AppToolSourceEnum.community, + AppToolSourceEnum.systemTool + ]); + if (set.has(source)) { if (error) return 0; const pluginCurrentCost = plugin.currentCost ?? 0; diff --git a/packages/service/core/app/utils.ts b/packages/service/core/app/utils.ts index 8e91e43b8b..22d2a484d8 100644 --- a/packages/service/core/app/utils.ts +++ b/packages/service/core/app/utils.ts @@ -82,7 +82,7 @@ export async function rewriteAppWorkflowToDetail({ /* Add node(App Type) versionlabel and latest sign ==== */ await Promise.all( nodes.map(async (node) => { - // Tool node(简易模式/工作流) + // Tool node if (node.pluginId) { const result = await loadToolNode({ id: node.pluginId, versionId: node.version }); if (result.success) { diff --git a/packages/service/core/plugin/tool/systemToolSchema.ts b/packages/service/core/plugin/tool/systemToolSchema.ts index f7e8a3a2a1..56fa807a5b 100644 --- a/packages/service/core/plugin/tool/systemToolSchema.ts +++ b/packages/service/core/plugin/tool/systemToolSchema.ts @@ -7,6 +7,7 @@ export const collectionName = 'system_plugin_tools'; const SystemToolSchema = new Schema({ pluginId: { + // commercial-id type: String, required: true }, diff --git a/packages/service/core/workflow/dispatch/ai/agent/sub/tool/index.ts b/packages/service/core/workflow/dispatch/ai/agent/sub/tool/index.ts index 7884c156a9..eadc8528f8 100644 --- a/packages/service/core/workflow/dispatch/ai/agent/sub/tool/index.ts +++ b/packages/service/core/workflow/dispatch/ai/agent/sub/tool/index.ts @@ -20,6 +20,7 @@ import { getAppVersionById } from '../../../../../../app/version/controller'; import { MCPClient } from '../../../../../../app/mcp'; import { runHTTPTool } from '../../../../../../app/http'; import { getS3ChatSource } from '../../../../../../../common/s3/sources/chat'; +import { parseToolId } from '../../../../child/runTool'; type SystemInputConfigType = { type: SystemToolSecretInputTypeEnum; @@ -175,8 +176,7 @@ export const dispatchTool = async ({ ] }; } else if (toolConfig?.mcpTool?.toolId) { - const { pluginId } = splitCombineToolId(toolConfig.mcpTool.toolId); - const [parentId, toolSetName, toolName] = pluginId.split('/'); + const { parentId, toolName } = parseToolId(toolConfig.mcpTool.toolId); const tool = await getAppVersionById({ appId: parentId, versionId: version @@ -203,8 +203,7 @@ export const dispatchTool = async ({ usages: [] }; } else if (toolConfig?.httpTool?.toolId) { - const { pluginId } = splitCombineToolId(toolConfig.httpTool.toolId); - const [parentId, toolSetName, toolName] = pluginId.split('/'); + const { parentId, toolName } = parseToolId(toolConfig.httpTool.toolId); if (!parentId || !toolName) { return Promise.reject(`Invalid HTTP tool id: ${toolConfig.httpTool.toolId}`); } 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 5444cf592b..636e0acfa5 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 @@ -25,7 +25,7 @@ import type { SubAppInitType } from '../type'; import { getToolConfigStatus } from '@fastgpt/global/core/app/formEdit/utils'; import { getLogger, LogCategories } from '../../../../../../../common/logger'; -export const agentSkillToToolRuntime = async ({ +export const getAgentRuntimeTools = async ({ tools, tmbId, lang @@ -195,18 +195,15 @@ export const agentSkillToToolRuntime = async ({ if (!app) return []; const toolList = await getMCPChildren(app); - const toolSetId = mcpToolsetVal.toolId ?? toolNode.pluginId; + const toolSetId = mcpToolsetVal.toolId || toolNode.pluginId; const children = toolList.map((tool, index) => { const newToolNode = getMCPToolRuntimeNode({ toolSetId, + toolsetName: toolNode.name, nodeId: `${toolSetId}${index}`, avatar: toolNode.avatar, - tool: { - ...tool, - name: `${toolNode.name}/${tool.name}` - } + tool }); - return newToolNode; }); @@ -232,15 +229,12 @@ export const agentSkillToToolRuntime = async ({ } else if (httpToolsetVal) { const children = httpToolsetVal.toolList.map((tool: HttpToolConfigType, index) => { const newToolNode = getHTTPToolRuntimeNode({ - tool: { - ...tool, - name: `${toolNode.name}/${tool.name}` - }, + tool, nodeId: `${pluginId}${index}`, avatar: toolNode.avatar, - toolSetId: pluginId + toolSetId: pluginId, + toolsetName: toolNode.name }); - return newToolNode; }); diff --git a/packages/service/core/workflow/dispatch/ai/agent/utils.ts b/packages/service/core/workflow/dispatch/ai/agent/utils.ts index ba931eb1a3..b2157c23b5 100644 --- a/packages/service/core/workflow/dispatch/ai/agent/utils.ts +++ b/packages/service/core/workflow/dispatch/ai/agent/utils.ts @@ -2,7 +2,7 @@ import type { localeType } from '@fastgpt/global/common/i18n/type'; import type { SkillToolType } from '@fastgpt/global/core/ai/skill/type'; import type { ChatCompletionTool } from '@fastgpt/global/core/ai/type'; import type { SubAppRuntimeType } from './type'; -import { agentSkillToToolRuntime } from './sub/tool/utils'; +import { getAgentRuntimeTools } from './sub/tool/utils'; import { readFileTool } from './sub/file/utils'; import { PlanAgentTool } from './sub/plan/constants'; import { datasetSearchTool } from './sub/dataset/utils'; @@ -43,7 +43,7 @@ export const getSubapps = async ({ } /* System tool */ - const formatTools = await agentSkillToToolRuntime({ + const formatTools = await getAgentRuntimeTools({ tools, tmbId, lang diff --git a/packages/service/core/workflow/dispatch/child/runTool.ts b/packages/service/core/workflow/dispatch/child/runTool.ts index 2c758646ef..fbe38ec735 100644 --- a/packages/service/core/workflow/dispatch/child/runTool.ts +++ b/packages/service/core/workflow/dispatch/child/runTool.ts @@ -19,7 +19,6 @@ import { getSystemToolById } from '../../../app/tool/controller'; import { textAdaptGptResponse } from '@fastgpt/global/core/workflow/runtime/utils'; import { pushTrack } from '../../../../common/middle/tracks/utils'; import { getNodeErrResponse } from '../utils'; -import { splitCombineToolId } from '@fastgpt/global/core/app/tool/utils'; import { getAppVersionById } from '../../../../core/app/version/controller'; import { runHTTPTool } from '../../../app/http'; import { getS3ChatSource } from '../../../../common/s3/sources/chat'; @@ -207,8 +206,7 @@ export const dispatchRunTool = async (props: RunToolProps): Promise { + const formatId = id.split('-').slice(1).join('-'); + const [parentId, toolsetNameOrToolName, legacyToolName] = formatId.split('/'); + + if (legacyToolName) { + // 旧版格式: source-appId/toolsetName/toolName + return { parentId, toolName: legacyToolName }; + } + + // 新版格式: source-appId/toolName + return { parentId, toolName: toolsetNameOrToolName }; +}; diff --git a/packages/service/core/workflow/dispatch/plugin/run.ts b/packages/service/core/workflow/dispatch/plugin/run.ts index 0c776b0ad3..2534297f04 100644 --- a/packages/service/core/workflow/dispatch/plugin/run.ts +++ b/packages/service/core/workflow/dispatch/plugin/run.ts @@ -14,18 +14,22 @@ import { storeNodes2RuntimeNodes } from '@fastgpt/global/core/workflow/runtime/utils'; import { type DispatchNodeResultType } from '@fastgpt/global/core/workflow/runtime/type'; -import { authPluginByTmbId } from '../../../../support/permission/app/auth'; +import { authWorkflowToolByTmbId } from '../../../../support/permission/app/auth'; import { ReadPermissionVal } from '@fastgpt/global/support/permission/constant'; import { computedAppToolUsage } from '../../../app/tool/runtime/utils'; import { filterSystemVariables, getNodeErrResponse } from '../utils'; import { serverGetWorkflowToolRunUserQuery } from '../../../app/tool/workflowTool/utils'; -import type { NodeInputKeyEnum, NodeOutputKeyEnum } from '@fastgpt/global/core/workflow/constants'; -import { getChildAppRuntimeById } from '../../../app/tool/controller'; +import { + type NodeInputKeyEnum, + type NodeOutputKeyEnum +} from '@fastgpt/global/core/workflow/constants'; import { runWorkflow } from '../index'; import { getUserChatInfo } from '../../../../support/user/team/utils'; import { dispatchRunTool } from '../child/runTool'; import type { AppToolRuntimeType } from '@fastgpt/global/core/app/tool/type'; import { anyValueDecrypt } from '../../../../common/secret/utils'; +import { getAppVersionById } from '../../../app/version/controller'; +import { parseI18nString } from '@fastgpt/global/common/i18n/utils'; type RunPluginProps = ModuleDispatchProps<{ [NodeInputKeyEnum.forbidStream]?: boolean; @@ -51,7 +55,7 @@ export const dispatchRunPlugin = async (props: RunPluginProps): Promise node.flowNodeType === FlowNodeTypeEnum.pluginOutput) ?.inputs.reduce>((acc, cur) => { acc[cur.key] = cur.isToolOutput === false ? false : true; return acc; }, {}) ?? {}; const runtimeNodes = storeNodes2RuntimeNodes( - plugin.nodes, - getWorkflowEntryNodeIds(plugin.nodes) + workflowTool.nodes, + getWorkflowEntryNodeIds(workflowTool.nodes) ).map((node) => { - // Update plugin input value + // Update workflowTool input value if (node.flowNodeType === FlowNodeTypeEnum.pluginInput) { return { ...node, @@ -129,7 +155,7 @@ export const dispatchRunPlugin = async (props: RunPluginProps): Promise item.moduleType === FlowNodeTypeEnum.pluginOutput); const usagePoints = await computedAppToolUsage({ - plugin, + plugin: workflowTool, childrenUsage: flowUsages, error: !!output?.pluginOutput?.error }); @@ -182,20 +208,17 @@ export const dispatchRunPlugin = async (props: RunPluginProps): Promise { - const filterArr = [FlowNodeTypeEnum.pluginOutput]; - return !filterArr.includes(item.moduleType as any); - }) + pluginDetail: toolData?.permission?.hasWritePer // Not system workflowTool + ? flowResponses : undefined }, [DispatchNodeResponseKeyEnum.nodeDispatchUsages]: [ { - moduleName: plugin.name, + moduleName: workflowTool.name, totalPoints: usagePoints } ], @@ -212,7 +235,7 @@ export const dispatchRunPlugin = async (props: RunPluginProps): Promise { const newToolNode = getMCPToolRuntimeNode({ nodeId: `${toolSetNode.nodeId}${index}`, toolSetId, + toolsetName: toolSetNode.name, avatar: toolSetNode.avatar, - tool: { - ...tool, - name: `${toolSetNode.name}/${tool.name}` - } + tool }); - nodes.push(newToolNode); pushEdges(newToolNode.nodeId); }); } else if (httpToolsetVal) { httpToolsetVal.toolList.forEach((tool: HttpToolConfigType, index: number) => { const newToolNode = getHTTPToolRuntimeNode({ - tool: { - ...tool, - name: `${toolSetNode.name}/${tool.name}` - }, + tool, nodeId: `${toolSetNode.nodeId}${index}`, avatar: toolSetNode.avatar, - toolSetId: toolSetNode.pluginId! + toolSetId: toolSetNode.pluginId!, + toolsetName: toolSetNode.name }); - nodes.push(newToolNode); pushEdges(newToolNode.nodeId); }); diff --git a/packages/service/support/permission/app/auth.ts b/packages/service/support/permission/app/auth.ts index 8068e2e6d7..aa17bef9ae 100644 --- a/packages/service/support/permission/app/auth.ts +++ b/packages/service/support/permission/app/auth.ts @@ -15,12 +15,11 @@ import { type PermissionValueType } from '@fastgpt/global/support/permission/typ import { AppFolderTypeList, AppTypeEnum } from '@fastgpt/global/core/app/constants'; import { type ParentIdType } from '@fastgpt/global/common/parentFolder/type'; import { type AuthModeType, type AuthResponseType } from '../type'; -import { splitCombineToolId } from '@fastgpt/global/core/app/tool/utils'; import { AppReadChatLogPerVal } from '@fastgpt/global/support/permission/app/constant'; import { parseHeaderCert } from '../auth/common'; import { sumPer } from '@fastgpt/global/support/permission/utils'; -export const authPluginByTmbId = async ({ +export const authWorkflowToolByTmbId = async ({ tmbId, appId, per @@ -29,16 +28,12 @@ export const authPluginByTmbId = async ({ appId: string; per: PermissionValueType; }) => { - const { authAppId } = splitCombineToolId(appId); - if (authAppId) { - const { app } = await authAppByTmbId({ - appId: authAppId, - tmbId, - per - }); - - return app; - } + const { app } = await authAppByTmbId({ + appId, + tmbId, + per + }); + return app; }; export const authAppByTmbId = async ({ diff --git a/packages/web/i18n/en/common.json b/packages/web/i18n/en/common.json index f744528dd7..faf4fae3e2 100644 --- a/packages/web/i18n/en/common.json +++ b/packages/web/i18n/en/common.json @@ -177,7 +177,7 @@ "code_error.outlink_error.invalid_link": "Invalid Share Link", "code_error.outlink_error.link_not_exist": "Share Link Does Not Exist", "code_error.outlink_error.un_auth_user": "Identity Verification Failed", - "code_error.plugin_error.not_exist": "Tool deleted", + "error.tool_not_exist": "Tool deleted", "code_error.plugin_error.un_auth": "No permission to operate the tool", "code_error.system_error.community_version_num_limit": "Exceeded Open Source Version Limit, Please Upgrade to Commercial Version: https://fastgpt.io", "code_error.system_error.license_app_amount_limit": "Exceed the maximum number of applications in the system", diff --git a/packages/web/i18n/zh-CN/common.json b/packages/web/i18n/zh-CN/common.json index fd0725cc06..0b25ba8fd7 100644 --- a/packages/web/i18n/zh-CN/common.json +++ b/packages/web/i18n/zh-CN/common.json @@ -177,7 +177,7 @@ "code_error.outlink_error.invalid_link": "分享链接无效", "code_error.outlink_error.link_not_exist": "分享链接不存在", "code_error.outlink_error.un_auth_user": "身份校验失败", - "code_error.plugin_error.not_exist": "工具已删除", + "error.tool_not_exist": "工具已删除", "code_error.plugin_error.un_auth": "无权操作该工具", "code_error.system_error.community_version_num_limit": "超出社区版数量限制,请升级商业版: https://fastgpt.in", "code_error.system_error.license_app_amount_limit": "超出系统最大应用数量", diff --git a/packages/web/i18n/zh-Hant/common.json b/packages/web/i18n/zh-Hant/common.json index 45ef4904ce..8bc7ce0145 100644 --- a/packages/web/i18n/zh-Hant/common.json +++ b/packages/web/i18n/zh-Hant/common.json @@ -176,7 +176,7 @@ "code_error.outlink_error.invalid_link": "分享連結無效", "code_error.outlink_error.link_not_exist": "分享連結不存在", "code_error.outlink_error.un_auth_user": "身份驗證失敗", - "code_error.plugin_error.not_exist": "工具已刪除", + "error.tool_not_exist": "工具已刪除", "code_error.plugin_error.un_auth": "無權操作該工具", "code_error.system_error.community_version_num_limit": "超出開源版數量限制,請升級商業版:https://fastgpt.io", "code_error.system_error.license_app_amount_limit": "超出系統最大應用數量", diff --git a/projects/app/package.json b/projects/app/package.json index 463cf84a65..428ebc6ba7 100644 --- a/projects/app/package.json +++ b/projects/app/package.json @@ -1,6 +1,6 @@ { "name": "app", - "version": "4.14.8", + "version": "4.14.8.1", "private": false, "scripts": { "dev": "npm run build:workers && next dev", diff --git a/projects/app/src/pages/api/core/app/mcpTools/create.ts b/projects/app/src/pages/api/core/app/mcpTools/create.ts index b760d0250d..e0b07e38d2 100644 --- a/projects/app/src/pages/api/core/app/mcpTools/create.ts +++ b/projects/app/src/pages/api/core/app/mcpTools/create.ts @@ -55,8 +55,7 @@ async function handler( toolList, name, avatar, - headerSecret: formatedHeaderAuth, - toolId: '' + headerSecret: formatedHeaderAuth }) ], session diff --git a/projects/app/src/pages/api/core/app/mcpTools/update.ts b/projects/app/src/pages/api/core/app/mcpTools/update.ts index 48a546ec9b..87617d73ce 100644 --- a/projects/app/src/pages/api/core/app/mcpTools/update.ts +++ b/projects/app/src/pages/api/core/app/mcpTools/update.ts @@ -28,8 +28,7 @@ async function handler(req: ApiRequestProps, res: ApiRes toolList, headerSecret: formatedHeaderAuth, name: app.name, - avatar: app.avatar, - toolId: '' + avatar: app.avatar }); await mongoSessionRun(async (session) => { diff --git a/projects/app/src/pages/api/core/app/tool/getVersionList.ts b/projects/app/src/pages/api/core/app/tool/getVersionList.ts index ea4f2804f5..4936b55f5b 100644 --- a/projects/app/src/pages/api/core/app/tool/getVersionList.ts +++ b/projects/app/src/pages/api/core/app/tool/getVersionList.ts @@ -39,7 +39,7 @@ async function handler( // System tool plugin if (source === AppToolSourceEnum.systemTool) { - const item = await getSystemToolByIdAndVersionId(formatPluginId); + const item = await getSystemToolByIdAndVersionId(pluginId); return { total: 0, diff --git a/test/cases/global/core/app/tool/httpTool/utils.test.ts b/test/cases/global/core/app/tool/httpTool/utils.test.ts index e6051dc035..8c5cee1ee4 100644 --- a/test/cases/global/core/app/tool/httpTool/utils.test.ts +++ b/test/cases/global/core/app/tool/httpTool/utils.test.ts @@ -103,14 +103,15 @@ describe('httpTool utils', () => { const result = getHTTPToolRuntimeNode({ tool, nodeId: 'node-123', - toolSetId: 'toolset-456' + toolSetId: 'toolset-456', + toolsetName: 'toolsetName' }); expect(result.nodeId).toBe('node-123'); expect(result.flowNodeType).toBe(FlowNodeTypeEnum.tool); expect(result.avatar).toBe('core/app/type/httpToolsFill'); expect(result.intro).toBe('Search for items'); - expect(result.name).toBe('searchTool'); + expect(result.name).toBe('toolsetName/searchTool'); expect(result.toolConfig?.httpTool?.toolId).toBe( `${AppToolSourceEnum.http}-toolset-456/searchTool` ); @@ -128,7 +129,8 @@ describe('httpTool utils', () => { tool, nodeId: 'node-789', avatar: 'custom-icon', - toolSetId: 'toolset-abc' + toolSetId: 'toolset-abc', + toolsetName: 'toolsetName' }); expect(result.avatar).toBe('custom-icon'); @@ -145,7 +147,8 @@ describe('httpTool utils', () => { const result = getHTTPToolRuntimeNode({ tool, nodeId: 'node-001', - toolSetId: 'toolset-002' + toolSetId: 'toolset-002', + toolsetName: 'toolsetName' }); const rawResponseOutput = result.outputs.find((o) => o.key === NodeOutputKeyEnum.rawResponse); diff --git a/test/cases/global/core/app/tool/mcpTool/utils.test.ts b/test/cases/global/core/app/tool/mcpTool/utils.test.ts index e06783516f..2c75047879 100644 --- a/test/cases/global/core/app/tool/mcpTool/utils.test.ts +++ b/test/cases/global/core/app/tool/mcpTool/utils.test.ts @@ -28,8 +28,7 @@ describe('mcpTool utils', () => { const result = getMCPToolSetRuntimeNode({ url: 'https://mcp.example.com/api', - toolList, - toolId: 'mcp-tool-123' + toolList }); expect(result.flowNodeType).toBe(FlowNodeTypeEnum.toolSet); @@ -40,7 +39,6 @@ describe('mcpTool utils', () => { expect(result.nodeId).toHaveLength(16); expect(result.toolConfig?.mcpToolSet?.url).toBe('https://mcp.example.com/api'); expect(result.toolConfig?.mcpToolSet?.toolList).toEqual(toolList); - expect(result.toolConfig?.mcpToolSet?.toolId).toBe('mcp-tool-123'); }); it('should create runtime node with all optional params', () => { @@ -49,7 +47,6 @@ describe('mcpTool utils', () => { const result = getMCPToolSetRuntimeNode({ url: 'https://mcp.example.com/api', toolList, - toolId: 'mcp-tool-456', name: 'My MCP Tools', avatar: 'custom-mcp-avatar', headerSecret: { id: 'secret-1', key: 'Authorization' } @@ -81,14 +78,15 @@ describe('mcpTool utils', () => { const result = getMCPToolRuntimeNode({ tool, nodeId: 'node-123', - toolSetId: 'toolset-456' + toolSetId: 'toolset-456', + toolsetName: 'toolsetName' }); expect(result.nodeId).toBe('node-123'); expect(result.flowNodeType).toBe(FlowNodeTypeEnum.tool); expect(result.avatar).toBe('core/app/type/mcpToolsFill'); expect(result.intro).toBe('Search for information'); - expect(result.name).toBe('searchTool'); + expect(result.name).toBe('toolsetName/searchTool'); expect(result.toolConfig?.mcpTool?.toolId).toBe( `${AppToolSourceEnum.mcp}-toolset-456/searchTool` ); @@ -105,7 +103,8 @@ describe('mcpTool utils', () => { tool, nodeId: 'node-789', avatar: 'custom-icon', - toolSetId: 'toolset-abc' + toolSetId: 'toolset-abc', + toolsetName: 'toolsetName' }); expect(result.avatar).toBe('custom-icon'); @@ -121,7 +120,8 @@ describe('mcpTool utils', () => { const result = getMCPToolRuntimeNode({ tool, nodeId: 'node-001', - toolSetId: 'toolset-002' + toolSetId: 'toolset-002', + toolsetName: 'toolsetName' }); expect(result.outputs).toHaveLength(1); @@ -142,7 +142,8 @@ describe('mcpTool utils', () => { const result = getMCPToolRuntimeNode({ tool, nodeId: 'node-test', - toolSetId: 'parent-123' + toolSetId: 'parent-123', + toolsetName: 'toolsetName' }); expect(result.toolConfig?.mcpTool?.toolId).toBe('mcp-parent-123/myTool'); diff --git a/test/cases/global/core/app/tool/utils.test.ts b/test/cases/global/core/app/tool/utils.test.ts index db64dab8a5..2d22ee237c 100644 --- a/test/cases/global/core/app/tool/utils.test.ts +++ b/test/cases/global/core/app/tool/utils.test.ts @@ -25,7 +25,6 @@ describe('splitCombineToolId', () => { expect(result.source).toBe(AppToolSourceEnum.personal); expect(result.pluginId).toBe('507f1f77bcf86cd799439011'); - expect(result.parentId).toBe('507f1f77bcf86cd799439011'); }); }); @@ -34,14 +33,14 @@ describe('splitCombineToolId', () => { const result = splitCombineToolId('commercial-507f1f77bcf86cd799439011'); expect(result.source).toBe(AppToolSourceEnum.commercial); - expect(result.pluginId).toBe('commercial-507f1f77bcf86cd799439011'); + expect(result.pluginId).toBe('507f1f77bcf86cd799439011'); }); - it('should convert commercial-dalle3 to systemTool', () => { + it('should convert commercial-dalle3 to systemTool(Adapt)', () => { const result = splitCombineToolId('commercial-dalle3'); expect(result.source).toBe(AppToolSourceEnum.systemTool); - expect(result.pluginId).toBe('systemTool-dalle3'); + expect(result.pluginId).toBe('dalle3'); }); }); @@ -50,14 +49,14 @@ describe('splitCombineToolId', () => { const result = splitCombineToolId('systemTool-websearch'); expect(result.source).toBe(AppToolSourceEnum.systemTool); - expect(result.pluginId).toBe('systemTool-websearch'); + expect(result.pluginId).toBe('websearch'); }); it('should handle systemTool with complex id', () => { const result = splitCombineToolId('systemTool-code-interpreter'); expect(result.source).toBe(AppToolSourceEnum.systemTool); - expect(result.pluginId).toBe('systemTool-code-interpreter'); + expect(result.pluginId).toBe('code-interpreter'); }); }); @@ -85,7 +84,7 @@ describe('splitCombineToolId', () => { expect(result.source).toBe(AppToolSourceEnum.http); expect(result.pluginId).toBe('507f1f77bcf86cd799439011'); - expect(result.parentId).toBe('507f1f77bcf86cd799439011'); + expect(result.authAppId).toBe('507f1f77bcf86cd799439011'); }); it('should parse http-parentId/toolName format correctly', () => { @@ -93,7 +92,6 @@ describe('splitCombineToolId', () => { expect(result.source).toBe(AppToolSourceEnum.http); expect(result.pluginId).toBe('507f1f77bcf86cd799439011/apiTool'); - expect(result.parentId).toBe('507f1f77bcf86cd799439011'); }); }); @@ -102,13 +100,13 @@ describe('splitCombineToolId', () => { const result = splitCombineToolId('community-oldPlugin'); expect(result.source).toBe(AppToolSourceEnum.systemTool); - expect(result.pluginId).toBe('systemTool-oldPlugin'); + expect(result.pluginId).toBe('oldPlugin'); }); }); describe('error handling', () => { it('should throw error when pluginId is empty after split', () => { - expect(() => splitCombineToolId('commercial-')).toThrow('pluginId not found'); + expect(() => splitCombineToolId('commercial-')).toThrow('toolId not found'); }); }); }); @@ -121,7 +119,7 @@ describe('getToolRawId', () => { it('should return pluginId for commercial tool', () => { const result = getToolRawId('commercial-507f1f77bcf86cd799439011'); - expect(result).toBe('commercial-507f1f77bcf86cd799439011'); + expect(result).toBe('507f1f77bcf86cd799439011'); }); it('should return parentId for mcp tool with toolName', () => { @@ -136,7 +134,7 @@ describe('getToolRawId', () => { it('should handle systemTool correctly', () => { const result = getToolRawId('systemTool-websearch'); - expect(result).toBe('systemTool-websearch'); + expect(result).toBe('websearch'); }); it('should handle personal tool with prefix', () => { @@ -146,6 +144,6 @@ describe('getToolRawId', () => { it('should handle converted community tool', () => { const result = getToolRawId('community-oldPlugin'); - expect(result).toBe('systemTool-oldPlugin'); + expect(result).toBe('oldPlugin'); }); }); diff --git a/test/cases/service/core/workflow/dispatch/tools/runTool.test.ts b/test/cases/service/core/workflow/dispatch/tools/runTool.test.ts new file mode 100644 index 0000000000..a43fab75bf --- /dev/null +++ b/test/cases/service/core/workflow/dispatch/tools/runTool.test.ts @@ -0,0 +1,204 @@ +import { describe, it, expect } from 'vitest'; +import { parseToolId } from '@fastgpt/service/core/workflow/dispatch/child/runTool'; + +describe('parseToolId', () => { + describe('新版格式: source-appId/toolName', () => { + it('should parse mcp tool format correctly', () => { + const result = parseToolId('mcp-507f1f77bcf86cd799439011/apiTool'); + expect(result.parentId).toBe('507f1f77bcf86cd799439011'); + expect(result.toolName).toBe('apiTool'); + }); + + it('should parse http tool format correctly', () => { + const result = parseToolId('http-507f1f77bcf86cd799439011/weatherAPI'); + expect(result.parentId).toBe('507f1f77bcf86cd799439011'); + expect(result.toolName).toBe('weatherAPI'); + }); + + it('should handle tool names with special characters', () => { + const result = parseToolId('mcp-507f1f77bcf86cd799439011/api-tool_v2'); + expect(result.parentId).toBe('507f1f77bcf86cd799439011'); + expect(result.toolName).toBe('api-tool_v2'); + }); + + it('should handle tool names with numbers', () => { + const result = parseToolId('http-507f1f77bcf86cd799439011/tool123'); + expect(result.parentId).toBe('507f1f77bcf86cd799439011'); + expect(result.toolName).toBe('tool123'); + }); + }); + + describe('旧版格式: source-appId/toolsetName/toolName', () => { + it('should parse old mcp format correctly', () => { + const result = parseToolId('mcp-507f1f77bcf86cd799439011/toolset/apiTool'); + expect(result.parentId).toBe('507f1f77bcf86cd799439011'); + expect(result.toolName).toBe('apiTool'); + }); + + it('should parse old http format correctly', () => { + const result = parseToolId('http-507f1f77bcf86cd799439011/MyToolset/weatherAPI'); + expect(result.parentId).toBe('507f1f77bcf86cd799439011'); + expect(result.toolName).toBe('weatherAPI'); + }); + + it('should ignore toolset name in old format', () => { + const result = parseToolId('mcp-507f1f77bcf86cd799439011/ignoredToolset/actualTool'); + expect(result.parentId).toBe('507f1f77bcf86cd799439011'); + expect(result.toolName).toBe('actualTool'); + // toolsetName 应该被忽略 + }); + + it('should handle toolset names with special characters', () => { + const result = parseToolId('http-507f1f77bcf86cd799439011/tool-set_v1/myTool'); + expect(result.parentId).toBe('507f1f77bcf86cd799439011'); + expect(result.toolName).toBe('myTool'); + }); + }); + + describe('边界情况', () => { + it('should handle appId with hyphens', () => { + const result = parseToolId('mcp-507f-1f77-bcf8-6cd7-99439011/tool'); + expect(result.parentId).toBe('507f-1f77-bcf8-6cd7-99439011'); + expect(result.toolName).toBe('tool'); + }); + + it('should handle multiple hyphens in source prefix', () => { + const result = parseToolId('custom-source-507f1f77bcf86cd799439011/tool'); + expect(result.parentId).toBe('source-507f1f77bcf86cd799439011'); + expect(result.toolName).toBe('tool'); + }); + + it('should handle tool names with slashes in old format', () => { + // 注意: split('/') 只会分割成三个部分,所以第三个部分是 'tool' + const result = parseToolId('mcp-507f1f77bcf86cd799439011/toolset/tool/extra'); + expect(result.parentId).toBe('507f1f77bcf86cd799439011'); + // 实际上 split('/') 会得到 ['507f1f77bcf86cd799439011', 'toolset', 'tool/extra'] + // 但由于解构赋值,legacyToolName 会是 'tool/extra' + // 等等,让我重新理解代码逻辑... + // formatId.split('/') 会得到 ['507f1f77bcf86cd799439011', 'toolset', 'tool', 'extra'] + // 解构赋值只取前三个: parentId='507f1f77bcf86cd799439011', toolsetNameOrToolName='toolset', legacyToolName='tool' + // 所以 toolName 应该是 'tool',而不是 'tool/extra' + expect(result.toolName).toBe('tool'); + }); + + it('should handle empty tool name', () => { + const result = parseToolId('mcp-507f1f77bcf86cd799439011/'); + expect(result.parentId).toBe('507f1f77bcf86cd799439011'); + expect(result.toolName).toBe(''); + }); + + it('should handle empty toolset and tool name in old format', () => { + const result = parseToolId('mcp-507f1f77bcf86cd799439011//'); + expect(result.parentId).toBe('507f1f77bcf86cd799439011'); + expect(result.toolName).toBe(''); + }); + }); + + describe('向后兼容性', () => { + it('should correctly identify new format vs old format', () => { + // 新版格式: 只有两个部分(appId 和 toolName) + const newFormat = parseToolId('mcp-507f1f77bcf86cd799439011/tool'); + expect(newFormat.toolName).toBe('tool'); + + // 旧版格式: 有三个部分(appId, toolsetName, toolName) + const oldFormat = parseToolId('mcp-507f1f77bcf86cd799439011/toolset/tool'); + expect(oldFormat.toolName).toBe('tool'); + + // 两者的 toolName 应该相同 + expect(newFormat.toolName).toBe(oldFormat.toolName); + }); + + it('should handle migration from old to new format', () => { + const oldId = 'mcp-507f1f77bcf86cd799439011/MyToolset/weatherAPI'; + const newId = 'mcp-507f1f77bcf86cd799439011/weatherAPI'; + + const oldResult = parseToolId(oldId); + const newResult = parseToolId(newId); + + // 两种格式应该解析出相同的 parentId 和 toolName + expect(oldResult.parentId).toBe(newResult.parentId); + expect(oldResult.toolName).toBe(newResult.toolName); + }); + }); + + describe('真实场景测试', () => { + it('should parse real MCP tool ID', () => { + const result = parseToolId('mcp-65f8a9b2c3d4e5f6a7b8c9d0/filesystem/readFile'); + expect(result.parentId).toBe('65f8a9b2c3d4e5f6a7b8c9d0'); + expect(result.toolName).toBe('readFile'); + }); + + it('should parse real HTTP tool ID', () => { + const result = parseToolId('http-65f8a9b2c3d4e5f6a7b8c9d0/WeatherAPI'); + expect(result.parentId).toBe('65f8a9b2c3d4e5f6a7b8c9d0'); + expect(result.toolName).toBe('WeatherAPI'); + }); + + it('should handle Chinese tool names', () => { + const result = parseToolId('mcp-507f1f77bcf86cd799439011/天气查询'); + expect(result.parentId).toBe('507f1f77bcf86cd799439011'); + expect(result.toolName).toBe('天气查询'); + }); + + it('should handle tool names with spaces', () => { + const result = parseToolId('http-507f1f77bcf86cd799439011/Weather API'); + expect(result.parentId).toBe('507f1f77bcf86cd799439011'); + expect(result.toolName).toBe('Weather API'); + }); + }); + + describe('性能测试', () => { + it('should parse tool IDs efficiently', () => { + const iterations = 10000; + const start = Date.now(); + + for (let i = 0; i < iterations; i++) { + parseToolId('mcp-507f1f77bcf86cd799439011/toolset/tool'); + } + + const duration = Date.now() - start; + console.log(`Parsed ${iterations} tool IDs in ${duration}ms`); + + // 性能检查: 10000 次解析应该在 100ms 内完成 + expect(duration).toBeLessThan(100); + }); + + it('should handle batch parsing', () => { + const toolIds = [ + 'mcp-507f1f77bcf86cd799439011/tool1', + 'http-507f1f77bcf86cd799439011/tool2', + 'mcp-507f1f77bcf86cd799439011/toolset/tool3', + 'http-507f1f77bcf86cd799439011/toolset/tool4' + ]; + + const results = toolIds.map((id) => parseToolId(id)); + + expect(results).toHaveLength(4); + expect(results[0].toolName).toBe('tool1'); + expect(results[1].toolName).toBe('tool2'); + expect(results[2].toolName).toBe('tool3'); + expect(results[3].toolName).toBe('tool4'); + }); + }); + + describe('错误处理', () => { + it('should handle missing slash', () => { + const result = parseToolId('mcp-507f1f77bcf86cd799439011'); + expect(result.parentId).toBe('507f1f77bcf86cd799439011'); + expect(result.toolName).toBeUndefined(); + }); + + it('should handle only source prefix', () => { + const result = parseToolId('mcp-'); + expect(result.parentId).toBe(''); + expect(result.toolName).toBeUndefined(); + }); + + it('should handle malformed ID gracefully', () => { + // 虽然这些是无效的 ID,但函数应该不会崩溃 + expect(() => parseToolId('invalid')).not.toThrow(); + expect(() => parseToolId('')).not.toThrow(); + expect(() => parseToolId('/')).not.toThrow(); + }); + }); +});