diff --git a/frontend/src/components/ai/AIBuiltinToolsCatalog.tsx b/frontend/src/components/ai/AIBuiltinToolsCatalog.tsx index 033fbf2..201189e 100644 --- a/frontend/src/components/ai/AIBuiltinToolsCatalog.tsx +++ b/frontend/src/components/ai/AIBuiltinToolsCatalog.tsx @@ -42,8 +42,8 @@ const BUILTIN_TOOL_FLOWS = [ }, { title: 'AI 应用健康总览', - steps: 'inspect_app_health → inspect_ai_setup_health / inspect_app_logs / inspect_recent_connection_failures', - description: '适合用户反馈 AI 不稳定、连接和 MCP 问题交织、或需要先看整体健康状态时,一次汇总配置、日志、连接失败和工作区现场。', + steps: 'inspect_app_health → inspect_ai_setup_health / inspect_app_logs / inspect_recent_connection_failures / inspect_ai_last_render_error', + description: '适合用户反馈 AI 不稳定、连接和 MCP 问题交织、回复气泡显示异常,或需要先看整体健康状态时,一次汇总配置、日志、连接失败、渲染异常和工作区现场。', }, { title: '一键体检 AI 配置', diff --git a/frontend/src/components/ai/aiAppHealthInsights.test.ts b/frontend/src/components/ai/aiAppHealthInsights.test.ts index fb1b9e7..4dc4ca3 100644 --- a/frontend/src/components/ai/aiAppHealthInsights.test.ts +++ b/frontend/src/components/ai/aiAppHealthInsights.test.ts @@ -129,4 +129,116 @@ describe('buildAIAppHealthSnapshot', () => { expect(snapshot.blockers).toContain('当前活动供应商缺少接口地址'); expect(snapshot.summary.chatReady).toBe(false); }); + + it('marks the app health as degraded when the last ai message render error is present', () => { + 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_last_render_error'], + 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: '打开页面', + }], + userPromptSettings: { + global: '回答前先核对上下文。', + database: '', + jvm: '', + jvmDiagnostic: '', + }, + activeContext: { + connectionId: 'conn-1', + dbName: 'crm', + }, + aiContexts: { + 'conn-1:crm': [{ + dbName: 'crm', + tableName: 'orders', + ddl: 'CREATE TABLE orders (...)', + }], + }, + 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: { lines: ['2026/06/10 09:00:00.000000 [INFO] started'] }, + }, + connectionFailureReadResult: { + success: true, + data: { lines: [] }, + }, + lastRenderErrorSnapshot: { + hasError: true, + summary: '已记录到最近一次 AI 消息渲染异常', + messageId: 'msg-1', + role: 'assistant', + recordedAt: 1780700000000, + contentPreview: '回复预览', + errorMessage: 'Cannot read properties of undefined', + nextActions: ['先按 messageId 和 contentPreview 对照当前会话。'], + }, + }); + + expect(snapshot.status).toBe('degraded'); + expect(snapshot.summary.hasLastAIMessageRenderError).toBe(true); + expect(snapshot.summary.lastAIMessageRenderErrorId).toBe('msg-1'); + expect(snapshot.warnings).toContain('最近记录到 AI 消息渲染异常,可能影响回复气泡展示或 Markdown 渲染'); + expect(snapshot.nextActions).toContain('调用 inspect_ai_last_render_error 查看最近一次气泡渲染异常的 messageId、内容预览和组件栈'); + expect(snapshot.lastRenderError.errorMessage).toBe('Cannot read properties of undefined'); + }); }); diff --git a/frontend/src/components/ai/aiAppHealthInsights.ts b/frontend/src/components/ai/aiAppHealthInsights.ts index 2225960..6cbfc1f 100644 --- a/frontend/src/components/ai/aiAppHealthInsights.ts +++ b/frontend/src/components/ai/aiAppHealthInsights.ts @@ -17,6 +17,19 @@ import { buildActiveTabSnapshot, buildWorkspaceTabsSnapshot } from './aiWorkspac type AIAppHealthStatus = 'ready' | 'needs_attention' | 'degraded' | 'blocked'; +interface AILastRenderErrorHealthSnapshot { + hasError: boolean; + summary: string; + messageId?: string; + role?: string; + recordedAt?: number | null; + contentPreview?: string; + errorMessage?: string; + stackPreview?: string; + componentStackPreview?: string; + nextActions?: string[]; +} + const DEFAULT_APP_HEALTH_LOG_LIMIT = 120; const MAX_APP_HEALTH_LOG_LIMIT = 240; @@ -58,6 +71,12 @@ const buildUnreadLogSnapshot = (message: string, lineLimit: number) => ({ message, }); +const buildEmptyLastRenderErrorSnapshot = (): AILastRenderErrorHealthSnapshot => ({ + hasError: false, + summary: '当前还没有记录到 AI 消息渲染异常。', + nextActions: [], +}); + const summarizeAppLogSnapshot = ( readResult: any, options: { @@ -148,6 +167,7 @@ export const buildAIAppHealthSnapshot = (params: { activeTabId?: string | null; appLogReadResult?: any; connectionFailureReadResult?: any; + lastRenderErrorSnapshot?: AILastRenderErrorHealthSnapshot; keyword?: unknown; connectionKeyword?: unknown; lineLimit?: unknown; @@ -178,6 +198,7 @@ export const buildAIAppHealthSnapshot = (params: { lineLimit, includeLogLines: params.includeLogLines === true, }); + const lastRenderError = params.lastRenderErrorSnapshot || buildEmptyLastRenderErrorSnapshot(); const connectionFailures = summarizeConnectionFailures(params.connectionFailureReadResult, { keyword: params.connectionKeyword ?? params.keyword, lineLimit, @@ -230,9 +251,15 @@ export const buildAIAppHealthSnapshot = (params: { appendUnique(nextActions, '如果要分析当前 SQL,先打开或选中目标 SQL 页签,再调用 inspect_active_tab'); } + if (lastRenderError.hasError) { + appendUnique(warnings, '最近记录到 AI 消息渲染异常,可能影响回复气泡展示或 Markdown 渲染'); + appendUnique(nextActions, '调用 inspect_ai_last_render_error 查看最近一次气泡渲染异常的 messageId、内容预览和组件栈'); + (lastRenderError.nextActions || []).forEach((action) => appendUnique(nextActions, action)); + } + const status: AIAppHealthStatus = blockers.length > 0 ? 'blocked' - : connectionFailures.failureEventCount > 0 || Number(appLog.levelBreakdown.ERROR) > 0 + : connectionFailures.failureEventCount > 0 || Number(appLog.levelBreakdown.ERROR) > 0 || lastRenderError.hasError ? 'degraded' : warnings.length > 0 ? 'needs_attention' @@ -279,6 +306,8 @@ export const buildAIAppHealthSnapshot = (params: { appLogWarnCount: Number(appLog.levelBreakdown.WARN) || 0, recentConnectionFailureCount: connectionFailures.failureEventCount, primaryConnectionFailureLabel: connectionFailures.primaryCategoryLabel, + hasLastAIMessageRenderError: lastRenderError.hasError, + lastAIMessageRenderErrorId: lastRenderError.messageId || '', }, aiSetup: { status: setupHealth.status, @@ -290,6 +319,7 @@ export const buildAIAppHealthSnapshot = (params: { summary: setupHealth.summary, }, appLog, + lastRenderError, connectionFailures, workspace, activeTab, diff --git a/frontend/src/components/ai/aiLocalToolExecutor.appHealthInspection.test.ts b/frontend/src/components/ai/aiLocalToolExecutor.appHealthInspection.test.ts index c448ab0..e736014 100644 --- a/frontend/src/components/ai/aiLocalToolExecutor.appHealthInspection.test.ts +++ b/frontend/src/components/ai/aiLocalToolExecutor.appHealthInspection.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it, vi } from 'vitest'; +import { afterEach, describe, expect, it, vi } from 'vitest'; import type { AIToolCall, SavedConnection } from '../../types'; import { executeLocalAIToolCall } from './aiLocalToolExecutor'; @@ -24,7 +24,21 @@ const buildToolCall = (name: string, args: Record): AIToolCall }); describe('aiLocalToolExecutor inspect_app_health', () => { + afterEach(() => { + delete (globalThis as Record).__gonaviLastAIMessageRenderError; + }); + it('returns an app-level health snapshot across ai setup, logs, connection failures, and workspace tabs', async () => { + (globalThis as Record).__gonaviLastAIMessageRenderError = { + messageId: 'msg-render-1', + role: 'assistant', + contentPreview: '这是一条触发渲染异常的 AI 回复预览', + message: 'Cannot read properties of undefined', + stack: 'TypeError: Cannot read properties of undefined\n at AIMessageBubble.tsx:12:3', + componentStack: '\n at AIMessageBubble\n at AIChatPanelConversationView', + recordedAt: 1780700000000, + }; + const readAppLogTail = vi.fn() .mockResolvedValueOnce({ success: true, @@ -100,6 +114,10 @@ describe('aiLocalToolExecutor inspect_app_health', () => { expect(result.content).toContain('"appLogErrorCount":1'); expect(result.content).toContain('"recentConnectionFailureCount":1'); expect(result.content).toContain('"activeTabTitle":"订单查询"'); + expect(result.content).toContain('"hasLastAIMessageRenderError":true'); + expect(result.content).toContain('"lastAIMessageRenderErrorId":"msg-render-1"'); + expect(result.content).toContain('"messageId":"msg-render-1"'); + expect(result.content).toContain('inspect_ai_last_render_error'); 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 index ab7d9d2..8dc0d75 100644 --- a/frontend/src/components/ai/aiSnapshotInspectionAppHealthToolExecutor.ts +++ b/frontend/src/components/ai/aiSnapshotInspectionAppHealthToolExecutor.ts @@ -8,6 +8,7 @@ import type { } from '../../types'; import { BUILTIN_AI_TOOL_INFO } from '../../utils/aiToolRegistry'; import { buildAIAppHealthSnapshot } from './aiAppHealthInsights'; +import { buildAILastRenderErrorSnapshot } from './aiLastRenderErrorInsights'; import type { AISnapshotInspectionRuntime, AISnapshotInspectionRuntimeState, @@ -120,6 +121,7 @@ export async function executeAppHealthSnapshotToolCall( activeTabId, appLogReadResult, connectionFailureReadResult, + lastRenderErrorSnapshot: buildAILastRenderErrorSnapshot(), keyword, connectionKeyword, lineLimit, diff --git a/frontend/src/components/ai/aiSystemContextMessages.test.ts b/frontend/src/components/ai/aiSystemContextMessages.test.ts index 3c14bd3..d77e5ec 100644 --- a/frontend/src/components/ai/aiSystemContextMessages.test.ts +++ b/frontend/src/components/ai/aiSystemContextMessages.test.ts @@ -75,7 +75,7 @@ describe('buildAISystemContextMessages', () => { 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_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 353b03b..6352c45 100644 --- a/frontend/src/components/ai/aiSystemInspectionGuidance.ts +++ b/frontend/src/components/ai/aiSystemInspectionGuidance.ts @@ -59,7 +59,7 @@ export const appendDatabaseInspectionGuidanceMessages = ( messages, availableToolNames, 'inspect_app_health', - '如果用户提到“AI 不稳定”“整体帮我看看”“GoNavi AI 现在还有哪些明显问题”“连接、MCP、日志一起排查”,优先调用 inspect_app_health 获取 AI 配置、应用日志、连接失败和工作区页签的全局健康总览,再决定下钻 inspect_ai_setup_health、inspect_app_logs 或 inspect_recent_connection_failures。', + '如果用户提到“AI 不稳定”“整体帮我看看”“GoNavi AI 现在还有哪些明显问题”“连接、MCP、日志一起排查”或“AI 回复气泡显示异常”,优先调用 inspect_app_health 获取 AI 配置、应用日志、连接失败、回复气泡渲染异常和工作区页签的全局健康总览,再决定下钻 inspect_ai_setup_health、inspect_app_logs、inspect_recent_connection_failures 或 inspect_ai_last_render_error。', ); appendGuidanceIfToolAvailable( messages, diff --git a/frontend/src/utils/aiBuiltinInspectionToolInfo.ts b/frontend/src/utils/aiBuiltinInspectionToolInfo.ts index 983f41d..0778b40 100644 --- a/frontend/src/utils/aiBuiltinInspectionToolInfo.ts +++ b/frontend/src/utils/aiBuiltinInspectionToolInfo.ts @@ -6,14 +6,14 @@ export const BUILTIN_AI_INSPECTION_TOOL_INFO: AIBuiltinToolInfo[] = [ icon: "🧭", desc: "一键查看 AI 应用健康总览", detail: - "汇总 AI 配置、供应商发送前置、MCP 接入、应用日志 ERROR/WARN、最近连接失败/冷却和当前工作区页签,给出阻塞项、运行期异常信号和下一步探针建议。适合用户说“AI 不稳定”“整体帮我看看”“连接和 MCP 一起排查”时先做一次全局摸底。", + "汇总 AI 配置、供应商发送前置、MCP 接入、应用日志 ERROR/WARN、最近连接失败/冷却、AI 回复气泡渲染异常和当前工作区页签,给出阻塞项、运行期异常信号和下一步探针建议。适合用户说“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/日志需要一起排查或要求先看全局状态时,优先调用该工具。", + "读取 GoNavi AI 应用健康总览,汇总 AI 供应商与发送前置、MCP 接入、应用日志 ERROR/WARN、最近连接失败/冷却、AI 回复气泡渲染异常和当前工作区页签,并返回阻塞项、运行期异常信号与下一步探针建议。适用于用户提到 AI 不稳定、整体不成熟、连接/MCP/日志/回复气泡异常需要一起排查或要求先看全局状态时,优先调用该工具。", parameters: { type: "object", properties: {