mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-06-15 19:19:35 +08:00
✨ feat(ai): 将渲染异常纳入应用健康总览
- 在 inspect_app_health 汇总最近一次 AI 回复气泡渲染异常 - 同步内置工具目录和系统探针引导 - 补充应用健康和本地工具执行器测试
This commit is contained in:
@@ -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 配置',
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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, '');
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 读取真实安全边界');
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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: {
|
||||
|
||||
Reference in New Issue
Block a user