feat(ai): 将渲染异常纳入应用健康总览

- 在 inspect_app_health 汇总最近一次 AI 回复气泡渲染异常

- 同步内置工具目录和系统探针引导

- 补充应用健康和本地工具执行器测试
This commit is contained in:
Syngnat
2026-06-10 09:24:07 +08:00
parent d0b9a7f50c
commit 0834d8cb3d
8 changed files with 170 additions and 8 deletions

View File

@@ -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 配置',

View File

@@ -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');
});
});

View File

@@ -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,

View File

@@ -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<string, unknown>): AIToolCall
});
describe('aiLocalToolExecutor inspect_app_health', () => {
afterEach(() => {
delete (globalThis as Record<string, unknown>).__gonaviLastAIMessageRenderError;
});
it('returns an app-level health snapshot across ai setup, logs, connection failures, and workspace tabs', async () => {
(globalThis as Record<string, unknown>).__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, '');
});

View File

@@ -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,

View File

@@ -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 读取真实安全边界');

View File

@@ -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_logsinspect_recent_connection_failures。',
'如果用户提到“AI 不稳定”“整体帮我看看”“GoNavi AI 现在还有哪些明显问题”“连接、MCP、日志一起排查”或“AI 回复气泡显示异常”,优先调用 inspect_app_health 获取 AI 配置、应用日志、连接失败、回复气泡渲染异常和工作区页签的全局健康总览,再决定下钻 inspect_ai_setup_health、inspect_app_logsinspect_recent_connection_failures 或 inspect_ai_last_render_error。',
);
appendGuidanceIfToolAvailable(
messages,

View File

@@ -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: {