♻️ refactor(tool): centralize availability checks (#12938)

* ♻️ refactor(tool): centralize availability checks

* 🐛 fix(tool): preserve windows skill fallback

* 🐛 fix(tool): restore stdio engine filtering
This commit is contained in:
Innei
2026-03-12 20:17:02 +08:00
committed by GitHub
parent fd90f83f0f
commit 46f9135308
10 changed files with 177 additions and 39 deletions

View File

@@ -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<PluginTagProps>(({ 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<PluginTagProps>(({ plugins }) => {
};
});
const count = plugins.length;
const count = visiblePlugins.length;
return (
<DropdownMenu items={items}>
<Tag style={{ cursor: 'pointer' }}>
{<Icon icon={LucideToyBrick} />}
{pluginHelpers.getPluginTitle(displayPlugin) || plugins[0]}
{count > 1 && <div>({plugins.length - 1}+)</div>}
{pluginHelpers.getPluginTitle(displayPlugin) || visiblePlugins[0]}
{count > 1 && <div>({visiblePlugins.length - 1}+)</div>}
</Tag>
</DropdownMenu>
);

View File

@@ -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);
});

View File

@@ -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;
}

View File

@@ -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);
});
});

View File

@@ -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<ToolAvailabilityContext, 'installedPlugins'> = {},
) => {
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<ToolAvailabilityContext, 'installedPlugins'> = {},
) => (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));

View File

@@ -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

View File

@@ -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,

View File

@@ -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 {

View File

@@ -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;

View File

@@ -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<InstallPluginMeta>((p) => ({
author: p.manifest?.author,
createdAt: p.manifest?.createdAt || (p.manifest as any)?.createAt,