mirror of
https://github.com/lobehub/lobehub.git
synced 2026-03-26 13:19:34 +07:00
♻️ 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:
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
53
src/helpers/toolAvailability.test.ts
Normal file
53
src/helpers/toolAvailability.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
54
src/helpers/toolAvailability.ts
Normal file
54
src/helpers/toolAvailability.ts
Normal 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));
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user