From 25fb3502e1763e9dc8d538a1e66c38177e618b80 Mon Sep 17 00:00:00 2001 From: Syngnat Date: Tue, 9 Jun 2026 04:56:30 +0800 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20feat(ai-tools):=20=E6=96=B0?= =?UTF-8?q?=E5=A2=9EAI=E9=85=8D=E7=BD=AE=E4=BD=93=E6=A3=80=E6=8E=A2?= =?UTF-8?q?=E9=92=88=E5=B9=B6=E6=8B=86=E5=88=86=E6=9C=AC=E5=9C=B0=E5=BF=AB?= =?UTF-8?q?=E7=85=A7=E6=89=A7=E8=A1=8C=E5=99=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 inspect_ai_setup_health 统一诊断供应商、聊天前置、MCP 接入和提示词技能状态 - 拆分 AI 配置类本地快照执行器与共享运行时类型,收缩 aiSnapshotInspectionToolExecutor 体积 - 补充内置工具目录、系统提示链路、定向测试与构建验证 --- .../ai/AIBuiltinToolsCatalog.test.tsx | 2 + .../components/ai/AIBuiltinToolsCatalog.tsx | 5 + .../aiLocalToolExecutor.aiSetupHealth.test.ts | 115 +++++++++++ .../src/components/ai/aiLocalToolRuntime.ts | 2 +- .../ai/aiSetupHealthInsights.test.ts | 134 ++++++++++++ .../components/ai/aiSetupHealthInsights.ts | 178 ++++++++++++++++ ...iSnapshotInspectionAIConfigToolExecutor.ts | 191 ++++++++++++++++++ .../ai/aiSnapshotInspectionToolExecutor.ts | 146 ++----------- .../ai/aiSnapshotInspectionToolTypes.ts | 25 +++ .../ai/aiSystemContextMessages.test.ts | 3 +- .../components/ai/aiSystemContextMessages.ts | 14 ++ frontend/src/utils/aiBuiltinToolInfo.ts | 17 ++ frontend/src/utils/aiToolRegistry.test.ts | 8 + 13 files changed, 714 insertions(+), 126 deletions(-) create mode 100644 frontend/src/components/ai/aiLocalToolExecutor.aiSetupHealth.test.ts create mode 100644 frontend/src/components/ai/aiSetupHealthInsights.test.ts create mode 100644 frontend/src/components/ai/aiSetupHealthInsights.ts create mode 100644 frontend/src/components/ai/aiSnapshotInspectionAIConfigToolExecutor.ts create mode 100644 frontend/src/components/ai/aiSnapshotInspectionToolTypes.ts diff --git a/frontend/src/components/ai/AIBuiltinToolsCatalog.test.tsx b/frontend/src/components/ai/AIBuiltinToolsCatalog.test.tsx index d830b92..5a35f88 100644 --- a/frontend/src/components/ai/AIBuiltinToolsCatalog.test.tsx +++ b/frontend/src/components/ai/AIBuiltinToolsCatalog.test.tsx @@ -26,6 +26,8 @@ describe('AIBuiltinToolsCatalog', () => { expect(markup).toContain('inspect_table_bundle'); expect(markup).toContain('全库快速摸底'); expect(markup).toContain('inspect_database_bundle'); + expect(markup).toContain('一键体检 AI 配置'); + expect(markup).toContain('inspect_ai_setup_health'); expect(markup).toContain('查看 AI 当前能力'); expect(markup).toContain('inspect_ai_runtime'); expect(markup).toContain('核对写入安全边界'); diff --git a/frontend/src/components/ai/AIBuiltinToolsCatalog.tsx b/frontend/src/components/ai/AIBuiltinToolsCatalog.tsx index 1644ef5..17b9498 100644 --- a/frontend/src/components/ai/AIBuiltinToolsCatalog.tsx +++ b/frontend/src/components/ai/AIBuiltinToolsCatalog.tsx @@ -37,6 +37,11 @@ const BUILTIN_TOOL_FLOWS = [ steps: 'inspect_database_bundle → inspect_table_bundle', description: '适合先看整库有哪些表、每张表大概有哪些字段,再对目标表继续做深挖快照。', }, + { + title: '一键体检 AI 配置', + steps: 'inspect_ai_setup_health → inspect_ai_providers / inspect_mcp_setup / inspect_ai_guidance', + description: '适合先拿到一份 AI 配置健康快照,看清当前是供应商没配好、聊天发送前置没满足、MCP 没接入,还是提示词 / Skills / 上下文还不完整,再决定往哪条探针继续下钻。', + }, { title: '查看 AI 当前能力', steps: 'inspect_ai_runtime → inspect_ai_context / inspect_current_connection', diff --git a/frontend/src/components/ai/aiLocalToolExecutor.aiSetupHealth.test.ts b/frontend/src/components/ai/aiLocalToolExecutor.aiSetupHealth.test.ts new file mode 100644 index 0000000..aa9fdcd --- /dev/null +++ b/frontend/src/components/ai/aiLocalToolExecutor.aiSetupHealth.test.ts @@ -0,0 +1,115 @@ +import { describe, expect, it, vi } from 'vitest'; + +import type { AIToolCall, SavedConnection } from '../../types'; +import { executeLocalAIToolCall } from './aiLocalToolExecutor'; + +const buildConnection = (): SavedConnection => ({ + id: 'conn-1', + name: '主库', + config: { + type: 'mysql', + host: '127.0.0.1', + port: 3306, + user: 'root', + }, +}); + +const buildToolCall = (name: string, args: Record): AIToolCall => ({ + id: `call-${name}`, + type: 'function', + function: { + name, + arguments: JSON.stringify(args), + }, +}); + +describe('aiLocalToolExecutor inspect_ai_setup_health', () => { + it('returns an actionable ai setup health snapshot for diagnosing provider, mcp, and guidance issues together', async () => { + const result = await executeLocalAIToolCall({ + toolCall: buildToolCall('inspect_ai_setup_health', {}), + connections: [buildConnection()], + activeContext: { + connectionId: 'conn-1', + dbName: 'crm', + }, + aiContexts: { + 'conn-1:crm': [], + }, + mcpTools: [], + skills: [{ + id: 'skill-1', + name: '结构审查', + description: '优先核对字段', + systemPrompt: '先看字段和索引,再给结论。', + enabled: true, + scopes: ['database'], + requiredTools: ['get_columns'], + }], + userPromptSettings: { + global: '回答前先核对上下文。', + database: '', + jvm: '', + jvmDiagnostic: '', + }, + dynamicModels: ['gpt-5.4', 'gpt-4.1'], + toolContextMap: new Map(), + runtime: { + getDatabases: vi.fn(), + getTables: vi.fn(), + getAIRuntimeState: vi.fn().mockResolvedValue({ + activeProviderId: 'provider-1', + providers: [{ + id: 'provider-1', + type: 'openai', + name: 'OpenAI 主账号', + apiKey: '', + hasSecret: true, + baseUrl: 'https://api.openai.com/v1', + model: 'gpt-5.4', + models: ['gpt-5.4', 'gpt-4.1'], + maxTokens: 32000, + temperature: 0.2, + }], + safetyLevel: 'readonly', + contextLevel: 'schema_only', + }), + getMCPServers: vi.fn().mockResolvedValue([ + { + id: 'server-1', + name: 'Browser', + transport: 'stdio', + command: 'uvx', + args: ['mcp-server-browser'], + env: {}, + enabled: true, + timeoutSeconds: 20, + }, + ]), + getMCPClientInstallStatuses: vi.fn().mockResolvedValue([ + { + client: 'codex', + displayName: 'Codex', + installed: false, + matchesCurrent: false, + clientDetected: true, + clientCommand: 'codex', + clientPath: 'C:/Tools/codex.exe', + configPath: 'C:/Users/demo/.codex/config.toml', + command: 'gonavi-mcp-server', + args: ['stdio'], + message: '未检测到 Codex 用户级 GoNavi MCP 配置', + }, + ]), + }, + }); + + expect(result.success).toBe(true); + expect(result.content).toContain('"status":"needs_attention"'); + expect(result.content).toContain('"activeProviderName":"OpenAI 主账号"'); + expect(result.content).toContain('"chatStatus":"ready"'); + expect(result.content).toContain('"enabledMCPServerCount":1'); + expect(result.content).toContain('"currentExternalClientCount":0'); + expect(result.content).toContain('如需让外部 CLI 使用 GoNavi MCP'); + expect(result.content).toContain('当前聊天已就绪,但还没有挂载任何表结构上下文'); + }); +}); diff --git a/frontend/src/components/ai/aiLocalToolRuntime.ts b/frontend/src/components/ai/aiLocalToolRuntime.ts index 75f15df..47dbeb4 100644 --- a/frontend/src/components/ai/aiLocalToolRuntime.ts +++ b/frontend/src/components/ai/aiLocalToolRuntime.ts @@ -1,6 +1,6 @@ import { DBGetAllColumns, DBGetDatabases, DBGetTables, ReadSQLFile } from '../../../wailsjs/go/app/App'; -import type { AISnapshotInspectionRuntime } from './aiSnapshotInspectionToolExecutor'; +import type { AISnapshotInspectionRuntime } from './aiSnapshotInspectionToolTypes'; export interface AIToolContextEntry { connectionId: string; diff --git a/frontend/src/components/ai/aiSetupHealthInsights.test.ts b/frontend/src/components/ai/aiSetupHealthInsights.test.ts new file mode 100644 index 0000000..a6071b9 --- /dev/null +++ b/frontend/src/components/ai/aiSetupHealthInsights.test.ts @@ -0,0 +1,134 @@ +import { describe, expect, it } from 'vitest'; + +import { buildAISetupHealthSnapshot } from './aiSetupHealthInsights'; + +describe('buildAISetupHealthSnapshot', () => { + it('marks the setup as blocked when the active provider is missing critical pieces', () => { + const snapshot = buildAISetupHealthSnapshot({ + providers: [{ + id: 'provider-1', + type: 'openai', + name: 'OpenAI 主账号', + apiKey: '', + hasSecret: false, + baseUrl: '', + model: '', + models: [], + maxTokens: 32000, + temperature: 0.2, + }], + activeProviderId: 'provider-1', + builtinToolNames: ['inspect_ai_setup_health', 'inspect_ai_runtime'], + mcpServers: [], + mcpClientStatuses: [], + mcpTools: [], + skills: [], + dynamicModels: [], + userPromptSettings: { + global: '', + database: '', + jvm: '', + jvmDiagnostic: '', + }, + activeContext: { + connectionId: 'conn-1', + dbName: 'crm', + }, + activeContextItems: [], + }); + + expect(snapshot.status).toBe('blocked'); + expect(snapshot.blockers).toContain('当前活动供应商缺少 API Key / Secret'); + expect(snapshot.blockers).toContain('当前活动供应商缺少接口地址'); + expect(snapshot.blockers).toContain('当前活动供应商还没有选中模型'); + expect(snapshot.nextActions).toContain('补齐当前活动供应商的密钥'); + expect(snapshot.nextActions).toContain('为当前活动供应商选择一个可用模型'); + expect(snapshot.summary.chatReady).toBe(false); + }); + + it('summarizes a ready setup while still surfacing optional next-step guidance', () => { + const snapshot = buildAISetupHealthSnapshot({ + providers: [{ + id: 'provider-1', + type: 'openai', + name: 'OpenAI 主账号', + apiKey: '', + hasSecret: true, + baseUrl: 'https://api.openai.com/v1', + model: 'gpt-5.4', + models: ['gpt-5.4', 'gpt-4.1'], + maxTokens: 32000, + temperature: 0.2, + }], + activeProviderId: 'provider-1', + safetyLevel: 'readonly', + contextLevel: 'with_samples', + builtinToolNames: ['inspect_ai_setup_health', 'inspect_ai_runtime', 'get_columns'], + mcpServers: [{ + id: 'server-1', + name: 'Browser', + transport: 'stdio', + command: 'uvx', + args: ['mcp-server-browser'], + env: {}, + enabled: true, + timeoutSeconds: 20, + }], + mcpClientStatuses: [{ + client: 'codex', + displayName: 'Codex', + installed: true, + matchesCurrent: true, + clientDetected: true, + clientCommand: 'codex', + clientPath: 'C:/Tools/codex.exe', + configPath: 'C:/Users/demo/.codex/config.toml', + command: 'gonavi-mcp-server', + args: ['stdio'], + message: '已接入当前 GoNavi MCP', + }], + mcpTools: [{ + alias: 'browser_open', + originalName: 'browser_open', + serverId: 'server-1', + serverName: 'Browser', + title: '打开页面', + description: '打开页面', + }], + skills: [{ + id: 'skill-1', + name: '结构审查', + description: '优先核对字段', + systemPrompt: '先看字段和索引,再给结论。', + enabled: true, + scopes: ['database'], + requiredTools: ['get_columns'], + }], + dynamicModels: ['gpt-5.4', 'gpt-4.1'], + userPromptSettings: { + global: '回答前先核对上下文。', + database: '', + jvm: '', + jvmDiagnostic: '', + }, + activeContext: { + connectionId: 'conn-1', + dbName: 'crm', + }, + activeContextItems: [{ + dbName: 'crm', + tableName: 'orders', + ddl: 'CREATE TABLE orders (...)', + }], + }); + + expect(snapshot.status).toBe('ready'); + expect(snapshot.ready).toBe(true); + expect(snapshot.blockers).toHaveLength(0); + expect(snapshot.summary.activeProviderName).toBe('OpenAI 主账号'); + expect(snapshot.summary.currentExternalClientCount).toBe(1); + expect(snapshot.summary.enabledSkillCount).toBe(1); + expect(snapshot.mcp.message).toContain('当前共配置 1 个 MCP 服务'); + expect(snapshot.guidance.enabledSkillPreview).toContain('结构审查'); + }); +}); diff --git a/frontend/src/components/ai/aiSetupHealthInsights.ts b/frontend/src/components/ai/aiSetupHealthInsights.ts new file mode 100644 index 0000000..cc7f8ea --- /dev/null +++ b/frontend/src/components/ai/aiSetupHealthInsights.ts @@ -0,0 +1,178 @@ +import type { + AIContextItem, + AIMCPClientInstallStatus, + AIMCPServerConfig, + AIMCPToolDescriptor, + AIProviderConfig, + AISafetyLevel, + AISkillConfig, + AIUserPromptSettings, +} from '../../types'; +import { buildAIChatReadinessSnapshot } from './aiChatReadiness'; +import { buildAIGuidanceSnapshot } from './aiPromptInsights'; +import { buildAIProviderSnapshot } from './aiProviderInsights'; +import { buildAIRuntimeSnapshot } from './aiRuntimeInsights'; +import { buildMCPSetupSnapshot } from './aiMCPInsights'; + +type AISetupHealthStatus = 'ready' | 'needs_attention' | 'blocked'; + +const appendUnique = (items: string[], value: string) => { + const trimmed = String(value || '').trim(); + if (!trimmed || items.includes(trimmed)) { + return; + } + items.push(trimmed); +}; + +const summarizeSkillNames = (skills: Array<{ name?: string }>) => + skills + .map((skill) => String(skill?.name || '').trim()) + .filter(Boolean) + .slice(0, 6); + +export const buildAISetupHealthSnapshot = (params: { + providers?: AIProviderConfig[]; + activeProviderId?: string | null; + safetyLevel?: AISafetyLevel | string; + contextLevel?: string; + skills?: AISkillConfig[]; + mcpServers?: AIMCPServerConfig[]; + mcpClientStatuses?: AIMCPClientInstallStatus[]; + mcpTools?: AIMCPToolDescriptor[]; + dynamicModels?: string[]; + builtinToolNames?: string[]; + userPromptSettings?: AIUserPromptSettings; + activeContext?: { connectionId?: string | null; dbName?: string | null } | null; + activeContextItems?: AIContextItem[]; +}) => { + const activeContextItems = Array.isArray(params.activeContextItems) ? params.activeContextItems : []; + const runtimeSnapshot = buildAIRuntimeSnapshot({ + providers: params.providers, + activeProviderId: params.activeProviderId, + safetyLevel: params.safetyLevel, + contextLevel: params.contextLevel, + skills: params.skills, + mcpTools: params.mcpTools, + dynamicModels: params.dynamicModels, + builtinToolNames: params.builtinToolNames, + }); + const providerSnapshot = buildAIProviderSnapshot({ + providers: params.providers, + activeProviderId: params.activeProviderId, + dynamicModels: params.dynamicModels, + }); + const chatReadiness = buildAIChatReadinessSnapshot({ + providers: params.providers, + activeProviderId: params.activeProviderId, + dynamicModels: params.dynamicModels, + activeContext: params.activeContext, + activeContextItems, + }); + const mcpSnapshot = buildMCPSetupSnapshot({ + mcpServers: params.mcpServers, + mcpClientStatuses: params.mcpClientStatuses, + mcpTools: params.mcpTools, + }); + const guidanceSnapshot = buildAIGuidanceSnapshot({ + userPromptSettings: params.userPromptSettings, + skills: params.skills, + }); + + const blockers: string[] = []; + const warnings: string[] = []; + const nextActions: string[] = []; + + if (!providerSnapshot.hasActiveProvider) { + appendUnique(blockers, '当前没有活动 AI 供应商'); + appendUnique(nextActions, '先在 AI 设置里添加并选中一个活动供应商'); + } + + const activeProviderIssues = providerSnapshot.activeProvider?.issues || []; + if (activeProviderIssues.includes('missing_secret')) { + appendUnique(blockers, '当前活动供应商缺少 API Key / Secret'); + appendUnique(nextActions, '补齐当前活动供应商的密钥'); + } + if (activeProviderIssues.includes('missing_base_url')) { + appendUnique(blockers, '当前活动供应商缺少接口地址'); + appendUnique(nextActions, '补齐当前活动供应商的 baseUrl'); + } + if (activeProviderIssues.includes('missing_selected_model') || chatReadiness.status === 'missing_model') { + appendUnique(blockers, '当前活动供应商还没有选中模型'); + appendUnique(nextActions, '为当前活动供应商选择一个可用模型'); + } + if (chatReadiness.status === 'loading_models') { + appendUnique(warnings, '当前正在加载模型列表,模型选择尚未完成'); + appendUnique(nextActions, '等待模型列表加载完成后重新确认活动模型'); + } + + if (mcpSnapshot.serverCount === 0) { + appendUnique(warnings, '当前还没有配置任何 MCP 服务'); + appendUnique(nextActions, '如需扩展 AI 工具能力,可新增并测试至少 1 个 MCP 服务'); + } + if (mcpSnapshot.currentClientCount === 0) { + appendUnique(warnings, 'Claude Code / Codex 还没有任何客户端接入当前 GoNavi MCP'); + appendUnique(nextActions, '如需让外部 CLI 使用 GoNavi MCP,先把当前 GoNavi 接入 Claude Code 或 Codex'); + } + if (mcpSnapshot.enabledServerCount > 0 && runtimeSnapshot.mcpToolCount === 0) { + appendUnique(warnings, '已启用 MCP 服务,但当前还没有发现可用 MCP 工具'); + appendUnique(nextActions, '逐条测试已启用的 MCP 服务,确认命令、参数和环境变量能正确发现工具'); + } + if (guidanceSnapshot.customPromptCount === 0 && guidanceSnapshot.enabledSkillCount === 0) { + appendUnique(warnings, '当前没有自定义提示词或 Skills'); + appendUnique(nextActions, '如需固定回答风格或工作流,可补充自定义提示词或启用 Skills'); + } + if (chatReadiness.ready && params.activeContext?.connectionId && activeContextItems.length === 0) { + appendUnique(warnings, '当前聊天已就绪,但还没有挂载任何表结构上下文'); + appendUnique(nextActions, '如需更准的 SQL / 结构建议,可先把目标表结构关联到 AI 上下文'); + } + + const status: AISetupHealthStatus = blockers.length > 0 + ? 'blocked' + : warnings.length > 0 + ? 'needs_attention' + : 'ready'; + + const message = status === 'ready' + ? '当前 AI 配置体检通过,供应商、聊天前置和 MCP 运行链路都处于可用状态' + : status === 'blocked' + ? `当前 AI 配置存在 ${blockers.length} 个阻塞项,优先修复活动供应商和聊天前置条件` + : `当前 AI 配置整体可用,但还有 ${warnings.length} 个建议项可以继续优化`; + + return { + status, + ready: status === 'ready', + message, + blockers, + warnings, + nextActions, + summary: { + providerCount: providerSnapshot.providerCount, + readyProviderCount: providerSnapshot.readyProviderCount, + hasActiveProvider: providerSnapshot.hasActiveProvider, + activeProviderName: providerSnapshot.activeProvider?.name || providerSnapshot.activeProvider?.id || '', + chatStatus: chatReadiness.status, + chatReady: chatReadiness.ready, + safetyLevel: runtimeSnapshot.safetyLevel, + safetyLabel: runtimeSnapshot.safetyLabel, + contextLevel: runtimeSnapshot.contextLevel, + contextLabel: runtimeSnapshot.contextLabel, + enabledSkillCount: guidanceSnapshot.enabledSkillCount, + customPromptCount: guidanceSnapshot.customPromptCount, + mcpServerCount: mcpSnapshot.serverCount, + enabledMCPServerCount: mcpSnapshot.enabledServerCount, + installedExternalClientCount: mcpSnapshot.installedClientCount, + currentExternalClientCount: mcpSnapshot.currentClientCount, + discoveredMCPToolCount: mcpSnapshot.discoveredMCPToolCount, + totalAvailableToolCount: runtimeSnapshot.totalAvailableToolCount, + contextAttachedCount: chatReadiness.contextAttachedCount, + }, + runtime: runtimeSnapshot, + provider: providerSnapshot, + chatReadiness, + mcp: mcpSnapshot, + guidance: { + ...guidanceSnapshot, + enabledSkillPreview: summarizeSkillNames(guidanceSnapshot.enabledSkills), + }, + }; +}; diff --git a/frontend/src/components/ai/aiSnapshotInspectionAIConfigToolExecutor.ts b/frontend/src/components/ai/aiSnapshotInspectionAIConfigToolExecutor.ts new file mode 100644 index 0000000..8fa3d85 --- /dev/null +++ b/frontend/src/components/ai/aiSnapshotInspectionAIConfigToolExecutor.ts @@ -0,0 +1,191 @@ +import type { + AIContextItem, + AIMCPToolDescriptor, + AISkillConfig, + AIUserPromptSettings, + SavedConnection, + TabData, +} from '../../types'; +import { BUILTIN_AI_TOOL_INFO } from '../../utils/aiToolRegistry'; +import { buildAIChatReadinessSnapshot } from './aiChatReadiness'; +import { buildAIGuidanceSnapshot } from './aiPromptInsights'; +import { buildAIProviderSnapshot } from './aiProviderInsights'; +import { buildAIRuntimeSnapshot } from './aiRuntimeInsights'; +import { buildAISafetySnapshot } from './aiSafetyInsights'; +import { buildAISetupHealthSnapshot } from './aiSetupHealthInsights'; +import { buildMCPSetupSnapshot } from './aiMCPInsights'; +import type { + AISnapshotInspectionRuntime, + AISnapshotInspectionRuntimeState, + SnapshotInspectionResult, +} from './aiSnapshotInspectionToolTypes'; + +const BUILTIN_AI_TOOL_NAMES = BUILTIN_AI_TOOL_INFO.map((item) => item.name); + +interface ExecuteAIConfigSnapshotToolCallOptions { + toolName: string; + activeContext?: { connectionId: string; dbName: string } | null; + aiContexts?: Record; + connections: SavedConnection[]; + tabs?: TabData[]; + activeTabId?: string | null; + mcpTools: AIMCPToolDescriptor[]; + skills?: AISkillConfig[]; + userPromptSettings?: AIUserPromptSettings; + dynamicModels?: string[]; + runtime?: AISnapshotInspectionRuntime; +} + +const loadRuntimeState = async ( + runtime: AISnapshotInspectionRuntime | undefined, +): Promise => + typeof runtime?.getAIRuntimeState === 'function' + ? runtime.getAIRuntimeState() + : undefined; + +const loadMCPSetupState = async (runtime: AISnapshotInspectionRuntime | undefined) => + Promise.all([ + typeof runtime?.getMCPServers === 'function' ? runtime.getMCPServers() : Promise.resolve(undefined), + typeof runtime?.getMCPClientInstallStatuses === 'function' + ? runtime.getMCPClientInstallStatuses() + : Promise.resolve(undefined), + ]); + +export async function executeAIConfigSnapshotToolCall( + options: ExecuteAIConfigSnapshotToolCallOptions, +): Promise { + const { + toolName, + activeContext = null, + aiContexts = {}, + connections, + tabs = [], + activeTabId = null, + mcpTools, + skills = [], + userPromptSettings, + dynamicModels = [], + runtime, + } = options; + + try { + switch (toolName) { + case 'inspect_ai_setup_health': { + const runtimeState = await loadRuntimeState(runtime); + const [mcpServers, mcpClientInstallStatuses] = await loadMCPSetupState(runtime); + const activeContextKey = activeContext?.connectionId + ? `${activeContext.connectionId}:${activeContext.dbName || ''}` + : 'default'; + return { + content: JSON.stringify(buildAISetupHealthSnapshot({ + providers: Array.isArray(runtimeState?.providers) ? runtimeState.providers : [], + activeProviderId: runtimeState?.activeProviderId || '', + safetyLevel: runtimeState?.safetyLevel, + contextLevel: runtimeState?.contextLevel, + skills, + mcpServers: Array.isArray(mcpServers) ? mcpServers : [], + mcpClientStatuses: Array.isArray(mcpClientInstallStatuses) ? mcpClientInstallStatuses : [], + mcpTools, + dynamicModels, + builtinToolNames: BUILTIN_AI_TOOL_NAMES, + userPromptSettings, + activeContext, + activeContextItems: aiContexts[activeContextKey] || [], + })), + success: true, + }; + } + case 'inspect_ai_runtime': { + const runtimeState = await loadRuntimeState(runtime); + return { + content: JSON.stringify(buildAIRuntimeSnapshot({ + providers: Array.isArray(runtimeState?.providers) ? runtimeState.providers : [], + activeProviderId: runtimeState?.activeProviderId || '', + safetyLevel: runtimeState?.safetyLevel, + contextLevel: runtimeState?.contextLevel, + skills, + mcpTools, + dynamicModels, + builtinToolNames: BUILTIN_AI_TOOL_NAMES, + })), + success: true, + }; + } + case 'inspect_ai_safety': { + const runtimeState = await loadRuntimeState(runtime); + return { + content: JSON.stringify(buildAISafetySnapshot({ + safetyLevel: runtimeState?.safetyLevel, + activeContext, + tabs, + activeTabId, + connections, + })), + success: true, + }; + } + case 'inspect_ai_providers': { + const runtimeState = await loadRuntimeState(runtime); + return { + content: JSON.stringify(buildAIProviderSnapshot({ + providers: Array.isArray(runtimeState?.providers) ? runtimeState.providers : [], + activeProviderId: runtimeState?.activeProviderId || '', + dynamicModels, + })), + success: true, + }; + } + case 'inspect_ai_chat_readiness': { + const runtimeState = await loadRuntimeState(runtime); + const activeContextKey = activeContext?.connectionId + ? `${activeContext.connectionId}:${activeContext.dbName || ''}` + : 'default'; + return { + content: JSON.stringify(buildAIChatReadinessSnapshot({ + providers: Array.isArray(runtimeState?.providers) ? runtimeState.providers : [], + activeProviderId: runtimeState?.activeProviderId || '', + dynamicModels, + activeContext, + activeContextItems: aiContexts[activeContextKey] || [], + })), + success: true, + }; + } + case 'inspect_mcp_setup': { + const [mcpServers, mcpClientInstallStatuses] = await loadMCPSetupState(runtime); + return { + content: JSON.stringify(buildMCPSetupSnapshot({ + mcpServers: Array.isArray(mcpServers) ? mcpServers : [], + mcpClientStatuses: Array.isArray(mcpClientInstallStatuses) ? mcpClientInstallStatuses : [], + mcpTools, + })), + success: true, + }; + } + case 'inspect_ai_guidance': + return { + content: JSON.stringify(buildAIGuidanceSnapshot({ + userPromptSettings, + skills, + })), + success: true, + }; + default: + return null; + } + } catch (error: any) { + const label = { + inspect_ai_setup_health: '体检当前 AI 配置失败', + inspect_ai_runtime: '读取当前 AI 运行状态失败', + inspect_ai_safety: '读取当前 AI 安全边界失败', + inspect_ai_providers: '读取当前 AI 供应商配置失败', + inspect_ai_chat_readiness: '读取 AI 聊天发送前置状态失败', + inspect_mcp_setup: '读取 MCP 配置状态失败', + inspect_ai_guidance: '读取当前 AI 提示与技能配置失败', + }[toolName] || '读取 AI 配置探针失败'; + return { + content: `${label}: ${error?.message || error}`, + success: false, + }; + } +} diff --git a/frontend/src/components/ai/aiSnapshotInspectionToolExecutor.ts b/frontend/src/components/ai/aiSnapshotInspectionToolExecutor.ts index 9a54bec..2752214 100644 --- a/frontend/src/components/ai/aiSnapshotInspectionToolExecutor.ts +++ b/frontend/src/components/ai/aiSnapshotInspectionToolExecutor.ts @@ -1,11 +1,7 @@ import type { AIContextItem, - AIMCPClientInstallStatus, - AIMCPServerConfig, AIMCPToolDescriptor, AIChatMessage, - AIProviderConfig, - AISafetyLevel, AISkillConfig, AIUserPromptSettings, ExternalSQLDirectory, @@ -15,17 +11,10 @@ import type { TabData, } from '../../types'; import type { SqlLog } from '../../store'; -import { BUILTIN_AI_TOOL_INFO } from '../../utils/aiToolRegistry'; import { buildAIContextSnapshot } from './aiContextInsights'; import { buildAIChatSessionsSnapshot } from './aiChatSessionInsights'; import { buildConnectionCapabilitiesSnapshot } from './aiConnectionCapabilitiesInsights'; import { buildCurrentConnectionSnapshot } from './aiConnectionInsights'; -import { buildMCPSetupSnapshot } from './aiMCPInsights'; -import { buildAIGuidanceSnapshot } from './aiPromptInsights'; -import { buildAIChatReadinessSnapshot } from './aiChatReadiness'; -import { buildAIProviderSnapshot } from './aiProviderInsights'; -import { buildAIRuntimeSnapshot } from './aiRuntimeInsights'; -import { buildAISafetySnapshot } from './aiSafetyInsights'; import { buildSavedQueriesSnapshot, buildSqlSnippetsSnapshot, @@ -42,20 +31,11 @@ import { buildActiveTabSnapshot, buildWorkspaceTabsSnapshot, } from './aiWorkspaceInsights'; - -export interface AISnapshotInspectionRuntimeState { - providers?: AIProviderConfig[]; - activeProviderId?: string; - safetyLevel?: AISafetyLevel | string; - contextLevel?: string; -} - -export interface AISnapshotInspectionRuntime { - getAIRuntimeState?: () => Promise; - getMCPServers?: () => Promise; - getMCPClientInstallStatuses?: () => Promise; - readSQLFile?: (filePath: string) => Promise; -} +import { executeAIConfigSnapshotToolCall } from './aiSnapshotInspectionAIConfigToolExecutor'; +import type { + AISnapshotInspectionRuntime, + SnapshotInspectionResult, +} from './aiSnapshotInspectionToolTypes'; interface ExecuteSnapshotInspectionToolCallOptions { toolName: string; @@ -79,13 +59,6 @@ interface ExecuteSnapshotInspectionToolCallOptions { runtime?: AISnapshotInspectionRuntime; } -interface SnapshotInspectionResult { - content: string; - success: boolean; -} - -const BUILTIN_AI_TOOL_NAMES = BUILTIN_AI_TOOL_INFO.map((item) => item.name); - export async function executeSnapshotInspectionToolCall( options: ExecuteSnapshotInspectionToolCallOptions, ): Promise { @@ -112,93 +85,24 @@ export async function executeSnapshotInspectionToolCall( } = options; try { + const aiConfigResult = await executeAIConfigSnapshotToolCall({ + toolName, + activeContext, + aiContexts, + connections, + tabs, + activeTabId, + mcpTools, + skills, + userPromptSettings, + dynamicModels, + runtime, + }); + if (aiConfigResult) { + return aiConfigResult; + } + switch (toolName) { - case 'inspect_ai_runtime': { - const runtimeState = typeof runtime?.getAIRuntimeState === 'function' - ? await runtime.getAIRuntimeState() - : undefined; - return { - content: JSON.stringify(buildAIRuntimeSnapshot({ - providers: Array.isArray(runtimeState?.providers) ? runtimeState.providers : [], - activeProviderId: runtimeState?.activeProviderId || '', - safetyLevel: runtimeState?.safetyLevel, - contextLevel: runtimeState?.contextLevel, - skills, - mcpTools, - dynamicModels, - builtinToolNames: BUILTIN_AI_TOOL_NAMES, - })), - success: true, - }; - } - case 'inspect_ai_safety': { - const runtimeState = typeof runtime?.getAIRuntimeState === 'function' - ? await runtime.getAIRuntimeState() - : undefined; - return { - content: JSON.stringify(buildAISafetySnapshot({ - safetyLevel: runtimeState?.safetyLevel, - activeContext, - tabs, - activeTabId, - connections, - })), - success: true, - }; - } - case 'inspect_ai_providers': { - const runtimeState = typeof runtime?.getAIRuntimeState === 'function' - ? await runtime.getAIRuntimeState() - : undefined; - return { - content: JSON.stringify(buildAIProviderSnapshot({ - providers: Array.isArray(runtimeState?.providers) ? runtimeState.providers : [], - activeProviderId: runtimeState?.activeProviderId || '', - dynamicModels, - })), - success: true, - }; - } - case 'inspect_ai_chat_readiness': { - const runtimeState = typeof runtime?.getAIRuntimeState === 'function' - ? await runtime.getAIRuntimeState() - : undefined; - const activeContextKey = activeContext?.connectionId - ? `${activeContext.connectionId}:${activeContext.dbName || ''}` - : 'default'; - return { - content: JSON.stringify(buildAIChatReadinessSnapshot({ - providers: Array.isArray(runtimeState?.providers) ? runtimeState.providers : [], - activeProviderId: runtimeState?.activeProviderId || '', - dynamicModels, - activeContext, - activeContextItems: aiContexts[activeContextKey] || [], - })), - success: true, - }; - } - case 'inspect_mcp_setup': { - const [mcpServers, mcpClientInstallStatuses] = await Promise.all([ - typeof runtime?.getMCPServers === 'function' ? runtime.getMCPServers() : Promise.resolve(undefined), - typeof runtime?.getMCPClientInstallStatuses === 'function' ? runtime.getMCPClientInstallStatuses() : Promise.resolve(undefined), - ]); - return { - content: JSON.stringify(buildMCPSetupSnapshot({ - mcpServers: Array.isArray(mcpServers) ? mcpServers : [], - mcpClientStatuses: Array.isArray(mcpClientInstallStatuses) ? mcpClientInstallStatuses : [], - mcpTools, - })), - success: true, - }; - } - case 'inspect_ai_guidance': - return { - content: JSON.stringify(buildAIGuidanceSnapshot({ - userPromptSettings, - skills, - })), - success: true, - }; case 'inspect_current_connection': return { content: JSON.stringify(buildCurrentConnectionSnapshot({ @@ -371,12 +275,6 @@ export async function executeSnapshotInspectionToolCall( } } catch (error: any) { const label = { - inspect_ai_runtime: '读取当前 AI 运行状态失败', - inspect_ai_safety: '读取当前 AI 安全边界失败', - inspect_ai_providers: '读取当前 AI 供应商配置失败', - inspect_ai_chat_readiness: '读取 AI 聊天发送前置状态失败', - inspect_mcp_setup: '读取 MCP 配置状态失败', - inspect_ai_guidance: '读取当前 AI 提示与技能配置失败', inspect_current_connection: '读取当前连接失败', inspect_connection_capabilities: '读取当前连接能力矩阵失败', inspect_saved_connections: '读取本地连接清单失败', diff --git a/frontend/src/components/ai/aiSnapshotInspectionToolTypes.ts b/frontend/src/components/ai/aiSnapshotInspectionToolTypes.ts new file mode 100644 index 0000000..433fd79 --- /dev/null +++ b/frontend/src/components/ai/aiSnapshotInspectionToolTypes.ts @@ -0,0 +1,25 @@ +import type { + AIMCPClientInstallStatus, + AIMCPServerConfig, + AIProviderConfig, + AISafetyLevel, +} from '../../types'; + +export interface AISnapshotInspectionRuntimeState { + providers?: AIProviderConfig[]; + activeProviderId?: string; + safetyLevel?: AISafetyLevel | string; + contextLevel?: string; +} + +export interface AISnapshotInspectionRuntime { + getAIRuntimeState?: () => Promise; + getMCPServers?: () => Promise; + getMCPClientInstallStatuses?: () => Promise; + readSQLFile?: (filePath: string) => Promise; +} + +export interface SnapshotInspectionResult { + content: string; + success: boolean; +} diff --git a/frontend/src/components/ai/aiSystemContextMessages.test.ts b/frontend/src/components/ai/aiSystemContextMessages.test.ts index a01a619..bfccdf9 100644 --- a/frontend/src/components/ai/aiSystemContextMessages.test.ts +++ b/frontend/src/components/ai/aiSystemContextMessages.test.ts @@ -68,13 +68,14 @@ describe('buildAISystemContextMessages', () => { connections: [connections[0]], tabs: [], activeTabId: null, - availableToolNames: ['inspect_workspace_tabs', 'inspect_ai_runtime', 'inspect_ai_safety', 'inspect_ai_providers', 'inspect_ai_chat_readiness', 'inspect_mcp_setup', 'inspect_ai_guidance', 'inspect_ai_context', 'inspect_current_connection', 'inspect_connection_capabilities', 'inspect_saved_connections', 'inspect_external_sql_directories', 'inspect_external_sql_file', 'inspect_recent_sql_activity', 'inspect_saved_queries', 'inspect_ai_sessions', 'inspect_sql_snippets', 'get_columns'], + availableToolNames: ['inspect_workspace_tabs', 'inspect_ai_setup_health', 'inspect_ai_runtime', 'inspect_ai_safety', 'inspect_ai_providers', 'inspect_ai_chat_readiness', 'inspect_mcp_setup', 'inspect_ai_guidance', 'inspect_ai_context', 'inspect_current_connection', 'inspect_connection_capabilities', 'inspect_saved_connections', 'inspect_external_sql_directories', 'inspect_external_sql_file', 'inspect_recent_sql_activity', 'inspect_saved_queries', 'inspect_ai_sessions', 'inspect_sql_snippets', 'get_columns'], skills, userPromptSettings, }); const joined = messages.map((message) => message.content).join('\n'); expect(joined).toContain('inspect_workspace_tabs 盘点当前工作区'); + expect(joined).toContain('inspect_ai_setup_health 先拿到整体现状'); expect(joined).toContain('inspect_ai_runtime 读取当前 AI 运行状态'); expect(joined).toContain('inspect_ai_safety 读取真实安全边界'); expect(joined).toContain('inspect_ai_providers 读取真实供应商配置'); diff --git a/frontend/src/components/ai/aiSystemContextMessages.ts b/frontend/src/components/ai/aiSystemContextMessages.ts index 78c4f87..2f43e36 100644 --- a/frontend/src/components/ai/aiSystemContextMessages.ts +++ b/frontend/src/components/ai/aiSystemContextMessages.ts @@ -108,6 +108,19 @@ const appendAIRuntimeInspectionGuidance = ( }); }; +const appendAISetupHealthInspectionGuidance = ( + messages: AISystemContextMessage[], + availableToolNames: string[], +) => { + if (!availableToolNames.includes('inspect_ai_setup_health')) { + return; + } + messages.push({ + role: 'system', + content: '如果用户提到“AI 为什么不好用”“帮我体检一下当前 AI 配置”“当前 AI 整体还有哪些明显问题”,优先调用 inspect_ai_setup_health 先拿到整体现状,再按需下钻 inspect_ai_providers、inspect_ai_chat_readiness、inspect_mcp_setup 或 inspect_ai_guidance。', + }); +}; + const appendAISafetyInspectionGuidance = ( messages: AISystemContextMessage[], availableToolNames: string[], @@ -405,6 +418,7 @@ SELECT * FROM users WHERE status = 1; }); } appendAIRuntimeInspectionGuidance(systemMessages, availableToolNames); + appendAISetupHealthInspectionGuidance(systemMessages, availableToolNames); appendAISafetyInspectionGuidance(systemMessages, availableToolNames); appendAIChatReadinessInspectionGuidance(systemMessages, availableToolNames); appendAIProviderInspectionGuidance(systemMessages, availableToolNames); diff --git a/frontend/src/utils/aiBuiltinToolInfo.ts b/frontend/src/utils/aiBuiltinToolInfo.ts index d4b08a5..fe98ecd 100644 --- a/frontend/src/utils/aiBuiltinToolInfo.ts +++ b/frontend/src/utils/aiBuiltinToolInfo.ts @@ -307,6 +307,23 @@ export const BUILTIN_AI_TOOL_INFO: AIBuiltinToolInfo[] = [ }, }, }, + { + name: "inspect_ai_setup_health", + icon: "🩺", + desc: "一键体检当前 AI 配置健康度", + detail: + "汇总当前 AI 供应商、聊天发送前置、MCP 服务与外部客户端接入、提示词与 Skills、上下文挂载情况,并给出阻塞项、告警项和下一步建议。适合用户说“AI 为什么不好用”“帮我看下 AI 整体有没有问题”“现在这套 AI 配置还缺什么”时先做一次总览诊断。", + params: "无参数", + tool: { + type: "function", + function: { + name: "inspect_ai_setup_health", + description: + "体检当前 AI 配置健康度,返回供应商、模型、聊天发送前置、MCP 接入、提示词与 Skills、表结构上下文挂载等整体快照,并给出阻塞项、建议项和下一步动作。适用于用户提到 AI 为什么不好用、当前 AI 配置哪里还缺、是否已经能稳定工作时,优先读取这份总览诊断,不要拆成多次猜测。", + parameters: { type: "object", properties: {} }, + }, + }, + }, { name: "inspect_ai_runtime", icon: "🎛️", diff --git a/frontend/src/utils/aiToolRegistry.test.ts b/frontend/src/utils/aiToolRegistry.test.ts index 195c6d6..4257d90 100644 --- a/frontend/src/utils/aiToolRegistry.test.ts +++ b/frontend/src/utils/aiToolRegistry.test.ts @@ -10,6 +10,13 @@ describe('aiToolRegistry', () => { expect(info?.tool.function.description).toContain('当前供应商'); }); + it('registers the ai-setup-health inspector as a builtin tool', () => { + const info = BUILTIN_AI_TOOL_INFO.find((item) => item.name === 'inspect_ai_setup_health'); + expect(info).toBeTruthy(); + expect(info?.desc).toContain('体检当前 AI 配置'); + expect(info?.tool.function.description).toContain('聊天发送前置'); + }); + it('registers the ai-safety inspector as a builtin tool', () => { const info = BUILTIN_AI_TOOL_INFO.find((item) => item.name === 'inspect_ai_safety'); expect(info).toBeTruthy(); @@ -113,6 +120,7 @@ describe('aiToolRegistry', () => { }]); expect(tools.some((item) => item.function.name === 'inspect_ai_runtime')).toBe(true); + expect(tools.some((item) => item.function.name === 'inspect_ai_setup_health')).toBe(true); expect(tools.some((item) => item.function.name === 'inspect_ai_safety')).toBe(true); expect(tools.some((item) => item.function.name === 'inspect_ai_providers')).toBe(true); expect(tools.some((item) => item.function.name === 'inspect_ai_chat_readiness')).toBe(true);