From 017c1fcaac40812834cc4ea65e8a57a29567474d Mon Sep 17 00:00:00 2001 From: ONLY-yours <1349021570@qq.com> Date: Tue, 21 Apr 2026 12:07:24 +0800 Subject: [PATCH] =?UTF-8?q?=F0=9F=90=9B=20fix:=20make=20tool=20and=20skill?= =?UTF-8?q?=20activation=20case-insensitive?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit LLMs sometimes generate identifiers with different casing (e.g. "lobehub" instead of "LobeHub", or "Lobe-Web-Browsing" instead of "lobe-web-browsing"), causing activation to fail with "Not found". Normalize identifiers to lowercase before matching in all activator runtimes. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../src/ExecutionRuntime/index.ts | 10 +++++--- .../src/ExecutionRuntime/index.ts | 10 +++++--- .../toolExecution/serverRuntimes/activator.ts | 9 ++++++- .../__tests__/lobe-activator.test.ts | 25 +++++++++++++++++++ .../builtin/executors/lobe-activator.ts | 14 +++++++---- 5 files changed, 55 insertions(+), 13 deletions(-) diff --git a/packages/builtin-tool-activator/src/ExecutionRuntime/index.ts b/packages/builtin-tool-activator/src/ExecutionRuntime/index.ts index 7b1d7e8ec5..f63dbe695d 100644 --- a/packages/builtin-tool-activator/src/ExecutionRuntime/index.ts +++ b/packages/builtin-tool-activator/src/ExecutionRuntime/index.ts @@ -50,12 +50,16 @@ export class ActivatorExecutionRuntime { } try { + // Normalize identifiers to lowercase for case-insensitive matching + const normalizedIdentifiers = identifiers.map((id) => id.toLowerCase()); + const alreadyActive = this.service.getActivatedToolIds(); + const alreadyActiveLower = new Set(alreadyActive.map((id) => id.toLowerCase())); const toActivate: string[] = []; const alreadyActiveList: string[] = []; - for (const id of identifiers) { - if (alreadyActive.includes(id)) { + for (const id of normalizedIdentifiers) { + if (alreadyActiveLower.has(id)) { alreadyActiveList.push(id); } else { toActivate.push(id); @@ -65,7 +69,7 @@ export class ActivatorExecutionRuntime { // Fetch manifests for tools to activate const manifests = await this.service.getToolManifests(toActivate); - const foundIdentifiers = new Set(manifests.map((m) => m.identifier)); + const foundIdentifiers = new Set(manifests.map((m) => m.identifier.toLowerCase())); const notFound = toActivate.filter((id) => !foundIdentifiers.has(id)); const activatedTools: ActivatedToolInfo[] = manifests.map((m) => ({ diff --git a/packages/builtin-tool-skills/src/ExecutionRuntime/index.ts b/packages/builtin-tool-skills/src/ExecutionRuntime/index.ts index 21084ab51d..26e6a47a38 100644 --- a/packages/builtin-tool-skills/src/ExecutionRuntime/index.ts +++ b/packages/builtin-tool-skills/src/ExecutionRuntime/index.ts @@ -177,8 +177,9 @@ export class SkillsExecutionRuntime { } try { - // Check builtin skills first - const builtinSkill = this.builtinSkills.find((s) => s.name === id); + // Check builtin skills first (case-insensitive) + const idLower = id.toLowerCase(); + const builtinSkill = this.builtinSkills.find((s) => s.name.toLowerCase() === idLower); if (builtinSkill?.resources) { const meta = builtinSkill.resources[path]; if (meta?.content !== undefined) { @@ -231,8 +232,9 @@ export class SkillsExecutionRuntime { async activateSkill(args: ActivateSkillParams): Promise { const { name } = args; - // Check builtin skills first — no DB query needed - const builtinSkill = this.builtinSkills.find((s) => s.name === name); + // Check builtin skills first — no DB query needed (case-insensitive) + const nameLower = name.toLowerCase(); + const builtinSkill = this.builtinSkills.find((s) => s.name.toLowerCase() === nameLower); if (builtinSkill) { let content = builtinSkill.content; const hasResources = !!( diff --git a/src/server/services/toolExecution/serverRuntimes/activator.ts b/src/server/services/toolExecution/serverRuntimes/activator.ts index 28aad68456..83415cd0c2 100644 --- a/src/server/services/toolExecution/serverRuntimes/activator.ts +++ b/src/server/services/toolExecution/serverRuntimes/activator.ts @@ -45,8 +45,15 @@ export const activatorRuntime: ServerRuntimeRegistration = { // The caller is responsible for scoping this map to exclude hidden/internal tools. const results: ToolManifestInfo[] = []; + // Build a case-insensitive lookup map for tool manifests + const manifestMapLower: Record = {}; + for (const key of Object.keys(context.toolManifestMap)) { + manifestMapLower[key.toLowerCase()] = key; + } + for (const id of identifiers) { - const manifest = context.toolManifestMap[id]; + const actualKey = manifestMapLower[id.toLowerCase()]; + const manifest = actualKey ? context.toolManifestMap[actualKey] : undefined; if (!manifest) continue; results.push({ diff --git a/src/store/tool/slices/builtin/executors/__tests__/lobe-activator.test.ts b/src/store/tool/slices/builtin/executors/__tests__/lobe-activator.test.ts index 3228e5b578..e64605a5a7 100644 --- a/src/store/tool/slices/builtin/executors/__tests__/lobe-activator.test.ts +++ b/src/store/tool/slices/builtin/executors/__tests__/lobe-activator.test.ts @@ -96,6 +96,31 @@ describe('lobe-activator executor discovery allowlist', () => { expect(activatedIds).toHaveLength(0); }); + it('should match identifiers case-insensitively', async () => { + const tool = makeBuiltinTool('lobe-web-browsing'); + + mockGetState.mockReturnValue({ + builtinTools: [tool], + installedPlugins: [], + }); + + mockAvailableToolsForDiscovery.mockReturnValue([ + { description: 'desc', identifier: 'lobe-web-browsing', name: 'lobe-web-browsing' }, + ]); + + const result = await activatorExecutor.invoke( + 'activateTools', + { identifiers: ['Lobe-Web-Browsing'] }, + { messageId: 'msg-1', operationId: 'op-1' }, + ); + + expect(result.success).toBe(true); + + const state = result.state as any; + const activatedIds = state.activatedTools?.map((t: any) => t.identifier) ?? []; + expect(activatedIds).toContain('lobe-web-browsing'); + }); + it('should allow discoverable plugins', async () => { const plugin = makePlugin('community-plugin'); diff --git a/src/store/tool/slices/builtin/executors/lobe-activator.ts b/src/store/tool/slices/builtin/executors/lobe-activator.ts index 9edf8f7dbb..a4c8c8d167 100644 --- a/src/store/tool/slices/builtin/executors/lobe-activator.ts +++ b/src/store/tool/slices/builtin/executors/lobe-activator.ts @@ -43,15 +43,17 @@ const service: ActivatorRuntimeService = { // Only allow activation of tools that passed discovery filters // (discoverable, platform-available, not internal/hidden) const discoverable = new Set( - toolSelectors.availableToolsForDiscovery(s).map((t) => t.identifier), + toolSelectors.availableToolsForDiscovery(s).map((t) => t.identifier.toLowerCase()), ); - const allowedIds = identifiers.filter((id) => discoverable.has(id)); + const allowedIds = identifiers.filter((id) => discoverable.has(id.toLowerCase())); const results: ToolManifestInfo[] = []; for (const id of allowedIds) { + const idLower = id.toLowerCase(); + // Search builtin tools - const builtin = s.builtinTools.find((t) => t.identifier === id); + const builtin = s.builtinTools.find((t) => t.identifier.toLowerCase() === idLower); if (builtin) { results.push({ apiDescriptions: builtin.manifest.api.map((a) => ({ @@ -67,7 +69,7 @@ const service: ActivatorRuntimeService = { } // Search installed plugins - const plugin = s.installedPlugins.find((p) => p.identifier === id); + const plugin = s.installedPlugins.find((p) => p.identifier.toLowerCase() === idLower); if (plugin?.manifest) { results.push({ apiDescriptions: (plugin.manifest.api || []).map((a) => ({ @@ -84,7 +86,9 @@ const service: ActivatorRuntimeService = { // Search LobeHub Skill servers const lobehubSkillServer = s.lobehubSkillServers?.find( - (server) => server.identifier === id && server.status === LobehubSkillStatus.CONNECTED, + (server) => + server.identifier.toLowerCase() === idLower && + server.status === LobehubSkillStatus.CONNECTED, ); if (lobehubSkillServer?.tools) { results.push({