feat(ai-tools): 新增 AI 应用健康总览探针

This commit is contained in:
Syngnat
2026-06-10 00:41:48 +08:00
parent 73e93e955c
commit 01d8fe44ce
10 changed files with 731 additions and 1 deletions

View File

@@ -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 当前能力');

View File

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

View File

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

View File

@@ -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<string, AIContextItem[]>;
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,
};
};

View File

@@ -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<string, unknown>): 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, '');
});
});

View File

@@ -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<string, any>;
activeContext?: { connectionId: string; dbName: string } | null;
aiContexts?: Record<string, AIContextItem[]>;
connections: SavedConnection[];
tabs?: TabData[];
activeTabId?: string | null;
mcpTools: AIMCPToolDescriptor[];
skills?: AISkillConfig[];
userPromptSettings?: AIUserPromptSettings;
dynamicModels?: string[];
runtime?: AISnapshotInspectionRuntime;
}
const loadRuntimeState = async (
runtime: AISnapshotInspectionRuntime | undefined,
): Promise<AISnapshotInspectionRuntimeState | undefined> =>
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<SnapshotInspectionResult | null> {
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,
};
}
}

View File

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

View File

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

View File

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

View File

@@ -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: "🩺",