diff --git a/frontend/src/components/ai/AIBuiltinToolsCatalog.test.tsx b/frontend/src/components/ai/AIBuiltinToolsCatalog.test.tsx index a754347..8ef364d 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_app_health'); expect(markup).toContain('一键体检 AI 配置'); expect(markup).toContain('inspect_ai_setup_health'); expect(markup).toContain('查看 AI 当前能力'); diff --git a/frontend/src/components/ai/AIBuiltinToolsCatalog.tsx b/frontend/src/components/ai/AIBuiltinToolsCatalog.tsx index af30012..033fbf2 100644 --- a/frontend/src/components/ai/AIBuiltinToolsCatalog.tsx +++ b/frontend/src/components/ai/AIBuiltinToolsCatalog.tsx @@ -40,6 +40,11 @@ const BUILTIN_TOOL_FLOWS = [ steps: 'inspect_database_bundle → inspect_table_bundle', description: '适合先看整库有哪些表、每张表大概有哪些字段,再对目标表继续做深挖快照。', }, + { + title: 'AI 应用健康总览', + steps: 'inspect_app_health → inspect_ai_setup_health / inspect_app_logs / inspect_recent_connection_failures', + description: '适合用户反馈 AI 不稳定、连接和 MCP 问题交织、或需要先看整体健康状态时,一次汇总配置、日志、连接失败和工作区现场。', + }, { title: '一键体检 AI 配置', steps: 'inspect_ai_setup_health → inspect_ai_providers / inspect_mcp_setup / inspect_ai_guidance', diff --git a/frontend/src/components/ai/aiAppHealthInsights.test.ts b/frontend/src/components/ai/aiAppHealthInsights.test.ts new file mode 100644 index 0000000..fb1b9e7 --- /dev/null +++ b/frontend/src/components/ai/aiAppHealthInsights.test.ts @@ -0,0 +1,132 @@ +import { describe, expect, it } from 'vitest'; + +import { buildAIAppHealthSnapshot } from './aiAppHealthInsights'; + +describe('buildAIAppHealthSnapshot', () => { + it('marks the app health as degraded when logs and connection failures show runtime problems', () => { + const snapshot = buildAIAppHealthSnapshot({ + 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'], + maxTokens: 32000, + temperature: 0.2, + }], + activeProviderId: 'provider-1', + safetyLevel: 'readonly', + contextLevel: 'schema_only', + builtinToolNames: ['inspect_app_health', 'inspect_ai_setup_health', 'inspect_app_logs'], + mcpServers: [{ + id: 'server-1', + name: 'GoNavi MCP', + transport: 'stdio', + command: 'gonavi-mcp-server', + args: ['stdio'], + env: {}, + enabled: true, + timeoutSeconds: 20, + }], + mcpClientStatuses: [], + mcpTools: [], + userPromptSettings: { + global: '', + database: '', + jvm: '', + jvmDiagnostic: '', + }, + activeContext: { + connectionId: 'conn-1', + dbName: 'crm', + }, + aiContexts: { + 'conn-1:crm': [], + }, + connections: [{ + id: 'conn-1', + name: '主库', + config: { + type: 'mysql', + host: '127.0.0.1', + port: 3306, + user: 'root', + }, + }], + tabs: [{ + id: 'query-1', + title: '订单查询', + type: 'query', + connectionId: 'conn-1', + dbName: 'crm', + query: 'select * from orders', + }], + activeTabId: 'query-1', + appLogReadResult: { + success: true, + data: { + logPath: 'C:/Users/demo/.GoNavi/Logs/gonavi.log', + requestedLineLimit: 120, + lines: [ + '2026/06/10 09:00:00.000000 [INFO] started', + '2026/06/10 09:00:01.000000 [ERROR] MCP server boot failed', + ], + }, + }, + connectionFailureReadResult: { + success: true, + data: { + logPath: 'C:/Users/demo/.GoNavi/Logs/gonavi.log', + requestedLineLimit: 120, + lines: [ + '2026/06/10 09:01:00.000000 [ERROR] 建立数据库连接失败:类型=mysql 地址=127.0.0.1:3306 数据库=crm 用户=root;错误链:连接建立后验证失败:127.0.0.1:3306 验证失败: Error 1064 (42000): syntax error', + '2026/06/10 09:01:01.000000 [WARN] 命中数据库连接失败冷却:类型=mysql 地址=127.0.0.1:3306 数据库=crm 缓存Key=abc 剩余=29s 原因=连接建立后验证失败:127.0.0.1:3306 验证失败: Error 1064 (42000): syntax error', + ], + }, + }, + }); + + expect(snapshot.status).toBe('degraded'); + expect(snapshot.summary.appLogErrorCount).toBe(1); + expect(snapshot.summary.recentConnectionFailureCount).toBe(2); + expect(snapshot.summary.activeTabTitle).toBe('订单查询'); + expect(snapshot.warnings).toContain('最近应用日志里有 1 条 ERROR,需要优先查看 inspect_app_logs'); + expect(snapshot.nextActions).toContain('调用 inspect_recent_connection_failures 查看最新连接失败根因,再决定是否检查当前连接或保存连接配置'); + expect(snapshot.appLog.lines).toHaveLength(0); + expect(snapshot.appLog.linesOmitted).toBe(true); + }); + + it('marks missing provider pieces as blocked even when logs are clean', () => { + const snapshot = buildAIAppHealthSnapshot({ + providers: [{ + id: 'provider-1', + type: 'openai', + name: 'OpenAI 主账号', + apiKey: '', + hasSecret: false, + baseUrl: '', + model: '', + models: [], + maxTokens: 32000, + temperature: 0.2, + }], + activeProviderId: 'provider-1', + appLogReadResult: { + success: true, + data: { lines: ['2026/06/10 09:00:00.000000 [INFO] started'] }, + }, + connectionFailureReadResult: { + success: true, + data: { lines: [] }, + }, + }); + + expect(snapshot.status).toBe('blocked'); + expect(snapshot.blockers).toContain('当前活动供应商缺少 API Key / Secret'); + expect(snapshot.blockers).toContain('当前活动供应商缺少接口地址'); + expect(snapshot.summary.chatReady).toBe(false); + }); +}); diff --git a/frontend/src/components/ai/aiAppHealthInsights.ts b/frontend/src/components/ai/aiAppHealthInsights.ts new file mode 100644 index 0000000..2225960 --- /dev/null +++ b/frontend/src/components/ai/aiAppHealthInsights.ts @@ -0,0 +1,297 @@ +import type { + AIContextItem, + AIMCPClientInstallStatus, + AIMCPServerConfig, + AIMCPToolDescriptor, + AIProviderConfig, + AISafetyLevel, + AISkillConfig, + AIUserPromptSettings, + SavedConnection, + TabData, +} from '../../types'; +import { buildAISetupHealthSnapshot } from './aiSetupHealthInsights'; +import { buildAppLogSnapshot } from './aiAppLogInsights'; +import { buildRecentConnectionFailureSnapshot } from './aiConnectionFailureInsights'; +import { buildActiveTabSnapshot, buildWorkspaceTabsSnapshot } from './aiWorkspaceInsights'; + +type AIAppHealthStatus = 'ready' | 'needs_attention' | 'degraded' | 'blocked'; + +const DEFAULT_APP_HEALTH_LOG_LIMIT = 120; +const MAX_APP_HEALTH_LOG_LIMIT = 240; + +const appendUnique = (items: string[], value: string) => { + const trimmed = String(value || '').trim(); + if (!trimmed || items.includes(trimmed)) { + return; + } + items.push(trimmed); +}; + +const normalizeAppHealthLogLimit = (value: unknown): number => { + const normalized = Math.floor(Number(value) || DEFAULT_APP_HEALTH_LOG_LIMIT); + if (normalized < 1) return 1; + if (normalized > MAX_APP_HEALTH_LOG_LIMIT) return MAX_APP_HEALTH_LOG_LIMIT; + return normalized; +}; + +const resolveActiveContextKey = (activeContext?: { connectionId?: string | null; dbName?: string | null } | null): string => + activeContext?.connectionId ? `${activeContext.connectionId}:${activeContext.dbName || ''}` : 'default'; + +const buildUnreadLogSnapshot = (message: string, lineLimit: number) => ({ + readable: false, + logPath: '', + requestedLineLimit: lineLimit, + returnedLineCount: 0, + fileWindowTruncated: false, + matchedLinesTruncated: false, + levelBreakdown: { + INFO: 0, + WARN: 0, + ERROR: 0, + OTHER: 0, + }, + hasWarnings: false, + hasErrors: false, + lines: [] as string[], + linesOmitted: false, + message, +}); + +const summarizeAppLogSnapshot = ( + readResult: any, + options: { + keyword?: unknown; + lineLimit: number; + includeLogLines?: boolean; + }, +) => { + if (!readResult?.success) { + return buildUnreadLogSnapshot( + `GoNavi 应用日志暂不可读: ${readResult?.message || '当前环境未提供日志读取能力'}`, + options.lineLimit, + ); + } + + const snapshot = buildAppLogSnapshot({ + readResult, + keyword: options.keyword, + lineLimit: options.lineLimit, + }); + return { + readable: true, + ...snapshot, + lines: options.includeLogLines ? snapshot.lines : [], + linesOmitted: !options.includeLogLines && snapshot.lines.length > 0, + }; +}; + +const summarizeConnectionFailures = ( + readResult: any, + options: { + keyword?: unknown; + lineLimit: number; + }, +) => { + if (!readResult?.success) { + return { + readable: false, + logPath: '', + keyword: String(options.keyword || '').trim(), + requestedLineLimit: options.lineLimit, + returnedLineCount: 0, + fileWindowTruncated: false, + matchedLinesTruncated: false, + failureEventCount: 0, + hasRecentFailures: false, + primaryCategory: '', + primaryCategoryLabel: '', + cooldownHitCount: 0, + validationFailureCount: 0, + sshFailureCount: 0, + categorySummary: [], + addresses: [], + latestFailureAt: '', + latestFailure: null, + recentFailures: [], + nextActions: [] as string[], + message: `连接失败日志暂不可读: ${readResult?.message || '当前环境未提供日志读取能力'}`, + }; + } + + return { + readable: true, + ...buildRecentConnectionFailureSnapshot({ + readResult, + keyword: options.keyword, + lineLimit: options.lineLimit, + }), + }; +}; + +export const buildAIAppHealthSnapshot = (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; + aiContexts?: Record; + connections?: SavedConnection[]; + tabs?: TabData[]; + activeTabId?: string | null; + appLogReadResult?: any; + connectionFailureReadResult?: any; + keyword?: unknown; + connectionKeyword?: unknown; + lineLimit?: unknown; + includeLogLines?: boolean; +}) => { + const connections = Array.isArray(params.connections) ? params.connections : []; + const tabs = Array.isArray(params.tabs) ? params.tabs : []; + const lineLimit = normalizeAppHealthLogLimit(params.lineLimit); + const activeContextKey = resolveActiveContextKey(params.activeContext); + const activeContextItems = params.aiContexts?.[activeContextKey] || []; + const setupHealth = buildAISetupHealthSnapshot({ + providers: params.providers, + activeProviderId: params.activeProviderId, + safetyLevel: params.safetyLevel, + contextLevel: params.contextLevel, + skills: params.skills, + mcpServers: params.mcpServers, + mcpClientStatuses: params.mcpClientStatuses, + mcpTools: params.mcpTools, + dynamicModels: params.dynamicModels, + builtinToolNames: params.builtinToolNames, + userPromptSettings: params.userPromptSettings, + activeContext: params.activeContext, + activeContextItems, + }); + const appLog = summarizeAppLogSnapshot(params.appLogReadResult, { + keyword: params.keyword, + lineLimit, + includeLogLines: params.includeLogLines === true, + }); + const connectionFailures = summarizeConnectionFailures(params.connectionFailureReadResult, { + keyword: params.connectionKeyword ?? params.keyword, + lineLimit, + }); + const workspace = buildWorkspaceTabsSnapshot({ + tabs, + activeTabId: params.activeTabId, + connections, + includeContent: false, + limit: 8, + }); + const activeTab = buildActiveTabSnapshot({ + tabs, + activeTabId: params.activeTabId, + connections, + includeContent: false, + }); + const activeTabTitle = activeTab.hasActiveTab && 'title' in activeTab ? activeTab.title : ''; + const activeTabType = activeTab.hasActiveTab && 'type' in activeTab ? activeTab.type : ''; + + const blockers = [...setupHealth.blockers]; + const warnings = [...setupHealth.warnings]; + const nextActions = [...setupHealth.nextActions]; + + if (!appLog.readable) { + appendUnique(warnings, '当前无法读取 GoNavi 应用日志,启动异常和 MCP/连接错误缺少日志证据'); + appendUnique(nextActions, '确认当前运行环境支持读取 gonavi.log 后,再调用 inspect_app_logs 下钻日志细节'); + } else { + const errorCount = Number(appLog.levelBreakdown.ERROR) || 0; + const warnCount = Number(appLog.levelBreakdown.WARN) || 0; + if (errorCount > 0) { + appendUnique(warnings, `最近应用日志里有 ${errorCount} 条 ERROR,需要优先查看 inspect_app_logs`); + appendUnique(nextActions, '调用 inspect_app_logs 查看最近 ERROR/WARN 原文,确认是否影响 AI、MCP 或数据库连接'); + } else if (warnCount > 0) { + appendUnique(warnings, `最近应用日志里有 ${warnCount} 条 WARN,建议确认是否为已知可忽略警告`); + appendUnique(nextActions, '如用户反馈不稳定,先调用 inspect_app_logs 查看 WARN 是否集中在 AI/MCP/连接链路'); + } + } + + if (!connectionFailures.readable) { + appendUnique(warnings, '当前无法读取连接失败日志,数据库连接冷却和验证失败缺少结构化证据'); + } else if (connectionFailures.failureEventCount > 0) { + appendUnique(warnings, `最近识别到 ${connectionFailures.failureEventCount} 条连接失败/冷却记录`); + appendUnique(nextActions, '调用 inspect_recent_connection_failures 查看最新连接失败根因,再决定是否检查当前连接或保存连接配置'); + connectionFailures.nextActions.forEach((action: string) => appendUnique(nextActions, action)); + } + + if (workspace.totalTabs === 0) { + appendUnique(warnings, '当前工作区没有打开任何页签,AI 缺少可直接读取的活动编辑器上下文'); + appendUnique(nextActions, '如果要分析当前 SQL,先打开或选中目标 SQL 页签,再调用 inspect_active_tab'); + } + + const status: AIAppHealthStatus = blockers.length > 0 + ? 'blocked' + : connectionFailures.failureEventCount > 0 || Number(appLog.levelBreakdown.ERROR) > 0 + ? 'degraded' + : warnings.length > 0 + ? 'needs_attention' + : 'ready'; + + const message = status === 'ready' + ? '当前 AI 应用健康总览通过,AI 配置、日志、连接失败和工作区上下文都没有明显异常' + : status === 'blocked' + ? `当前 AI 应用健康存在 ${blockers.length} 个阻塞项,优先修复供应商和发送前置条件` + : status === 'degraded' + ? '当前 AI 应用健康存在运行期异常信号,建议先下钻日志或连接失败记录' + : `当前 AI 应用健康整体可用,但还有 ${warnings.length} 个建议项`; + + return { + status, + ready: status === 'ready', + message, + blockers, + warnings, + nextActions, + summary: { + aiSetupStatus: setupHealth.status, + chatReady: setupHealth.summary.chatReady, + hasActiveProvider: setupHealth.summary.hasActiveProvider, + activeProviderName: setupHealth.summary.activeProviderName, + safetyLevel: setupHealth.summary.safetyLevel, + contextLevel: setupHealth.summary.contextLevel, + providerCount: setupHealth.summary.providerCount, + enabledSkillCount: setupHealth.summary.enabledSkillCount, + customPromptCount: setupHealth.summary.customPromptCount, + mcpServerCount: setupHealth.summary.mcpServerCount, + enabledMCPServerCount: setupHealth.summary.enabledMCPServerCount, + discoveredMCPToolCount: setupHealth.summary.discoveredMCPToolCount, + totalAvailableToolCount: setupHealth.summary.totalAvailableToolCount, + connectionCount: connections.length, + activeContextConnectionId: params.activeContext?.connectionId || '', + activeContextDbName: params.activeContext?.dbName || '', + workspaceTabCount: workspace.totalTabs, + activeTabId: params.activeTabId || '', + activeTabTitle, + activeTabType, + appLogReadable: appLog.readable, + appLogErrorCount: Number(appLog.levelBreakdown.ERROR) || 0, + appLogWarnCount: Number(appLog.levelBreakdown.WARN) || 0, + recentConnectionFailureCount: connectionFailures.failureEventCount, + primaryConnectionFailureLabel: connectionFailures.primaryCategoryLabel, + }, + aiSetup: { + status: setupHealth.status, + ready: setupHealth.ready, + message: setupHealth.message, + blockers: setupHealth.blockers, + warnings: setupHealth.warnings, + nextActions: setupHealth.nextActions, + summary: setupHealth.summary, + }, + appLog, + connectionFailures, + workspace, + activeTab, + }; +}; diff --git a/frontend/src/components/ai/aiLocalToolExecutor.appHealthInspection.test.ts b/frontend/src/components/ai/aiLocalToolExecutor.appHealthInspection.test.ts new file mode 100644 index 0000000..c448ab0 --- /dev/null +++ b/frontend/src/components/ai/aiLocalToolExecutor.appHealthInspection.test.ts @@ -0,0 +1,106 @@ +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_app_health', () => { + it('returns an app-level health snapshot across ai setup, logs, connection failures, and workspace tabs', async () => { + const readAppLogTail = vi.fn() + .mockResolvedValueOnce({ + success: true, + data: { + logPath: 'C:/Users/demo/.GoNavi/Logs/gonavi.log', + requestedLineLimit: 120, + lines: [ + '2026/06/10 09:00:00.000000 [INFO] started', + '2026/06/10 09:00:01.000000 [ERROR] MCP server boot failed', + ], + }, + }) + .mockResolvedValueOnce({ + success: true, + data: { + logPath: 'C:/Users/demo/.GoNavi/Logs/gonavi.log', + requestedLineLimit: 120, + lines: [ + '2026/06/10 09:01:00.000000 [ERROR] 建立数据库连接失败:类型=mysql 地址=127.0.0.1:3306 数据库=crm 用户=root;错误链:连接建立后验证失败:127.0.0.1:3306 验证失败: Error 1064 (42000): syntax error', + ], + }, + }); + + const result = await executeLocalAIToolCall({ + toolCall: buildToolCall('inspect_app_health', { + lineLimit: 120, + }), + connections: [buildConnection()], + activeContext: { + connectionId: 'conn-1', + dbName: 'crm', + }, + tabs: [{ + id: 'query-1', + title: '订单查询', + type: 'query', + connectionId: 'conn-1', + dbName: 'crm', + query: 'select * from orders', + }], + activeTabId: 'query-1', + mcpTools: [], + 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'], + maxTokens: 32000, + temperature: 0.2, + }], + safetyLevel: 'readonly', + contextLevel: 'schema_only', + }), + getMCPServers: vi.fn().mockResolvedValue([]), + getMCPClientInstallStatuses: vi.fn().mockResolvedValue([]), + readAppLogTail, + }, + }); + + expect(result.success).toBe(true); + expect(result.content).toContain('"status":"degraded"'); + expect(result.content).toContain('"activeProviderName":"OpenAI 主账号"'); + expect(result.content).toContain('"appLogErrorCount":1'); + expect(result.content).toContain('"recentConnectionFailureCount":1'); + expect(result.content).toContain('"activeTabTitle":"订单查询"'); + expect(result.content).toContain('inspect_recent_connection_failures'); + expect(readAppLogTail).toHaveBeenCalledWith(120, ''); + }); +}); diff --git a/frontend/src/components/ai/aiSnapshotInspectionAppHealthToolExecutor.ts b/frontend/src/components/ai/aiSnapshotInspectionAppHealthToolExecutor.ts new file mode 100644 index 0000000..ab7d9d2 --- /dev/null +++ b/frontend/src/components/ai/aiSnapshotInspectionAppHealthToolExecutor.ts @@ -0,0 +1,136 @@ +import type { + AIContextItem, + AIMCPToolDescriptor, + AISkillConfig, + AIUserPromptSettings, + SavedConnection, + TabData, +} from '../../types'; +import { BUILTIN_AI_TOOL_INFO } from '../../utils/aiToolRegistry'; +import { buildAIAppHealthSnapshot } from './aiAppHealthInsights'; +import type { + AISnapshotInspectionRuntime, + AISnapshotInspectionRuntimeState, + SnapshotInspectionResult, +} from './aiSnapshotInspectionToolTypes'; + +const BUILTIN_AI_TOOL_NAMES = BUILTIN_AI_TOOL_INFO.map((item) => item.name); +const DEFAULT_APP_HEALTH_LOG_LIMIT = 120; +const MAX_APP_HEALTH_LOG_LIMIT = 240; + +interface ExecuteAppHealthSnapshotToolCallOptions { + toolName: string; + args: Record; + 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), + ]); + +const readLogTail = async ( + runtime: AISnapshotInspectionRuntime | undefined, + lineLimit: number, + keyword: string, +) => { + if (typeof runtime?.readAppLogTail !== 'function') { + return { success: false, message: '当前环境暂不支持读取 GoNavi 应用日志' }; + } + return runtime.readAppLogTail(lineLimit, keyword); +}; + +const normalizeLineLimit = (value: unknown): number => { + const normalized = Math.floor(Number(value) || DEFAULT_APP_HEALTH_LOG_LIMIT); + if (normalized < 1) return 1; + if (normalized > MAX_APP_HEALTH_LOG_LIMIT) return MAX_APP_HEALTH_LOG_LIMIT; + return normalized; +}; + +export async function executeAppHealthSnapshotToolCall( + options: ExecuteAppHealthSnapshotToolCallOptions, +): Promise { + const { + toolName, + args, + activeContext = null, + aiContexts = {}, + connections, + tabs = [], + activeTabId = null, + mcpTools, + skills = [], + userPromptSettings, + dynamicModels = [], + runtime, + } = options; + + if (toolName !== 'inspect_app_health') { + return null; + } + + try { + const lineLimit = normalizeLineLimit(args.lineLimit); + const keyword = String(args.keyword || '').trim(); + const connectionKeyword = String(args.connectionKeyword ?? args.keyword ?? '').trim(); + const [runtimeState, mcpState, appLogReadResult, connectionFailureReadResult] = await Promise.all([ + loadRuntimeState(runtime), + loadMCPSetupState(runtime), + readLogTail(runtime, lineLimit, keyword), + readLogTail(runtime, lineLimit, connectionKeyword), + ]); + const [mcpServers, mcpClientInstallStatuses] = mcpState; + + return { + content: JSON.stringify(buildAIAppHealthSnapshot({ + 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, + aiContexts, + connections, + tabs, + activeTabId, + appLogReadResult, + connectionFailureReadResult, + keyword, + connectionKeyword, + lineLimit, + includeLogLines: args.includeLogLines === true, + })), + success: true, + }; + } catch (error: any) { + return { + content: `读取 AI 应用健康总览失败: ${error?.message || error}`, + success: false, + }; + } +} diff --git a/frontend/src/components/ai/aiSnapshotInspectionToolExecutor.ts b/frontend/src/components/ai/aiSnapshotInspectionToolExecutor.ts index b7bedd0..66f2fa1 100644 --- a/frontend/src/components/ai/aiSnapshotInspectionToolExecutor.ts +++ b/frontend/src/components/ai/aiSnapshotInspectionToolExecutor.ts @@ -36,6 +36,7 @@ import { buildShortcutSnapshot } from './aiShortcutInsights'; import { buildAILastRenderErrorSnapshot } from './aiLastRenderErrorInsights'; import { buildRecentConnectionFailureSnapshot } from './aiConnectionFailureInsights'; import { executeAIConfigSnapshotToolCall } from './aiSnapshotInspectionAIConfigToolExecutor'; +import { executeAppHealthSnapshotToolCall } from './aiSnapshotInspectionAppHealthToolExecutor'; import type { AISnapshotInspectionRuntime, SnapshotInspectionResult, @@ -90,6 +91,24 @@ export async function executeSnapshotInspectionToolCall( } = options; try { + const appHealthResult = await executeAppHealthSnapshotToolCall({ + toolName, + args, + activeContext, + aiContexts, + connections, + tabs, + activeTabId, + mcpTools, + skills, + userPromptSettings, + dynamicModels, + runtime, + }); + if (appHealthResult) { + return appHealthResult; + } + const aiConfigResult = await executeAIConfigSnapshotToolCall({ toolName, activeContext, @@ -374,6 +393,7 @@ export async function executeSnapshotInspectionToolCall( inspect_saved_queries: '读取已保存查询失败', inspect_sql_snippets: '读取 SQL 片段失败', inspect_shortcuts: '读取快捷键配置失败', + inspect_app_health: '读取 AI 应用健康总览失败', }[toolName] || '读取本地探针快照失败'; return { content: `${label}: ${error?.message || error}`, diff --git a/frontend/src/components/ai/aiSystemContextMessages.test.ts b/frontend/src/components/ai/aiSystemContextMessages.test.ts index e0cc8cb..3c14bd3 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_setup_health', 'inspect_ai_runtime', 'inspect_ai_safety', 'inspect_ai_providers', 'inspect_ai_chat_readiness', 'inspect_mcp_setup', 'inspect_mcp_authoring_guide', '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_sql_risk', 'inspect_recent_connection_failures', 'inspect_app_logs', 'inspect_ai_last_render_error', 'inspect_saved_queries', 'inspect_ai_sessions', 'inspect_sql_snippets', 'inspect_shortcuts', 'get_columns'], + availableToolNames: ['inspect_workspace_tabs', 'inspect_app_health', 'inspect_ai_setup_health', 'inspect_ai_runtime', 'inspect_ai_safety', 'inspect_ai_providers', 'inspect_ai_chat_readiness', 'inspect_mcp_setup', 'inspect_mcp_authoring_guide', '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_sql_risk', 'inspect_recent_connection_failures', 'inspect_app_logs', 'inspect_ai_last_render_error', 'inspect_saved_queries', 'inspect_ai_sessions', 'inspect_sql_snippets', 'inspect_shortcuts', 'get_columns'], skills, userPromptSettings, }); const joined = messages.map((message) => message.content).join('\n'); expect(joined).toContain('inspect_workspace_tabs 盘点当前工作区'); + expect(joined).toContain('inspect_app_health 获取 AI 配置、应用日志、连接失败和工作区页签的全局健康总览'); expect(joined).toContain('inspect_ai_setup_health 先拿到整体现状'); expect(joined).toContain('inspect_ai_runtime 读取当前 AI 运行状态'); expect(joined).toContain('inspect_ai_safety 读取真实安全边界'); diff --git a/frontend/src/components/ai/aiSystemInspectionGuidance.ts b/frontend/src/components/ai/aiSystemInspectionGuidance.ts index ceacb49..353b03b 100644 --- a/frontend/src/components/ai/aiSystemInspectionGuidance.ts +++ b/frontend/src/components/ai/aiSystemInspectionGuidance.ts @@ -55,6 +55,12 @@ export const appendDatabaseInspectionGuidanceMessages = ( '如果用户提到“当前 AI 上下文”“当前关联了哪些表”“现在带了哪些表结构”,优先调用 inspect_ai_context 读取当前挂载的表结构上下文,不要凭记忆复述。', ); appendAIRuntimeInspectionGuidance(messages, availableToolNames); + appendGuidanceIfToolAvailable( + messages, + availableToolNames, + 'inspect_app_health', + '如果用户提到“AI 不稳定”“整体帮我看看”“GoNavi AI 现在还有哪些明显问题”“连接、MCP、日志一起排查”,优先调用 inspect_app_health 获取 AI 配置、应用日志、连接失败和工作区页签的全局健康总览,再决定下钻 inspect_ai_setup_health、inspect_app_logs 或 inspect_recent_connection_failures。', + ); appendGuidanceIfToolAvailable( messages, availableToolNames, diff --git a/frontend/src/utils/aiBuiltinInspectionToolInfo.ts b/frontend/src/utils/aiBuiltinInspectionToolInfo.ts index 063579d..983f41d 100644 --- a/frontend/src/utils/aiBuiltinInspectionToolInfo.ts +++ b/frontend/src/utils/aiBuiltinInspectionToolInfo.ts @@ -1,6 +1,31 @@ import type { AIBuiltinToolInfo } from "./aiBuiltinToolInfo.types"; export const BUILTIN_AI_INSPECTION_TOOL_INFO: AIBuiltinToolInfo[] = [ + { + name: "inspect_app_health", + icon: "🧭", + desc: "一键查看 AI 应用健康总览", + detail: + "汇总 AI 配置、供应商发送前置、MCP 接入、应用日志 ERROR/WARN、最近连接失败/冷却和当前工作区页签,给出阻塞项、运行期异常信号和下一步探针建议。适合用户说“AI 不稳定”“整体帮我看看”“连接和 MCP 一起排查”时先做一次全局摸底。", + params: "keyword?, connectionKeyword?, lineLimit?(默认 120), includeLogLines?(默认 false)", + tool: { + type: "function", + function: { + name: "inspect_app_health", + description: + "读取 GoNavi AI 应用健康总览,汇总 AI 供应商与发送前置、MCP 接入、应用日志 ERROR/WARN、最近连接失败/冷却和当前工作区页签,并返回阻塞项、运行期异常信号与下一步探针建议。适用于用户提到 AI 不稳定、整体不成熟、连接/MCP/日志需要一起排查或要求先看全局状态时,优先调用该工具。", + parameters: { + type: "object", + properties: { + keyword: { type: "string", description: "可选,读取应用日志时按关键词过滤,例如 ai、mcp、mysql、error;不传则读取最近日志窗口" }, + connectionKeyword: { type: "string", description: "可选,分析连接失败日志时按连接类型、地址或错误关键词过滤;不传时复用 keyword" }, + lineLimit: { type: "number", description: "可选,每次最多分析多少行日志,默认 120,最大 240" }, + includeLogLines: { type: "boolean", description: "可选,是否在结果里附带日志原文行,默认 false;需要引用原文时再开启" }, + }, + }, + }, + }, + }, { name: "inspect_ai_setup_health", icon: "🩺",