diff --git a/locales/en-US/setting.json b/locales/en-US/setting.json index a3b563e3ed..cc6dd10a4a 100644 --- a/locales/en-US/setting.json +++ b/locales/en-US/setting.json @@ -1014,6 +1014,7 @@ "tab.usage": "Usage", "tools.activation.auto": "Auto", "tools.activation.auto.desc": "Smart", + "tools.activation.fixed.hint": "Always on — managed by the app and can’t be turned off", "tools.activation.pinned": "Pinned", "tools.activation.pinned.desc": "Always On", "tools.add": "Add Skill", @@ -1033,6 +1034,8 @@ "tools.builtins.lobe-agent-documents.title": "Documents", "tools.builtins.lobe-agent-management.description": "Create, manage, and orchestrate AI agents", "tools.builtins.lobe-agent-management.title": "Agent Management", + "tools.builtins.lobe-agent.description": "Built-in Lobe Agent capabilities: plan and todo management, sub-agent dispatch, and visual media analysis", + "tools.builtins.lobe-agent.title": "Lobe Agent", "tools.builtins.lobe-artifacts.description": "Generate and preview interactive UI components and visualizations", "tools.builtins.lobe-artifacts.readme": "Generate and live-preview interactive UI components, data visualizations, charts, SVG graphics, and web applications. Create rich visual content that users can interact with directly.", "tools.builtins.lobe-artifacts.title": "Artifacts", diff --git a/locales/zh-CN/setting.json b/locales/zh-CN/setting.json index 17c16ab1cc..2f0cc87d3b 100644 --- a/locales/zh-CN/setting.json +++ b/locales/zh-CN/setting.json @@ -1015,6 +1015,7 @@ "tab.usage": "用量", "tools.activation.auto": "自动", "tools.activation.auto.desc": "智能调用", + "tools.activation.fixed.hint": "由应用强制开启,始终可用,无法关闭", "tools.activation.pinned": "固定启用", "tools.activation.pinned.desc": "始终注入", "tools.add": "集成技能", @@ -1034,6 +1035,8 @@ "tools.builtins.lobe-agent-documents.title": "文档", "tools.builtins.lobe-agent-management.description": "创建、管理并编排 AI 助手", "tools.builtins.lobe-agent-management.title": "助手管理", + "tools.builtins.lobe-agent.description": "内置 Lobe Agent 能力:计划与待办管理、子助手调度、视觉媒体分析", + "tools.builtins.lobe-agent.title": "Lobe Agent", "tools.builtins.lobe-artifacts.description": "生成并预览交互式 UI 组件和可视化内容", "tools.builtins.lobe-artifacts.readme": "生成并实时预览交互式 UI 组件、数据可视化、图表、SVG 图形和 Web 应用。创建用户可直接交互的丰富可视化内容。", "tools.builtins.lobe-artifacts.title": "Artifacts", diff --git a/packages/builtin-tools/src/index.ts b/packages/builtin-tools/src/index.ts index ad7dbf196d..f8585be2dc 100644 --- a/packages/builtin-tools/src/index.ts +++ b/packages/builtin-tools/src/index.ts @@ -55,8 +55,20 @@ export const defaultToolIds = [ /** * Tool IDs that are always enabled regardless of user selection. * These are core system tools that the agent needs to function properly. + * + * `lobe-agent` is listed first: its built-in capabilities (plan + todo management, + * sub-agent dispatch, visual-media fallback) should be available on every agent-mode turn, + * not gated behind explicit injection. NOTE: these rules only apply in agent mode — chat + * mode (`enableAgentMode === false`) drops `alwaysOnToolIds` entirely. In manual + * skill-activate mode the discovery tools in `manualModeExcludeToolIds` are still removed + * from the defaults before the enable checker runs, so they end up disabled there. + * + * This list is also the source for the chat-input Tools popover's read-only "Pinned" + * section (`builtinToolSelectors.fixedDisplayMetaList`), so users can see what the app + * keeps active — that selector applies the same manual-mode exclusion to stay truthful. */ export const alwaysOnToolIds = [ + LobeAgentManifest.identifier, LobeActivatorManifest.identifier, SkillsManifest.identifier, SkillStoreManifest.identifier, diff --git a/src/features/ChatInput/ActionBar/Tools/useControls.tsx b/src/features/ChatInput/ActionBar/Tools/useControls.tsx index 2e1b933fa8..b0148e113a 100644 --- a/src/features/ChatInput/ActionBar/Tools/useControls.tsx +++ b/src/features/ChatInput/ActionBar/Tools/useControls.tsx @@ -157,6 +157,17 @@ const styles = createStaticStyles(({ css }) => ({ iconPinned: css` color: ${cssVar.colorInfo}; `, + fixedIndicator: css` + display: inline-flex; + flex: none; + align-items: center; + justify-content: center; + + width: 24px; + height: 24px; + + color: ${cssVar.colorTextQuaternary}; + `, policyButton: css` cursor: pointer; @@ -428,6 +439,14 @@ export const useControls = ({ closeDropdown }: { closeDropdown?: () => void } = : builtinToolSelectors.metaList, isEqual, ); + // Application-fixed tools (always-on, not user-controllable, e.g. lobe-agent). + // Rendered read-only at the top of the "Pinned" section so users can see what the + // app keeps active for every conversation. Mode-aware: in manual skill-activate mode the + // discovery tools the engine strips (activator, skill-store) are dropped from the list. + const fixedDisplayList = useToolStore( + builtinToolSelectors.fixedDisplayMetaList({ isManualMode: isManualSkillMode }), + isEqual, + ); const plugins = useAgentStore((s) => agentByIdSelectors.getAgentPluginsById(agentId)(s)); const updateSkillPolicy = useCallback( @@ -943,6 +962,70 @@ export const useControls = ({ closeDropdown }: { closeDropdown?: () => void } = [filteredBuiltinList, t, createManagedSkillItem, uninstallBuiltinTool], ); + // Application-fixed tool items (read-only). Always-on tools owned by the runtime + // (lobe-agent + always-on infra), so they get a fixed indicator instead of the policy + // menu and can't be switched to "auto" or uninstalled. + const fixedItems = useMemo( + () => + fixedDisplayList.map((item) => { + const title = t(`tools.builtins.${item.identifier}.title` as any, { + defaultValue: item.meta?.title || item.identifier, + }); + const icon = item.meta?.avatar ? ( + + ) : ( + + ); + const popoverContent = ( + + ) : ( + + ) + } + /> + ); + + return { + closeOnClick: false, + key: item.identifier, + label: ( + + + {icon} + {title} + {officialTag} + + + + + + + + + + + ), + popoverContent, + searchText: `${title} ${item.identifier}`, + } as SkillMenuItem; + }), + [fixedDisplayList, t], + ); + // Builtin Agent Skills list items (grouped under LobeHub) const builtinAgentSkillItems = useMemo( () => @@ -1190,7 +1273,8 @@ export const useControls = ({ closeDropdown }: { closeDropdown?: () => void } = }; const allPinnedItems = allSkillItems.filter((item) => checkedSet.has(String(item.key))); const allAutoItems = allSkillItems.filter((item) => !checkedSet.has(String(item.key))); - const pinnedItems = filterBySearch(allPinnedItems); + // App-fixed tools always lead the pinned section, ahead of user-pinned plugins. + const pinnedItems = filterBySearch([...fixedItems, ...allPinnedItems]); const autoItems = filterBySearch(allAutoItems); const renderActivationGroupLabel = ({ @@ -1270,11 +1354,11 @@ export const useControls = ({ closeDropdown }: { closeDropdown?: () => void } = ); const marketFooter = - allSkillItems.length > 0 ? ( + allSkillItems.length > 0 || fixedItems.length > 0 ? (
- {allPinnedItems.length} + {allPinnedItems.length + fixedItems.length} @@ -1703,6 +1787,6 @@ export const useControls = ({ closeDropdown }: { closeDropdown?: () => void } = marketFooter, marketHeader, marketItems, - pinnedCount: allPinnedItems.length, + pinnedCount: allPinnedItems.length + fixedItems.length, }; }; diff --git a/src/helpers/toolEngineering/index.test.ts b/src/helpers/toolEngineering/index.test.ts index 2f47854d7c..754f872081 100644 --- a/src/helpers/toolEngineering/index.test.ts +++ b/src/helpers/toolEngineering/index.test.ts @@ -248,11 +248,12 @@ describe('toolEngineering', () => { provider: 'openai', }); - expect(result.enabledToolIds).toEqual(['search', 'lobe-web-browsing']); - expect(result.enabledToolIds).toHaveLength(2); + // lobe-agent is always-on (alwaysOnToolIds), so it rides along with user tools. + expect(result.enabledToolIds).toEqual(['search', 'lobe-web-browsing', 'lobe-agent']); + expect(result.enabledToolIds).toHaveLength(3); }); - it('should enable visual understanding when it is injected into runtime plugin ids', () => { + it('should enable lobe-agent when it is injected into runtime plugin ids', () => { const toolsEngine = createAgentToolsEngine({ model: 'deepseek-chat', provider: 'deepseek' }, [ 'lobe-agent', ]); @@ -266,7 +267,7 @@ describe('toolEngineering', () => { expect(result.enabledToolIds).toContain('lobe-agent'); }); - it('should not enable visual understanding by default', () => { + it('should enable lobe-agent by default since it is always-on', () => { const toolsEngine = createAgentToolsEngine({ model: 'deepseek-chat', provider: 'deepseek', @@ -278,7 +279,7 @@ describe('toolEngineering', () => { toolIds: [], }); - expect(result.enabledToolIds).not.toContain('lobe-agent'); + expect(result.enabledToolIds).toContain('lobe-agent'); }); }); diff --git a/src/locales/default/setting.ts b/src/locales/default/setting.ts index 1a6c5f42d7..4f5a87f21c 100644 --- a/src/locales/default/setting.ts +++ b/src/locales/default/setting.ts @@ -1213,6 +1213,9 @@ When I am ___, I need ___ 'tools.builtins.lobe-agent-documents.title': 'Documents', 'tools.builtins.lobe-agent-management.description': 'Create, manage, and orchestrate AI agents', 'tools.builtins.lobe-agent-management.title': 'Agent Management', + 'tools.builtins.lobe-agent.description': + 'Built-in Lobe Agent capabilities: plan and todo management, sub-agent dispatch, and visual media analysis', + 'tools.builtins.lobe-agent.title': 'Lobe Agent', 'tools.builtins.lobe-brief.description': 'Report progress, deliver results, and request user decisions', 'tools.builtins.lobe-brief.title': 'Brief Tools', @@ -1501,6 +1504,7 @@ When I am ___, I need ___ 'tools.search': 'Search skills...', 'tools.activation.auto': 'Auto', 'tools.activation.auto.desc': 'Smart', + 'tools.activation.fixed.hint': 'Always on — managed by the app and can’t be turned off', 'tools.activation.pinned': 'Pinned', 'tools.activation.pinned.desc': 'Always On', 'tools.skillActivateMode.auto.desc': diff --git a/src/server/modules/Mecha/AgentToolsEngine/__tests__/index.test.ts b/src/server/modules/Mecha/AgentToolsEngine/__tests__/index.test.ts index 251cbc5026..5ed195e23f 100644 --- a/src/server/modules/Mecha/AgentToolsEngine/__tests__/index.test.ts +++ b/src/server/modules/Mecha/AgentToolsEngine/__tests__/index.test.ts @@ -269,7 +269,7 @@ describe('createServerAgentToolsEngine', () => { expect(result.enabledToolIds).toContain(LobeAgentManifest.identifier); }); - it('should not enable VisualUnderstanding by default', () => { + it('should enable lobe-agent by default since it is always-on', () => { const context = createMockContext(); const engine = createServerAgentToolsEngine(context, { agentConfig: { plugins: [] }, @@ -283,7 +283,8 @@ describe('createServerAgentToolsEngine', () => { toolIds: [], }); - expect(result.enabledToolIds).not.toContain(LobeAgentManifest.identifier); + // lobe-agent is in alwaysOnToolIds, so its core capabilities are on for every agent-mode turn. + expect(result.enabledToolIds).toContain(LobeAgentManifest.identifier); }); it('should enable KnowledgeBase when hasEnabledKnowledgeBases is true', () => { diff --git a/src/store/tool/slices/builtin/selectors.test.ts b/src/store/tool/slices/builtin/selectors.test.ts index 903a47a5e4..81e5106e91 100644 --- a/src/store/tool/slices/builtin/selectors.test.ts +++ b/src/store/tool/slices/builtin/selectors.test.ts @@ -110,6 +110,74 @@ describe('builtinToolSelectors', () => { }); }); + describe('fixedDisplayMetaList', () => { + const fixedState = { + ...initialState, + builtinTools: [ + { + hidden: true, + identifier: 'lobe-agent', + manifest: { + api: [], + identifier: 'lobe-agent', + meta: { avatar: '🤖', title: 'Lobe Agent' }, + systemRole: '', + }, + type: 'builtin', + }, + { + discoverable: false, + hidden: true, + identifier: 'lobe-activator', + manifest: { api: [], identifier: 'lobe-activator', meta: {}, systemRole: '' }, + type: 'builtin', + }, + { + hidden: true, + identifier: 'lobe-skill-store', + manifest: { api: [], identifier: 'lobe-skill-store', meta: {}, systemRole: '' }, + type: 'builtin', + }, + { + hidden: true, + identifier: 'tool-1', + manifest: { api: [], identifier: 'tool-1', meta: { title: 'Tool 1' }, systemRole: '' }, + type: 'builtin', + }, + ], + uninstalledBuiltinTools: [], + } as ToolStoreState; + + it('should surface app-fixed tools (e.g. lobe-agent) even though they are hidden', () => { + const result = builtinToolSelectors.fixedDisplayMetaList()(fixedState); + + // Only fixed-display ids are returned; lobe-agent leads the list, unrelated tools excluded. + expect(result.map((item) => item.identifier)).toContain('lobe-agent'); + expect(result.map((item) => item.identifier)).not.toContain('tool-1'); + expect(result[0].identifier).toBe('lobe-agent'); + }); + + it('should drop manual-mode-excluded discovery tools in manual mode', () => { + const result = builtinToolSelectors.fixedDisplayMetaList({ isManualMode: true })(fixedState); + const ids = result.map((item) => item.identifier); + + // activator + skill-store are stripped from defaults in manual mode, so they aren't on. + expect(ids).not.toContain('lobe-activator'); + expect(ids).not.toContain('lobe-skill-store'); + // lobe-agent stays on in manual mode. + expect(ids).toContain('lobe-agent'); + }); + + it('should skip fixed ids that are not registered in builtinTools', () => { + const state = { + ...initialState, + builtinTools: [], + } as unknown as ToolStoreState; + + expect(builtinToolSelectors.fixedDisplayMetaList()(state)).toEqual([]); + }); + }); + describe('installedAllMetaList', () => { it('should include all non-uninstalled tools in agent profile configuration', () => { const state = { diff --git a/src/store/tool/slices/builtin/selectors.ts b/src/store/tool/slices/builtin/selectors.ts index 45b8152606..ef456098b4 100644 --- a/src/store/tool/slices/builtin/selectors.ts +++ b/src/store/tool/slices/builtin/selectors.ts @@ -1,4 +1,8 @@ -import { runtimeManagedToolIds } from '@lobechat/builtin-tools'; +import { + alwaysOnToolIds, + manualModeExcludeToolIds, + runtimeManagedToolIds, +} from '@lobechat/builtin-tools'; import { type BuiltinSkill, type LobeToolMeta } from '@lobechat/types'; import { @@ -228,6 +232,32 @@ const installedAllMetaList = (s: ToolStoreState): LobeToolMetaWithAvailability[] return [...builtinMetas, ...getKlavisMetasWithAvailability(s)]; }; +const MANUAL_MODE_EXCLUDE_TOOL_IDS = new Set(manualModeExcludeToolIds); + +/** + * Get meta for the application-fixed tools (always-on, not user-controllable) that should + * be shown read-only in the chat-input Tools popover's "Pinned" section. + * + * These tools are normally `hidden` (and some are `discoverable: false`), so they never + * appear in `metaList` / `metaListIncludingHidden`. Here we read them directly from + * `builtinTools` by identifier, preserving the `alwaysOnToolIds` order and dropping any + * that aren't available in the current environment. + * + * The list must match what the engine actually enables: in manual skill-activate mode the + * discovery tools in `manualModeExcludeToolIds` (activator, skill-store) are stripped from + * the defaults before the enable checker runs, so they are NOT on — exclude them here too, + * otherwise the UI would claim a fixed tool that the runtime omits. + */ +const fixedDisplayMetaList = + ({ isManualMode }: { isManualMode: boolean } = { isManualMode: false }) => + (s: ToolStoreState): LobeToolMeta[] => + alwaysOnToolIds + .filter((id) => !(isManualMode && MANUAL_MODE_EXCLUDE_TOOL_IDS.has(id))) + .map((id) => s.builtinTools.find((tool) => tool.identifier === id)) + .filter((tool): tool is ToolStoreState['builtinTools'][number] => !!tool) + .filter((tool) => isBuiltinToolAvailableInCurrentEnv(tool.identifier)) + .map(toBuiltinMeta); + /** * Get installed builtin skills (excludes uninstalled ones) */ @@ -253,6 +283,7 @@ const isBuiltinToolInstalled = (identifier: string) => (s: ToolStoreState) => export const builtinToolSelectors = { allMetaList, discoverableMetaList, + fixedDisplayMetaList, installedAllMetaList, installedBuiltinSkills, isBuiltinToolInstalled,