From 46f9135308e643a0ab2e7e765dc0413061831765 Mon Sep 17 00:00:00 2001 From: Innei Date: Thu, 12 Mar 2026 20:17:02 +0800 Subject: [PATCH] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20refactor(tool):=20centrali?= =?UTF-8?q?ze=20availability=20checks=20(#12938)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * ♻️ refactor(tool): centralize availability checks * 🐛 fix(tool): preserve windows skill fallback * 🐛 fix(tool): restore stdio engine filtering --- src/features/PluginTag/index.tsx | 23 ++++++--- src/helpers/skillFilters.test.ts | 16 +++++- src/helpers/skillFilters.ts | 13 ++++- src/helpers/toolAvailability.test.ts | 53 ++++++++++++++++++++ src/helpers/toolAvailability.ts | 54 +++++++++++++++++++++ src/helpers/toolEngineering/index.ts | 17 ++++--- src/services/chat/mecha/skillEngineering.ts | 4 +- src/store/tool/selectors/tool.ts | 15 +++--- src/store/tool/slices/builtin/selectors.ts | 16 +++--- src/store/tool/slices/plugin/selectors.ts | 5 +- 10 files changed, 177 insertions(+), 39 deletions(-) create mode 100644 src/helpers/toolAvailability.test.ts create mode 100644 src/helpers/toolAvailability.ts diff --git a/src/features/PluginTag/index.tsx b/src/features/PluginTag/index.tsx index e1cfd65c79..6b8d25390f 100644 --- a/src/features/PluginTag/index.tsx +++ b/src/features/PluginTag/index.tsx @@ -4,11 +4,12 @@ import { type MenuProps } from '@lobehub/ui'; import { Center, DropdownMenu, Icon, Tag } from '@lobehub/ui'; import isEqual from 'fast-deep-equal'; import { LucideToyBrick } from 'lucide-react'; -import { memo } from 'react'; +import { memo, useMemo } from 'react'; import Avatar from '@/components/Plugins/PluginAvatar'; +import { filterToolIdsByCurrentEnv } from '@/helpers/toolAvailability'; import { pluginHelpers, useToolStore } from '@/store/tool'; -import { toolSelectors } from '@/store/tool/selectors'; +import { pluginSelectors, toolSelectors } from '@/store/tool/selectors'; import PluginStatus from './PluginStatus'; @@ -18,12 +19,18 @@ export interface PluginTagProps { const PluginTag = memo(({ plugins }) => { const list = useToolStore(toolSelectors.metaList, isEqual); + const installedPlugins = useToolStore(pluginSelectors.installedPlugins, isEqual); - const displayPlugin = useToolStore(toolSelectors.getMetaById(plugins[0]), isEqual); + const visiblePlugins = useMemo( + () => filterToolIdsByCurrentEnv(plugins, { installedPlugins }), + [installedPlugins, plugins], + ); - if (plugins.length === 0) return null; + const displayPlugin = useToolStore(toolSelectors.getMetaById(visiblePlugins[0] || ''), isEqual); - const items: MenuProps['items'] = plugins.map((id) => { + if (visiblePlugins.length === 0) return null; + + const items: MenuProps['items'] = visiblePlugins.map((id) => { const item = list.find((i) => i.identifier === id); const isDeprecated = !item; @@ -46,14 +53,14 @@ const PluginTag = memo(({ plugins }) => { }; }); - const count = plugins.length; + const count = visiblePlugins.length; return ( {} - {pluginHelpers.getPluginTitle(displayPlugin) || plugins[0]} - {count > 1 &&
({plugins.length - 1}+)
} + {pluginHelpers.getPluginTitle(displayPlugin) || visiblePlugins[0]} + {count > 1 &&
({visiblePlugins.length - 1}+)
}
); diff --git a/src/helpers/skillFilters.test.ts b/src/helpers/skillFilters.test.ts index b43ff1b136..c4b1e3704e 100644 --- a/src/helpers/skillFilters.test.ts +++ b/src/helpers/skillFilters.test.ts @@ -1,7 +1,12 @@ -import { describe, expect, it } from 'vitest'; +import { afterEach, describe, expect, it, vi } from 'vitest'; import { filterBuiltinSkills, shouldEnableBuiltinSkill } from './skillFilters'; +afterEach(() => { + vi.unstubAllGlobals(); + vi.resetModules(); +}); + describe('skillFilters', () => { it('should disable agent-browser on web environment', () => { expect(shouldEnableBuiltinSkill('lobe-agent-browser', { isDesktop: false })).toBe(false); @@ -19,6 +24,15 @@ describe('skillFilters', () => { ).toBe(false); }); + it('should preserve Windows detection for partial context overrides', async () => { + vi.stubGlobal('process', { ...process, platform: 'win32' }); + vi.resetModules(); + + const { shouldEnableBuiltinSkill } = await import('./skillFilters'); + + expect(shouldEnableBuiltinSkill('lobe-agent-browser', { isDesktop: true })).toBe(false); + }); + it('should keep non-desktop-only skills enabled', () => { expect(shouldEnableBuiltinSkill('lobe-artifacts', { isDesktop: false })).toBe(true); }); diff --git a/src/helpers/skillFilters.ts b/src/helpers/skillFilters.ts index 77dfa88e2c..9ccb825574 100644 --- a/src/helpers/skillFilters.ts +++ b/src/helpers/skillFilters.ts @@ -27,13 +27,22 @@ const DEFAULT_CONTEXT: BuiltinSkillFilterContext = { isWindows: getIsWindows(), }; +const resolveBuiltinSkillFilterContext = ( + context: BuiltinSkillFilterContext = DEFAULT_CONTEXT, +): BuiltinSkillFilterContext => ({ + isDesktop: context.isDesktop ?? DEFAULT_CONTEXT.isDesktop, + isWindows: context.isWindows ?? DEFAULT_CONTEXT.isWindows, +}); + export const shouldEnableBuiltinSkill = ( skillId: string, context: BuiltinSkillFilterContext = DEFAULT_CONTEXT, ): boolean => { + const resolvedContext = resolveBuiltinSkillFilterContext(context); + if (DESKTOP_ONLY_BUILTIN_SKILLS.has(skillId)) { - if (!context.isDesktop) return false; - if (WINDOWS_HIDDEN_BUILTIN_SKILLS.has(skillId) && context.isWindows) return false; + if (!resolvedContext.isDesktop) return false; + if (WINDOWS_HIDDEN_BUILTIN_SKILLS.has(skillId) && resolvedContext.isWindows) return false; return true; } diff --git a/src/helpers/toolAvailability.test.ts b/src/helpers/toolAvailability.test.ts new file mode 100644 index 0000000000..10ad2eaf2e --- /dev/null +++ b/src/helpers/toolAvailability.test.ts @@ -0,0 +1,53 @@ +import { describe, expect, it } from 'vitest'; + +import { + filterToolIdsByCurrentEnv, + isInstalledPluginAvailableInCurrentEnv, + isToolAvailableInCurrentEnv, +} from './toolAvailability'; + +describe('toolAvailability', () => { + it('should hide desktop-only builtin skills in web', () => { + expect( + filterToolIdsByCurrentEnv(['lobe-agent-browser', 'lobe-web-browsing'], { isDesktop: false }), + ).toEqual(['lobe-web-browsing']); + }); + + it('should hide stdio mcp plugins in web', () => { + expect( + filterToolIdsByCurrentEnv(['local-mcp', 'remote-mcp'], { + installedPlugins: [ + { + customParams: { mcp: { type: 'stdio' } }, + identifier: 'local-mcp', + }, + ], + isDesktop: false, + }), + ).toEqual(['remote-mcp']); + }); + + it('should keep deprecated tool ids visible for cleanup', () => { + expect(filterToolIdsByCurrentEnv(['deleted-plugin'], { isDesktop: false })).toEqual([ + 'deleted-plugin', + ]); + }); + + it('should mark stdio mcp plugins as unavailable in web', () => { + expect( + isInstalledPluginAvailableInCurrentEnv( + { customParams: { mcp: { type: 'stdio' } }, identifier: 'local-mcp' }, + { isDesktop: false }, + ), + ).toBe(false); + }); + + it('should mark desktop-only builtin tools as unavailable in web when injected', () => { + expect( + isToolAvailableInCurrentEnv('lobe-agent-browser', { + installedPlugins: [], + isDesktop: false, + }), + ).toBe(false); + }); +}); diff --git a/src/helpers/toolAvailability.ts b/src/helpers/toolAvailability.ts new file mode 100644 index 0000000000..47b9b5a748 --- /dev/null +++ b/src/helpers/toolAvailability.ts @@ -0,0 +1,54 @@ +import { isDesktop } from '@lobechat/const'; + +import { shouldEnableBuiltinSkill } from './skillFilters'; +import { shouldEnableTool } from './toolFilters'; + +export interface ToolAvailabilityInstalledPlugin { + customParams?: { + mcp?: { + type?: string; + } | null; + } | null; + identifier: string; +} + +export interface ToolAvailabilityContext { + installedPlugins?: ToolAvailabilityInstalledPlugin[]; + isDesktop?: boolean; + isWindows?: boolean; +} + +export const isBuiltinToolAvailableInCurrentEnv = (id: string) => shouldEnableTool(id); + +export const isBuiltinSkillAvailableInCurrentEnv = ( + id: string, + context: Omit = {}, +) => { + if (context.isDesktop === undefined && context.isWindows === undefined) { + return shouldEnableBuiltinSkill(id); + } + + return shouldEnableBuiltinSkill(id, { + isDesktop: context.isDesktop ?? isDesktop, + isWindows: context.isWindows, + }); +}; + +export const isInstalledPluginAvailableInCurrentEnv = ( + plugin: ToolAvailabilityInstalledPlugin, + context: Omit = {}, +) => (context.isDesktop ?? isDesktop) || plugin.customParams?.mcp?.type !== 'stdio'; + +export const isToolAvailableInCurrentEnv = (id: string, context: ToolAvailabilityContext = {}) => { + if (!isBuiltinToolAvailableInCurrentEnv(id)) return false; + if (!isBuiltinSkillAvailableInCurrentEnv(id, context)) return false; + + const plugin = context.installedPlugins?.find((item) => item.identifier === id); + + if (!plugin) return true; + + return isInstalledPluginAvailableInCurrentEnv(plugin, context); +}; + +export const filterToolIdsByCurrentEnv = (ids: string[], context: ToolAvailabilityContext = {}) => + ids.filter((id) => isToolAvailableInCurrentEnv(id, context)); diff --git a/src/helpers/toolEngineering/index.ts b/src/helpers/toolEngineering/index.ts index 9ae0a8eef4..6770e842ee 100644 --- a/src/helpers/toolEngineering/index.ts +++ b/src/helpers/toolEngineering/index.ts @@ -7,12 +7,12 @@ import { LocalSystemManifest } from '@lobechat/builtin-tool-local-system'; import { MemoryManifest } from '@lobechat/builtin-tool-memory'; import { WebBrowsingManifest } from '@lobechat/builtin-tool-web-browsing'; import { alwaysOnToolIds, defaultToolIds } from '@lobechat/builtin-tools'; -import { isDesktop } from '@lobechat/const'; import { createEnableChecker, type PluginEnableChecker } from '@lobechat/context-engine'; import { ToolsEngine } from '@lobechat/context-engine'; import { type ChatCompletionTool, type WorkingModel } from '@lobechat/types'; import { type LobeChatPluginManifest } from '@lobehub/chat-plugin-sdk'; +import { isToolAvailableInCurrentEnv } from '@/helpers/toolAvailability'; import { getAgentStoreState } from '@/store/agent'; import { agentChatConfigSelectors, agentSelectors } from '@/store/agent/selectors'; import { getToolStoreState } from '@/store/tool'; @@ -24,7 +24,6 @@ import { import { getSearchConfig } from '../getSearchConfig'; import { isCanUseFC } from '../isCanUseFC'; -import { shouldEnableTool } from '../toolFilters'; /** * Tools engine configuration options @@ -97,13 +96,15 @@ export const createAgentToolsEngine = ( enableChecker: createEnableChecker({ allowExplicitActivation: true, platformFilter: ({ pluginId }) => { - // Platform-specific constraints (e.g., LocalSystem desktop-only) - if (!shouldEnableTool(pluginId)) return false; + const toolStoreState = getToolStoreState(); + const installedPlugin = pluginSelectors.getInstalledPluginById(pluginId)(toolStoreState); - // Filter stdio MCP tools in non-desktop environments - if (!isDesktop) { - const plugin = pluginSelectors.getInstalledPluginById(pluginId)(getToolStoreState()); - if (plugin?.customParams?.mcp?.type === 'stdio') return false; + if ( + !isToolAvailableInCurrentEnv(pluginId, { + installedPlugins: installedPlugin ? [installedPlugin] : toolStoreState.installedPlugins, + }) + ) { + return false; } return undefined; // fall through to rules diff --git a/src/services/chat/mecha/skillEngineering.ts b/src/services/chat/mecha/skillEngineering.ts index 6f3206efd8..3620663b40 100644 --- a/src/services/chat/mecha/skillEngineering.ts +++ b/src/services/chat/mecha/skillEngineering.ts @@ -1,6 +1,6 @@ import { SkillEngine } from '@lobechat/context-engine'; -import { shouldEnableBuiltinSkill } from '@/helpers/skillFilters'; +import { isBuiltinSkillAvailableInCurrentEnv } from '@/helpers/toolAvailability'; import { getToolStoreState } from '@/store/tool'; /** @@ -15,7 +15,7 @@ export const createSkillEngine = (): SkillEngine => { // Source 1: builtin skills const builtinMetas = (toolState.builtinSkills || []) - .filter((s) => shouldEnableBuiltinSkill(s.identifier)) + .filter((s) => isBuiltinSkillAvailableInCurrentEnv(s.identifier)) .map((s) => ({ description: s.description, identifier: s.identifier, diff --git a/src/store/tool/selectors/tool.ts b/src/store/tool/selectors/tool.ts index 8a5192a47f..9ba1269f95 100644 --- a/src/store/tool/selectors/tool.ts +++ b/src/store/tool/selectors/tool.ts @@ -1,12 +1,11 @@ -import { - getKlavisServerByServerIdentifier, - getLobehubSkillProviderById, - isDesktop, -} from '@lobechat/const'; +import { getKlavisServerByServerIdentifier, getLobehubSkillProviderById } from '@lobechat/const'; import { type RenderDisplayControl } from '@lobechat/types'; import { type LobeChatPluginManifest } from '@lobehub/chat-plugin-sdk'; -import { shouldEnableTool } from '@/helpers/toolFilters'; +import { + isInstalledPluginAvailableInCurrentEnv, + isToolAvailableInCurrentEnv, +} from '@/helpers/toolAvailability'; import { type MetaData } from '@/types/meta'; import { type LobeToolMeta } from '@/types/tool/tool'; @@ -117,7 +116,7 @@ const availableToolsForDiscovery = (s: ToolStoreState): AvailableToolForDiscover const builtinItems = s.builtinTools .filter((tool) => tool.discoverable !== false) .filter((tool) => !builtinSkillIds.has(tool.identifier)) - .filter((tool) => shouldEnableTool(tool.identifier)) // platform check (e.g., desktop-only) + .filter((tool) => isToolAvailableInCurrentEnv(tool.identifier)) .map((tool) => ({ description: tool.manifest.meta?.description || '', identifier: tool.identifier, @@ -131,7 +130,7 @@ const availableToolsForDiscovery = (s: ToolStoreState): AvailableToolForDiscover .filter((p) => !lobehubSkillIds.has(p.identifier)) .filter((p) => !agentSkillIds.has(p.identifier)) .filter((p) => !p.customParams?.klavis) // extra safety for Klavis plugins - .filter((p) => isDesktop || p.customParams?.mcp?.type !== 'stdio') // platform check + .filter((plugin) => isInstalledPluginAvailableInCurrentEnv(plugin)) .map((plugin) => { const meta = plugin.manifest?.meta; return { diff --git a/src/store/tool/slices/builtin/selectors.ts b/src/store/tool/slices/builtin/selectors.ts index 471907c2d3..3efb5dc6b1 100644 --- a/src/store/tool/slices/builtin/selectors.ts +++ b/src/store/tool/slices/builtin/selectors.ts @@ -1,7 +1,9 @@ import { type BuiltinSkill, type LobeToolMeta } from '@lobechat/types'; -import { shouldEnableBuiltinSkill } from '@/helpers/skillFilters'; -import { shouldEnableTool } from '@/helpers/toolFilters'; +import { + isBuiltinSkillAvailableInCurrentEnv, + isBuiltinToolAvailableInCurrentEnv, +} from '@/helpers/toolAvailability'; import { type ToolStoreState } from '../../initialState'; import { agentSkillsSelectors } from '../agentSkills/selectors'; @@ -26,7 +28,7 @@ const toBuiltinMetaWithAvailability = ( t: ToolStoreState['builtinTools'][number], ): LobeToolMetaWithAvailability => ({ ...toBuiltinMeta(t), - availableInWeb: shouldEnableTool(t.identifier), + availableInWeb: isBuiltinToolAvailableInCurrentEnv(t.identifier), }); const toSkillMeta = (s: BuiltinSkill): LobeToolMeta => ({ @@ -42,7 +44,7 @@ const toSkillMeta = (s: BuiltinSkill): LobeToolMeta => ({ const toSkillMetaWithAvailability = (s: BuiltinSkill): LobeToolMetaWithAvailability => ({ ...toSkillMeta(s), - availableInWeb: shouldEnableBuiltinSkill(s.identifier), + availableInWeb: isBuiltinSkillAvailableInCurrentEnv(s.identifier), }); const getKlavisMetas = (s: ToolStoreState): LobeToolMeta[] => @@ -79,7 +81,7 @@ const metaList = (s: ToolStoreState): LobeToolMeta[] => { if (item.hidden) return false; // Filter platform-specific tools (e.g., LocalSystem desktop-only) - if (!shouldEnableTool(item.identifier)) return false; + if (!isBuiltinToolAvailableInCurrentEnv(item.identifier)) return false; // Exclude uninstalled tools if (uninstalledBuiltinTools.includes(item.identifier)) { @@ -92,7 +94,7 @@ const metaList = (s: ToolStoreState): LobeToolMeta[] => { const skillMetas = (s.builtinSkills || []) .filter((skill) => { - if (!shouldEnableBuiltinSkill(skill.identifier)) return false; + if (!isBuiltinSkillAvailableInCurrentEnv(skill.identifier)) return false; if (uninstalledBuiltinTools.includes(skill.identifier)) return false; return true; @@ -158,7 +160,7 @@ const installedAllMetaList = (s: ToolStoreState): LobeToolMetaWithAvailability[] */ const installedBuiltinSkills = (s: ToolStoreState): BuiltinSkill[] => (s.builtinSkills || []).filter((skill) => { - if (!shouldEnableBuiltinSkill(skill.identifier)) return false; + if (!isBuiltinSkillAvailableInCurrentEnv(skill.identifier)) return false; if (s.uninstalledBuiltinTools.includes(skill.identifier)) return false; return true; diff --git a/src/store/tool/slices/plugin/selectors.ts b/src/store/tool/slices/plugin/selectors.ts index 321ee520d0..3dd5b5d4ad 100644 --- a/src/store/tool/slices/plugin/selectors.ts +++ b/src/store/tool/slices/plugin/selectors.ts @@ -1,7 +1,7 @@ -import { isDesktop } from '@lobechat/const'; import { type LobeChatPluginManifest } from '@lobehub/chat-plugin-sdk'; import { uniq } from 'es-toolkit/compat'; +import { isInstalledPluginAvailableInCurrentEnv } from '@/helpers/toolAvailability'; import { type InstallPluginMeta, type LobeToolCustomPlugin } from '@/types/tool/plugin'; import { type ToolStoreState } from '../../initialState'; @@ -57,8 +57,7 @@ const installedPluginMetaList = (s: ToolStoreState) => installedPlugins(s) // Filter out Klavis plugins (they have their own display location) .filter((p) => !p.customParams?.klavis) - // Filter out stdio MCP plugins on non-desktop (stdio requires Electron IPC) - .filter((p) => isDesktop || p.customParams?.mcp?.type !== 'stdio') + .filter((plugin) => isInstalledPluginAvailableInCurrentEnv(plugin)) .map((p) => ({ author: p.manifest?.author, createdAt: p.manifest?.createdAt || (p.manifest as any)?.createAt,