feat(ai-tools): 新增连接失败诊断探针并接入快捷命令

- 新增基于 gonavi.log 的连接失败总结探针与结构化根因分类\n- 接入 slash 命令、内置工具目录、状态文案和系统提示\n- 补齐本地执行、insight 解析、指令筛选和注册链路测试
This commit is contained in:
Syngnat
2026-06-09 09:45:24 +08:00
parent 9be10beadc
commit 7d1e066997
13 changed files with 618 additions and 1 deletions

View File

@@ -66,6 +66,8 @@ describe('AIBuiltinToolsCatalog', () => {
expect(markup).toContain('inspect_recent_sql_activity');
expect(markup).toContain('排查应用日志');
expect(markup).toContain('inspect_app_logs');
expect(markup).toContain('排查连接失败与冷却');
expect(markup).toContain('inspect_recent_connection_failures');
expect(markup).toContain('排查 AI 气泡渲染异常');
expect(markup).toContain('inspect_ai_last_render_error');
expect(markup).toContain('复用历史 SQL');

View File

@@ -140,6 +140,11 @@ const BUILTIN_TOOL_FLOWS = [
steps: 'inspect_app_logs → inspect_mcp_setup / inspect_saved_connections / inspect_current_connection',
description: '适合先回看 gonavi.log 尾部的 ERROR/WARN再结合 MCP、连接和当前数据源状态继续定位启动异常、连接失败或外部工具拉起问题。',
},
{
title: '排查连接失败与冷却',
steps: 'inspect_recent_connection_failures → inspect_current_connection / inspect_saved_connections / inspect_app_logs',
description: '适合用户直接问“为什么连接不上”或已经看到冷却/验证失败提示时,先拿到结构化根因、最新地址和下一步建议,再决定回到连接配置还是看更长日志。',
},
{
title: '排查 AI 气泡渲染异常',
steps: 'inspect_ai_last_render_error → inspect_active_tab / inspect_ai_runtime',

View File

@@ -0,0 +1,67 @@
import { describe, expect, it } from 'vitest';
import { buildRecentConnectionFailureSnapshot } from './aiConnectionFailureInsights';
describe('buildRecentConnectionFailureSnapshot', () => {
it('summarizes recent validation failures and cooldown hits from gonavi.log lines', () => {
const snapshot = buildRecentConnectionFailureSnapshot({
readResult: {
success: true,
data: {
logPath: 'C:/Users/demo/.GoNavi/Logs/gonavi.log',
requestedLineLimit: 120,
lines: [
'2026/06/06 19:42:36.037552 [INFO] 获取数据库连接:类型=mysql 地址=10.188.101.184:1523 数据库=(default) 用户=glzc 超时=30s 拓扑=single SSH=192.168.66.28:22 用户=wyeye 缓存Key=026837d0a1b6 启动阶段=稳定期(age=34.322s)',
'2026/06/06 19:42:37.088288 [ERROR] 建立数据库连接失败:类型=mysql 地址=10.188.101.184:1523 数据库=(default) 用户=glzc 超时=30s 拓扑=single SSH=192.168.66.28:22 用户=wyeye 缓存Key=026837d0a1b6错误链连接建立后验证失败10.188.101.184:1523 验证失败: Error 10004 (HY000): Parametric information is abnormal.详细日志C:/Users/demo/.GoNavi/Logs/gonavi.log -> 连接建立后验证失败10.188.101.184:1523 验证失败: Error 10004 (HY000): Parametric information is abnormal.',
'2026/06/06 19:42:37.094045 [ERROR] DBGetDatabases 获取连接失败:类型=mysql 地址=10.188.101.184:1523 数据库=(default) 用户=glzc 超时=30s 拓扑=single SSH=192.168.66.28:22 用户=wyeye错误链连接建立后验证失败10.188.101.184:1523 验证失败: Error 10004 (HY000): Parametric information is abnormal.详细日志C:/Users/demo/.GoNavi/Logs/gonavi.log -> 连接建立后验证失败10.188.101.184:1523 验证失败: Error 10004 (HY000): Parametric information is abnormal.',
'2026/06/06 19:42:37.101316 [WARN] 命中数据库连接失败冷却:类型=mysql 地址=10.188.101.184:1523 数据库=(default) 用户=glzc 超时=30s 拓扑=single SSH=192.168.66.28:22 用户=wyeye 缓存Key=026837d0a1b6 剩余=29s 原因=连接建立后验证失败10.188.101.184:1523 验证失败: Error 10004 (HY000): Parametric information is abnormal.详细日志C:/Users/demo/.GoNavi/Logs/gonavi.log',
],
},
},
});
expect(snapshot.failureEventCount).toBe(3);
expect(snapshot.primaryCategory).toBe('parameter_compatibility');
expect(snapshot.cooldownHitCount).toBe(1);
expect(snapshot.addresses[0]?.address).toBe('10.188.101.184:1523');
expect(snapshot.latestFailure?.cooldownSeconds).toBe(29);
expect(snapshot.nextActions.join('\n')).toContain('multiStatements');
});
it('recognizes mysql compatibility fallback syntax errors as parameter or compatibility issues', () => {
const snapshot = buildRecentConnectionFailureSnapshot({
readResult: {
success: true,
data: {
logPath: 'C:/Users/demo/.GoNavi/Logs/gonavi.log',
lines: [
'2026/06/07 15:50:35.000000 [ERROR] DBGetDatabases 获取连接失败:类型=mysql 地址=127.0.0.1:48749 数据库=(default) 用户=root错误链连接最近失败正在冷却中请 29s 后重试上次错误连接建立后验证失败127.0.0.1:48749 [默认兼容参数] 验证失败: Error 1064 (42000): You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near \'%2Cutf8\' at line 1127.0.0.1:48749 [禁用 multiStatements 兼容重试] 验证失败: Error 1064 (42000): You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near \'%2Cutf8\' at line 1',
],
},
},
});
expect(snapshot.failureEventCount).toBe(1);
expect(snapshot.primaryCategory).toBe('parameter_compatibility');
expect(snapshot.recentFailures[0]?.rootCause).toContain('%2Cutf8');
expect(snapshot.nextActions.join('\n')).toContain('连接参数');
});
it('returns an empty-state message when the tail has no connection-related failures', () => {
const snapshot = buildRecentConnectionFailureSnapshot({
readResult: {
success: true,
data: {
logPath: 'C:/Users/demo/.GoNavi/Logs/gonavi.log',
lines: [
'2026/06/09 10:00:00.000000 [INFO] started',
'2026/06/09 10:00:01.000000 [INFO] ai ready',
],
},
},
});
expect(snapshot.failureEventCount).toBe(0);
expect(snapshot.message).toContain('没有识别到连接失败');
});
});

View File

@@ -0,0 +1,412 @@
const DEFAULT_CONNECTION_FAILURE_LOG_LIMIT = 120;
const MAX_CONNECTION_FAILURE_LOG_LIMIT = 240;
const MAX_RECENT_FAILURES = 8;
type ConnectionFailureCategory =
| 'cooldown'
| 'parameter_compatibility'
| 'validation'
| 'authentication'
| 'timeout'
| 'network'
| 'ssh'
| 'startup'
| 'other';
type ConnectionFailureEventType =
| 'connection_failure'
| 'connection_operation_failure'
| 'cooldown_hit'
| 'ssh_failure';
interface ConnectionFailureEvent {
rawLine: string;
timestamp: string;
level: string;
eventType: ConnectionFailureEventType;
category: ConnectionFailureCategory;
categoryLabel: string;
connectionType: string;
address: string;
dbName: string;
sshAddress: string;
rootCause: string;
cooldownSeconds: number | null;
}
const CONNECTION_FAILURE_CATEGORY_LABELS: Record<ConnectionFailureCategory, string> = {
cooldown: '冷却重试',
parameter_compatibility: '连接参数/兼容性',
validation: '连接验证失败',
authentication: '认证失败',
timeout: '连接超时',
network: '网络不可达',
ssh: 'SSH 隧道失败',
startup: '驱动/进程启动失败',
other: '其他连接异常',
};
const normalizeLogLines = (input: unknown): string[] =>
Array.isArray(input)
? input.map((line) => String(line || '').trim()).filter(Boolean)
: [];
const normalizeConnectionFailureLimit = (input: unknown): number => {
const value = Math.floor(Number(input) || DEFAULT_CONNECTION_FAILURE_LOG_LIMIT);
if (value < 1) {
return 1;
}
if (value > MAX_CONNECTION_FAILURE_LOG_LIMIT) {
return MAX_CONNECTION_FAILURE_LOG_LIMIT;
}
return value;
};
const extractLogTimestamp = (line: string): string => {
const match = String(line || '').match(/^(\d{4}\/\d{2}\/\d{2}\s+\d{2}:\d{2}:\d{2}(?:\.\d+)?)/);
return match?.[1] || '';
};
const extractLogLevelAndPayload = (line: string) => {
const match = String(line || '').match(/\[(INFO|WARN|ERROR)\]\s*(.*)$/);
if (match) {
return {
level: match[1],
payload: String(match[2] || '').trim(),
};
}
return {
level: 'OTHER',
payload: String(line || '').trim(),
};
};
const extractField = (payload: string, field: string): string => {
const match = payload.match(new RegExp(`${field}=([^\\s]+)`));
return String(match?.[1] || '').trim();
};
const extractAddressCandidates = (text: string): string[] => {
const matches = text.match(/\b(?:\d{1,3}(?:\.\d{1,3}){3}|localhost|[A-Za-z0-9._-]+):\d+\b/g);
return Array.isArray(matches) ? Array.from(new Set(matches)) : [];
};
const sanitizeRootCause = (value: string): string =>
String(value || '')
.replace(/(详细日志:[^]+/g, '')
.replace(/\s+/g, ' ')
.replace(/\s*->\s*/g, ' -> ')
.trim();
const extractRootCause = (payload: string): string => {
const errorChainIndex = payload.indexOf('错误链:');
if (errorChainIndex >= 0) {
return sanitizeRootCause(payload.slice(errorChainIndex + '错误链:'.length));
}
const reasonIndex = payload.indexOf('原因=');
if (reasonIndex >= 0) {
return sanitizeRootCause(payload.slice(reasonIndex + '原因='.length));
}
const cooldownIndex = payload.indexOf('连接最近失败,正在冷却中');
if (cooldownIndex >= 0) {
return sanitizeRootCause(payload.slice(cooldownIndex));
}
const sshFailureIndex = payload.indexOf('SSH 连接建立失败:');
if (sshFailureIndex >= 0) {
return sanitizeRootCause(payload.slice(sshFailureIndex + 'SSH 连接建立失败:'.length));
}
const validationIndex = payload.indexOf('连接建立后验证失败:');
if (validationIndex >= 0) {
return sanitizeRootCause(payload.slice(validationIndex));
}
return sanitizeRootCause(payload);
};
const extractCooldownSeconds = (payload: string, rootCause: string): number | null => {
const target = `${payload} ${rootCause}`;
const match = target.match(/(?:剩余=|请\s*)(\d+)s(?:\s*后重试)?/);
if (!match) {
return null;
}
const value = Number(match[1]);
return Number.isFinite(value) ? value : null;
};
const inferConnectionFailureCategory = (
payload: string,
rootCause: string,
): ConnectionFailureCategory => {
const text = `${payload} ${rootCause}`;
const lowerText = text.toLowerCase();
const errorText = rootCause || payload;
const lowerErrorText = errorText.toLowerCase();
if (
text.includes('SSH 连接建立失败')
|| /ssh\s+(dial|handshake|tunnel)/i.test(text)
|| /ssh.*(认证失败|连接失败|超时)/i.test(text)
) {
return 'ssh';
}
if (
lowerErrorText.includes('connect timeout')
|| lowerErrorText.includes('i/o timeout')
|| lowerErrorText.includes('deadline exceeded')
|| errorText.includes('连接超时')
|| /超时(?!\=)/.test(errorText)
) {
return 'timeout';
}
if (
lowerText.includes('access denied')
|| lowerText.includes('authentication failed')
|| lowerText.includes('password')
|| text.includes('凭据')
|| text.includes('认证')
|| text.includes('登录失败')
) {
return 'authentication';
}
if (
lowerText.includes('parametric information is abnormal')
|| lowerText.includes('error 1064')
|| lowerText.includes('%2cutf8')
|| lowerText.includes('multistatements')
|| text.includes('兼容参数')
|| lowerText.includes('syntax')
|| lowerText.includes('dsn')
|| text.includes('参数')
) {
return 'parameter_compatibility';
}
if (text.includes('验证失败')) {
return 'validation';
}
if (
lowerText.includes('connection refused')
|| lowerText.includes('dial tcp')
|| lowerText.includes('no route')
|| lowerText.includes('network is unreachable')
|| lowerText.includes('refused')
) {
return 'network';
}
if (lowerText.includes('spawn') || text.includes('启动失败') || text.includes('拉起失败')) {
return 'startup';
}
if (text.includes('冷却')) {
return 'cooldown';
}
return 'other';
};
const detectConnectionFailureEventType = (payload: string): ConnectionFailureEventType | null => {
if (payload.includes('命中数据库连接失败冷却:') || payload.includes('连接最近失败,正在冷却中')) {
return 'cooldown_hit';
}
if (payload.includes('SSH 连接建立失败:')) {
return 'ssh_failure';
}
if (payload.includes('建立数据库连接失败:')) {
return 'connection_failure';
}
if (/DB[A-Za-z0-9_]+\s+获取连接失败:/.test(payload)) {
return 'connection_operation_failure';
}
return null;
};
const buildConnectionFailureEvent = (line: string): ConnectionFailureEvent | null => {
const timestamp = extractLogTimestamp(line);
const { level, payload } = extractLogLevelAndPayload(line);
const eventType = detectConnectionFailureEventType(payload);
if (!eventType) {
return null;
}
const rootCause = extractRootCause(payload);
const extractedType = extractField(payload, '类型');
const extractedAddress = extractField(payload, '地址');
const addressCandidates = extractAddressCandidates(rootCause);
const address = extractedAddress || addressCandidates[0] || '';
const category = inferConnectionFailureCategory(payload, rootCause);
return {
rawLine: line,
timestamp,
level,
eventType,
category,
categoryLabel: CONNECTION_FAILURE_CATEGORY_LABELS[category],
connectionType: extractedType,
address,
dbName: extractField(payload, '数据库'),
sshAddress: extractField(payload, 'SSH'),
rootCause,
cooldownSeconds: extractCooldownSeconds(payload, rootCause),
};
};
const appendUnique = (items: string[], value: string) => {
const normalized = String(value || '').trim();
if (!normalized || items.includes(normalized)) {
return;
}
items.push(normalized);
};
const buildNextActions = (events: ConnectionFailureEvent[]): string[] => {
const categories = new Set(events.map((event) => event.category));
const hasCooldownEvent = events.some((event) => event.eventType === 'cooldown_hit');
const nextActions: string[] = [];
const latestEvent = events[events.length - 1];
if (hasCooldownEvent || categories.has('cooldown')) {
appendUnique(nextActions, '先修复上一次真实连接错误,再重试;只反复刷新会持续命中连接冷却。');
}
if (categories.has('parameter_compatibility')) {
appendUnique(nextActions, '优先核对连接参数、DSN 和协议兼容性,尤其是 multiStatements、charset、额外 query 参数和 URL 编码。');
}
if (categories.has('validation')) {
appendUnique(nextActions, '检查服务端返回的验证失败细节,确认当前数据库类型、驱动协议、库名或 Service Name 与目标服务匹配。');
}
if (categories.has('authentication')) {
appendUnique(nextActions, '核对用户名、密码、认证库、租户或 Service Name 是否正确,确认服务端是否允许当前账号登录。');
}
if (categories.has('ssh')) {
appendUnique(nextActions, '核对 SSH 跳板机地址、端口、账号和隧道目标地址,必要时先验证跳板机到数据库的连通性。');
}
if (categories.has('timeout') || categories.has('network')) {
appendUnique(nextActions, '检查目标地址、端口、防火墙、代理和隧道链路是否可达,确认服务端当前确实在监听。');
}
if (categories.has('startup')) {
appendUnique(nextActions, '检查驱动进程或外部依赖是否能正常启动,必要时先在本机或目标主机单独验证启动命令。');
}
if (latestEvent?.address) {
appendUnique(nextActions, `如需核对当前界面里的连接是否还是同一目标,可再调用 inspect_current_connection 检查是否仍指向 ${latestEvent.address}`);
}
if (nextActions.length === 0) {
appendUnique(nextActions, '先结合 inspect_current_connection 和 inspect_saved_connections 核对当前连接配置,再扩大日志窗口继续排查。');
}
return nextActions;
};
export const buildRecentConnectionFailureSnapshot = (params: {
readResult?: any;
keyword?: unknown;
lineLimit?: unknown;
}) => {
const data = params.readResult?.data && typeof params.readResult.data === 'object'
? params.readResult.data as Record<string, unknown>
: {};
const lines = normalizeLogLines(data.lines);
const keyword = String(data.keyword || params.keyword || '').trim();
const requestedLineLimit = normalizeConnectionFailureLimit(data.requestedLineLimit ?? params.lineLimit);
const events = lines
.map((line) => buildConnectionFailureEvent(line))
.filter((event): event is ConnectionFailureEvent => Boolean(event));
const latestEvent = events[events.length - 1];
const categoryCounts = new Map<ConnectionFailureCategory, number>();
const addressCounts = new Map<string, { count: number; connectionTypes: Set<string>; lastSeenAt: string }>();
events.forEach((event) => {
categoryCounts.set(event.category, (categoryCounts.get(event.category) || 0) + 1);
if (!event.address) {
return;
}
const current = addressCounts.get(event.address) || {
count: 0,
connectionTypes: new Set<string>(),
lastSeenAt: '',
};
current.count += 1;
if (event.connectionType) {
current.connectionTypes.add(event.connectionType);
}
current.lastSeenAt = event.timestamp || current.lastSeenAt;
addressCounts.set(event.address, current);
});
const categorySummary = Array.from(categoryCounts.entries())
.sort((left, right) => right[1] - left[1])
.map(([category, count]) => ({
category,
label: CONNECTION_FAILURE_CATEGORY_LABELS[category],
count,
}));
const primaryCategory = categorySummary[0]?.category || '';
const addresses = Array.from(addressCounts.entries())
.sort((left, right) => right[1].count - left[1].count)
.map(([address, summary]) => ({
address,
count: summary.count,
connectionTypes: Array.from(summary.connectionTypes),
lastSeenAt: summary.lastSeenAt,
}));
return {
logPath: String(data.logPath || ''),
keyword,
requestedLineLimit,
returnedLineCount: lines.length,
fileWindowTruncated: data.fileWindowTruncated === true,
matchedLinesTruncated: data.matchedLinesTruncated === true,
failureEventCount: events.length,
hasRecentFailures: events.length > 0,
primaryCategory,
primaryCategoryLabel: primaryCategory
? CONNECTION_FAILURE_CATEGORY_LABELS[primaryCategory as ConnectionFailureCategory]
: '',
cooldownHitCount: events.filter((event) => event.eventType === 'cooldown_hit').length,
validationFailureCount: events.filter((event) => event.category === 'validation').length,
sshFailureCount: events.filter((event) => event.category === 'ssh').length,
categorySummary,
addresses,
latestFailureAt: latestEvent?.timestamp || '',
latestFailure: latestEvent
? {
timestamp: latestEvent.timestamp,
level: latestEvent.level,
eventType: latestEvent.eventType,
category: latestEvent.category,
categoryLabel: latestEvent.categoryLabel,
connectionType: latestEvent.connectionType,
address: latestEvent.address,
dbName: latestEvent.dbName,
sshAddress: latestEvent.sshAddress,
cooldownSeconds: latestEvent.cooldownSeconds,
rootCause: latestEvent.rootCause,
rawLine: latestEvent.rawLine,
}
: null,
recentFailures: events
.slice(-MAX_RECENT_FAILURES)
.reverse()
.map((event) => ({
timestamp: event.timestamp,
level: event.level,
eventType: event.eventType,
category: event.category,
categoryLabel: event.categoryLabel,
connectionType: event.connectionType,
address: event.address,
dbName: event.dbName,
sshAddress: event.sshAddress,
cooldownSeconds: event.cooldownSeconds,
rootCause: event.rootCause,
rawLine: event.rawLine,
})),
nextActions: buildNextActions(events),
message: events.length > 0
? `最近日志里识别到 ${events.length} 条连接相关异常,最新一条是 ${latestEvent?.categoryLabel || '连接异常'}`
: keyword
? `最近日志里没有找到与“${keyword}”相关的连接失败记录`
: '最近日志里没有识别到连接失败、验证失败或连接冷却记录',
};
};

View File

@@ -0,0 +1,52 @@
import { describe, expect, it, vi } from 'vitest';
import type { AIToolCall } from '../../types';
import { executeLocalAIToolCall } from './aiLocalToolExecutor';
const buildToolCall = (
name: string,
args: Record<string, unknown>,
): AIToolCall => ({
id: `call-${name}`,
type: 'function',
function: {
name,
arguments: JSON.stringify(args),
},
});
describe('aiLocalToolExecutor inspect_recent_connection_failures', () => {
it('returns a structured snapshot for recent connection failures, cooldown hits, and compatibility errors', async () => {
const result = await executeLocalAIToolCall({
toolCall: buildToolCall('inspect_recent_connection_failures', {
keyword: 'mysql',
lineLimit: 120,
}),
connections: [],
mcpTools: [],
toolContextMap: new Map(),
runtime: {
getDatabases: vi.fn(),
getTables: vi.fn(),
readAppLogTail: vi.fn().mockResolvedValue({
success: true,
data: {
logPath: 'C:/Users/demo/.GoNavi/Logs/gonavi.log',
keyword: 'mysql',
requestedLineLimit: 120,
lines: [
'2026/06/07 15:50:35.000000 [ERROR] 建立数据库连接失败:类型=mysql 地址=127.0.0.1:48749 数据库=(default) 用户=root错误链连接建立后验证失败127.0.0.1:48749 [默认兼容参数] 验证失败: Error 1064 (42000): You have an error in your SQL syntax near \'%2Cutf8\' at line 1',
'2026/06/07 15:50:36.000000 [WARN] 命中数据库连接失败冷却:类型=mysql 地址=127.0.0.1:48749 数据库=(default) 用户=root 剩余=29s 原因=连接建立后验证失败127.0.0.1:48749 [禁用 multiStatements 兼容重试] 验证失败: Error 1064 (42000): You have an error in your SQL syntax near \'%2Cutf8\' at line 1',
],
},
}),
},
});
expect(result.success).toBe(true);
expect(result.content).toContain('"primaryCategory":"parameter_compatibility"');
expect(result.content).toContain('"cooldownHitCount":1');
expect(result.content).toContain('127.0.0.1:48749');
expect(result.content).toContain('multiStatements');
});
});

View File

@@ -14,6 +14,7 @@ describe('aiSlashCommands', () => {
expect(commands.some((command) => command.cmd === '/health')).toBe(true);
expect(commands.some((command) => command.cmd === '/mcp')).toBe(true);
expect(commands.some((command) => command.cmd === '/mcpadd')).toBe(true);
expect(commands.some((command) => command.cmd === '/connfail')).toBe(true);
expect(commands.some((command) => command.cmd === '/shortcuts')).toBe(true);
expect(commands.some((command) => command.cmd === '/applog')).toBe(true);
expect(commands.some((command) => command.cmd === '/airender')).toBe(true);
@@ -31,6 +32,11 @@ describe('aiSlashCommands', () => {
expect(filterAISlashCommands('/sho').map((command) => command.cmd)).toContain('/shortcuts');
});
it('supports filtering connection-failure diagnostics by chinese keyword and command prefix', () => {
expect(filterAISlashCommands('连接失败').map((command) => command.cmd)).toContain('/connfail');
expect(filterAISlashCommands('/conn').map((command) => command.cmd)).toContain('/connfail');
});
it('supports filtering app-log diagnostics by chinese keyword and command prefix', () => {
expect(filterAISlashCommands('日志').map((command) => command.cmd)).toContain('/applog');
expect(filterAISlashCommands('/app').map((command) => command.cmd)).toContain('/applog');
@@ -56,6 +62,7 @@ describe('aiSlashCommands', () => {
expect(featured).toContain('/health');
expect(featured).toContain('/mcp');
expect(featured).toContain('/mcpadd');
expect(featured).toContain('/connfail');
expect(featured).not.toContain('/shortcuts');
});
});

View File

@@ -50,6 +50,7 @@ export const DEFAULT_AI_SLASH_COMMANDS: AISlashCommandDefinition[] = [
{ cmd: '/health', label: '🩺 AI 配置体检', desc: '调用体检探针总览当前 AI 配置', prompt: '请先调用 inspect_ai_setup_health对当前 GoNavi AI 配置做一次完整体检,然后总结 blockers、warnings 和 nextActions。', category: 'diagnose', featured: true, keywords: ['health', '体检', 'ai配置', '探针'] },
{ cmd: '/mcp', label: '🪛 排查 MCP 接入', desc: '检查 MCP 服务和外部客户端状态', prompt: '请先调用 inspect_mcp_setup帮我盘点当前 MCP 服务、工具发现结果,以及 Claude Code / Codex 的接入状态。', category: 'diagnose', featured: true, keywords: ['mcp', 'codex', 'claude', '外部客户端'] },
{ cmd: '/mcpadd', label: '🧭 新增 MCP 指引', desc: '查看 command、args、env 和模板怎么填', prompt: '请先调用 inspect_mcp_authoring_guide再结合 inspect_mcp_setup告诉我新增 GoNavi MCP 服务时 command、args、env、timeout 应该怎么填,以及最接近的模板应该选哪个。', category: 'diagnose', featured: true, keywords: ['mcp新增', 'command', 'args', 'env', '模板'] },
{ cmd: '/connfail', label: '🧯 连接失败探针', desc: '总结最近连接失败、冷却和验证异常', prompt: '请先调用 inspect_recent_connection_failures帮我总结最近数据库连接失败、连接冷却、验证失败和 SSH 隧道异常的真实日志结论;如果已经有明确地址或类型,再结合 inspect_current_connection 或 inspect_saved_connections 继续缩小范围。', category: 'diagnose', featured: true, keywords: ['连接失败', '冷却', '验证失败', 'ssh', 'mysql'] },
{ cmd: '/shortcuts', label: '⌨️ 快捷键探针', desc: '读取当前 Win/Mac 快捷键配置', prompt: '请先调用 inspect_shortcuts告诉我当前 GoNavi 的快捷键配置,尤其是执行 SQL、切换结果区、打开 AI 面板和 AI 发送消息这些动作在当前平台和另一平台分别怎么按,是否改过默认值。', category: 'diagnose', keywords: ['快捷键', 'shortcuts', '结果区', 'mac', 'windows'] },
{ cmd: '/applog', label: '🪵 应用日志', desc: '回看最近 GoNavi 应用日志', prompt: '请先调用 inspect_app_logs帮我看最近 GoNavi 应用日志里的错误和警告如果我提到连接失败、MCP 拉起失败、启动异常或 gonavi.log就优先结合关键词继续筛。', category: 'diagnose', keywords: ['日志', 'gonavi.log', 'mcp报错', '连接失败', '启动异常'] },
{ cmd: '/airender', label: '🧯 AI 渲染异常', desc: '读取最近一次 AI 消息渲染失败记录', prompt: '请先调用 inspect_ai_last_render_error告诉我最近一次 AI 消息渲染失败记录里是哪条消息、报错摘要是什么,以及下一步该怎么排查。', category: 'diagnose', keywords: ['渲染失败', '气泡空白', 'ai消息', 'render', '白块'] },

View File

@@ -34,6 +34,7 @@ import {
} from './aiWorkspaceInsights';
import { buildShortcutSnapshot } from './aiShortcutInsights';
import { buildAILastRenderErrorSnapshot } from './aiLastRenderErrorInsights';
import { buildRecentConnectionFailureSnapshot } from './aiConnectionFailureInsights';
import { executeAIConfigSnapshotToolCall } from './aiSnapshotInspectionAIConfigToolExecutor';
import type {
AISnapshotInspectionRuntime,
@@ -269,6 +270,25 @@ export async function executeSnapshotInspectionToolCall(
success: true,
};
}
case 'inspect_recent_connection_failures': {
const readResult = typeof runtime?.readAppLogTail === 'function'
? await runtime.readAppLogTail(Number(args.lineLimit) || 120, String(args.keyword || ''))
: { success: false, message: '当前环境暂不支持读取 GoNavi 应用日志' };
if (!readResult?.success) {
return {
content: `读取最近连接失败记录失败: ${readResult?.message || '未知错误'}`,
success: false,
};
}
return {
content: JSON.stringify(buildRecentConnectionFailureSnapshot({
readResult,
keyword: args.keyword,
lineLimit: args.lineLimit,
})),
success: true,
};
}
case 'inspect_ai_last_render_error':
return {
content: JSON.stringify(buildAILastRenderErrorSnapshot()),
@@ -335,6 +355,7 @@ export async function executeSnapshotInspectionToolCall(
inspect_recent_sql_logs: '获取最近 SQL 日志失败',
inspect_recent_sql_activity: '汇总最近 SQL 活动失败',
inspect_app_logs: '读取 GoNavi 应用日志失败',
inspect_recent_connection_failures: '汇总最近连接失败记录失败',
inspect_ai_last_render_error: '读取最近一次 AI 渲染异常失败',
inspect_saved_queries: '读取已保存查询失败',
inspect_sql_snippets: '读取 SQL 片段失败',

View File

@@ -68,7 +68,7 @@ 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_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_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_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,
});
@@ -90,6 +90,7 @@ describe('buildAISystemContextMessages', () => {
expect(joined).toContain('inspect_external_sql_directories');
expect(joined).toContain('inspect_external_sql_file');
expect(joined).toContain('inspect_recent_sql_activity');
expect(joined).toContain('inspect_recent_connection_failures 读取真实连接失败总结');
expect(joined).toContain('inspect_app_logs 读取真实应用日志尾部');
expect(joined).toContain('inspect_ai_last_render_error 读取最近一次被隔离的前端渲染异常记录');
expect(joined).toContain('inspect_saved_queries');

View File

@@ -225,6 +225,19 @@ const appendAppLogInspectionGuidance = (
});
};
const appendConnectionFailureInspectionGuidance = (
messages: AISystemContextMessage[],
availableToolNames: string[],
) => {
if (!availableToolNames.includes('inspect_recent_connection_failures')) {
return;
}
messages.push({
role: 'system',
content: '如果用户提到“为什么连接不上”“连接最近失败正在冷却中”“验证失败”“SSH 隧道是不是有问题”“multiStatements / 参数兼容异常”,优先调用 inspect_recent_connection_failures 读取真实连接失败总结,再决定是否继续下钻 inspect_current_connection、inspect_saved_connections 或 inspect_app_logs。',
});
};
const appendAILastRenderErrorInspectionGuidance = (
messages: AISystemContextMessage[],
availableToolNames: string[],
@@ -478,6 +491,7 @@ SELECT * FROM users WHERE status = 1;
appendMCPAuthoringInspectionGuidance(systemMessages, availableToolNames);
appendAIGuidanceInspectionGuidance(systemMessages, availableToolNames);
appendShortcutInspectionGuidance(systemMessages, availableToolNames);
appendConnectionFailureInspectionGuidance(systemMessages, availableToolNames);
appendAppLogInspectionGuidance(systemMessages, availableToolNames);
appendAILastRenderErrorInspectionGuidance(systemMessages, availableToolNames);
if (availableToolNames.includes('inspect_current_connection')) {

View File

@@ -52,6 +52,7 @@ const TOOL_ACTION_LABELS: Record<string, string> = {
inspect_recent_sql_logs: '回看最近 SQL 执行日志',
inspect_recent_sql_activity: '总结最近 SQL 活动',
inspect_app_logs: '回看 GoNavi 应用日志',
inspect_recent_connection_failures: '总结最近连接失败记录',
inspect_ai_last_render_error: '读取最近一次 AI 渲染异常',
inspect_saved_queries: '检索本地已保存查询',
inspect_sql_snippets: '读取 SQL 片段模板',

View File

@@ -401,6 +401,29 @@ export const BUILTIN_AI_INSPECTION_TOOL_INFO: AIBuiltinToolInfo[] = [
},
},
},
{
name: "inspect_recent_connection_failures",
icon: "🧯",
desc: "总结最近数据库连接失败与冷却原因",
detail:
"从最近一段 gonavi.log 里提取数据库连接失败、连接验证失败、SSH 隧道异常和连接冷却命中记录自动归类主要问题类型、最新地址、最新根因和下一步建议。适合用户提到“为什么连接不上”“连接最近失败正在冷却”“验证失败”“SSH 隧道是不是有问题”时,先读这份结构化总结,而不是人工翻整段日志。",
params: "keyword?, lineLimit?(默认 120)",
tool: {
type: "function",
function: {
name: "inspect_recent_connection_failures",
description:
"汇总最近 GoNavi 应用日志中的数据库连接失败、连接验证失败、SSH 隧道失败和冷却命中记录并返回主要异常类别、最新地址、最新根因与建议动作。适用于用户提到为什么连接不上、最近一直命中连接冷却、服务端验证失败、multiStatements 或参数兼容异常时,优先读取这份结构化连接失败总结,不要直接让模型肉眼翻整段日志。",
parameters: {
type: "object",
properties: {
keyword: { type: "string", description: "可选,按连接类型、地址或异常关键词过滤,例如 mysql、ssh、timeout、127.0.0.1" },
lineLimit: { type: "number", description: "可选,最多分析多少行日志,默认 120最大 240" },
},
},
},
},
},
{
name: "inspect_ai_last_render_error",
icon: "🧯",

View File

@@ -108,6 +108,13 @@ describe('aiToolRegistry', () => {
expect(info?.tool.function.description).toContain('gonavi.log');
});
it('registers the recent-connection-failure inspector as a builtin tool', () => {
const info = BUILTIN_AI_TOOL_INFO.find((item) => item.name === 'inspect_recent_connection_failures');
expect(info).toBeTruthy();
expect(info?.desc).toContain('连接失败');
expect(info?.tool.function.description).toContain('multiStatements');
});
it('registers the ai-render-error inspector as a builtin tool', () => {
const info = BUILTIN_AI_TOOL_INFO.find((item) => item.name === 'inspect_ai_last_render_error');
expect(info).toBeTruthy();
@@ -118,6 +125,7 @@ describe('aiToolRegistry', () => {
it('registers the recent-sql-activity, saved-query, and sql-snippet inspectors as builtin tools', () => {
const recentActivityTool = BUILTIN_AI_TOOL_INFO.find((item) => item.name === 'inspect_recent_sql_activity');
const appLogTool = BUILTIN_AI_TOOL_INFO.find((item) => item.name === 'inspect_app_logs');
const connectionFailureTool = BUILTIN_AI_TOOL_INFO.find((item) => item.name === 'inspect_recent_connection_failures');
const renderErrorTool = BUILTIN_AI_TOOL_INFO.find((item) => item.name === 'inspect_ai_last_render_error');
const savedQueryTool = BUILTIN_AI_TOOL_INFO.find((item) => item.name === 'inspect_saved_queries');
const aiSessionsTool = BUILTIN_AI_TOOL_INFO.find((item) => item.name === 'inspect_ai_sessions');
@@ -127,6 +135,8 @@ describe('aiToolRegistry', () => {
expect(recentActivityTool?.tool.function.description).toContain('最近 SQL 活动');
expect(appLogTool?.desc).toContain('GoNavi 应用日志');
expect(appLogTool?.tool.function.description).toContain('应用日志');
expect(connectionFailureTool?.desc).toContain('连接失败');
expect(connectionFailureTool?.tool.function.description).toContain('连接冷却');
expect(renderErrorTool?.desc).toContain('渲染异常记录');
expect(renderErrorTool?.tool.function.description).toContain('气泡局部报错');
expect(savedQueryTool?.desc).toContain('已保存的 SQL 查询');
@@ -168,6 +178,7 @@ describe('aiToolRegistry', () => {
expect(tools.some((item) => item.function.name === 'inspect_external_sql_file')).toBe(true);
expect(tools.some((item) => item.function.name === 'inspect_recent_sql_activity')).toBe(true);
expect(tools.some((item) => item.function.name === 'inspect_app_logs')).toBe(true);
expect(tools.some((item) => item.function.name === 'inspect_recent_connection_failures')).toBe(true);
expect(tools.some((item) => item.function.name === 'inspect_ai_last_render_error')).toBe(true);
expect(tools.some((item) => item.function.name === 'inspect_saved_queries')).toBe(true);
expect(tools.some((item) => item.function.name === 'inspect_ai_sessions')).toBe(true);