mirror of
https://github.com/lobehub/lobehub
synced 2026-04-21 09:37:28 +00:00
🐛 fix: make tool and skill activation case-insensitive
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) <noreply@anthropic.com>
This commit is contained in:
parent
c5db823a69
commit
017c1fcaac
5 changed files with 55 additions and 13 deletions
|
|
@ -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) => ({
|
||||
|
|
|
|||
|
|
@ -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<BuiltinServerRuntimeOutput> {
|
||||
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 = !!(
|
||||
|
|
|
|||
|
|
@ -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<string, string> = {};
|
||||
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({
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
||||
|
|
|
|||
|
|
@ -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({
|
||||
|
|
|
|||
Loading…
Reference in a new issue