feat(ai-tools): 新增日志与快捷键探针并完善 MCP 配置引导

This commit is contained in:
Syngnat
2026-06-09 07:46:38 +08:00
parent ee5623d290
commit 6841e69008
33 changed files with 1782 additions and 807 deletions

View File

@@ -58,10 +58,14 @@ describe('AIBuiltinToolsCatalog', () => {
expect(markup).toContain('inspect_active_tab');
expect(markup).toContain('盘点当前工作区');
expect(markup).toContain('inspect_workspace_tabs');
expect(markup).toContain('查看当前快捷键配置');
expect(markup).toContain('inspect_shortcuts');
expect(markup).toContain('回看最近执行记录');
expect(markup).toContain('inspect_recent_sql_logs');
expect(markup).toContain('总结最近 SQL 活动');
expect(markup).toContain('inspect_recent_sql_activity');
expect(markup).toContain('排查应用日志');
expect(markup).toContain('inspect_app_logs');
expect(markup).toContain('复用历史 SQL');
expect(markup).toContain('inspect_saved_queries');
expect(markup).toContain('回看 AI 历史对话');

View File

@@ -120,6 +120,11 @@ const BUILTIN_TOOL_FLOWS = [
steps: 'inspect_workspace_tabs → inspect_active_tab → get_columns / execute_sql',
description: '适合先看当前打开了哪些 SQL / 表 / 命令页签,再切到目标页签继续做字段核对、对比分析和只读验证。',
},
{
title: '查看当前快捷键配置',
steps: 'inspect_shortcuts → inspect_active_tab / inspect_workspace_tabs',
description: '适合先确认当前 Win / Mac 快捷键、是否改过默认值以及结果区、AI 面板、查询执行等动作到底该怎么按,再结合当前页签解释具体使用场景。',
},
{
title: '回看最近执行记录',
steps: 'inspect_recent_sql_logs → get_columns / get_indexes / execute_sql',
@@ -130,6 +135,11 @@ const BUILTIN_TOOL_FLOWS = [
steps: 'inspect_recent_sql_activity → inspect_recent_sql_logs → inspect_current_connection',
description: '适合先看最近到底以读还是写为主、有没有 DDL 或删除、哪个库最近报错最多,再决定继续下钻哪条日志或哪个连接。',
},
{
title: '排查应用日志',
steps: 'inspect_app_logs → inspect_mcp_setup / inspect_saved_connections / inspect_current_connection',
description: '适合先回看 gonavi.log 尾部的 ERROR/WARN再结合 MCP、连接和当前数据源状态继续定位启动异常、连接失败或外部工具拉起问题。',
},
{
title: '复用历史 SQL',
steps: 'inspect_saved_queries → get_columns / execute_sql',

View File

@@ -62,13 +62,13 @@ describe('AIMCPClientInstallPanel', () => {
/>,
);
expect(markup).toContain('这里是在给外部客户端接入 GoNavi MCP');
expect(markup).toContain('入外部客户端');
expect(markup).toContain('这里是在把当前 GoNavi MCP 启动配置写给外部客户端');
expect(markup).toContain('入外部客户端配置');
expect(markup).toContain('目标客户端');
expect(markup).toContain('选择目标客户端');
expect(markup).toContain('写入当前 GoNavi 配置');
expect(markup).toContain('重启对应客户端');
expect(markup).toContain('未入');
expect(markup).toContain('未入');
expect(markup).toContain('需更新');
expect(markup).toContain('复制配置路径');
expect(markup).toContain('复制启动命令');
@@ -130,7 +130,7 @@ describe('AIMCPClientInstallPanel', () => {
expect(markup).toContain('写入到已选客户端');
expect(markup).toContain('CLI 检测:未检测到 claude');
expect(markup).toContain('未检测到本机 claude 命令');
expect(markup).toContain('已入');
expect(markup).toContain('已入');
});
it('makes repeated install avoidance explicit when the selected client already matches current GoNavi', () => {
@@ -181,8 +181,60 @@ describe('AIMCPClientInstallPanel', () => {
/>,
);
expect(markup).toContain('当前状态:已入当前 GoNavi无需重复写入');
expect(markup).toContain('当前已入,无需重复写入');
expect(markup).toContain('当前状态:已入当前 GoNavi无需重复写入');
expect(markup).toContain('当前已入,无需重复写入');
expect(markup).toContain('下面的主按钮会自动禁用,避免重复写入');
});
it('prefers the client that already matches current GoNavi over another stale installed record', () => {
const markup = renderToStaticMarkup(
<AIMCPClientInstallPanel
statuses={[
{
client: 'claude-code',
displayName: 'Claude Code',
installed: true,
matchesCurrent: true,
clientDetected: true,
clientCommand: 'claude',
message: '已检测到 Claude Code 用户级 GoNavi MCP 配置,且与当前 GoNavi 安装路径一致',
},
{
client: 'codex',
displayName: 'Codex',
installed: true,
matchesCurrent: false,
clientDetected: true,
clientCommand: 'codex',
message: '已检测到 Codex 中的 GoNavi MCP 记录,但与当前 GoNavi 安装路径不一致,建议更新',
},
]}
selectedClient="claude-code"
selectedStatus={{
client: 'claude-code',
displayName: 'Claude Code',
installed: true,
matchesCurrent: true,
clientDetected: true,
clientCommand: 'claude',
message: '已检测到 Claude Code 用户级 GoNavi MCP 配置,且与当前 GoNavi 安装路径一致',
}}
selectedCommandText="gonavi-mcp-server stdio"
darkMode={false}
overlayTheme={buildOverlayWorkbenchTheme(false)}
cardBg="#fff"
cardBorder="rgba(0,0,0,0.08)"
loading={false}
statusLoading={false}
onSelectClient={() => {}}
onRefreshStatus={() => {}}
onCopyConfigPath={() => {}}
onCopyLaunchCommand={() => {}}
onInstall={() => {}}
/>,
);
expect(markup).toContain('Claude Code 状态');
expect(markup).toContain('当前状态:已写入当前 GoNavi无需重复写入');
});
});

View File

@@ -30,7 +30,7 @@ const hasStatusIssue = (status: AIMCPClientInstallStatus | undefined) =>
const getStatusTone = (status: AIMCPClientInstallStatus | undefined, darkMode: boolean) => {
if (status?.matchesCurrent) {
return {
label: '已入',
label: '已入',
color: '#16a34a',
bg: darkMode ? 'rgba(34,197,94,0.18)' : 'rgba(34,197,94,0.12)',
};
@@ -50,7 +50,7 @@ const getStatusTone = (status: AIMCPClientInstallStatus | undefined, darkMode: b
};
}
return {
label: '未入',
label: '未入',
color: darkMode ? 'rgba(255,255,255,0.72)' : '#64748b',
bg: darkMode ? 'rgba(255,255,255,0.08)' : 'rgba(100,116,139,0.08)',
};
@@ -67,7 +67,7 @@ const resolveClientCommandName = (status: AIMCPClientInstallStatus | undefined)
const getStatusSummary = (status: AIMCPClientInstallStatus | undefined) => {
const label = status?.displayName || '这个客户端';
if (status?.matchesCurrent) {
return `${label}入当前这份 GoNavi MCP可直接在这个客户端里调用。`;
return `${label}入当前这份 GoNavi MCP可直接在这个客户端里调用。`;
}
if (status?.installed) {
return `${label} 里已经有旧的 GoNavi 记录,更新后会切到当前这份 GoNavi。`;
@@ -75,12 +75,12 @@ const getStatusSummary = (status: AIMCPClientInstallStatus | undefined) => {
if (hasStatusIssue(status)) {
return `${label} 的接入状态读取失败,建议先刷新检测。`;
}
return `当前还没有把 GoNavi MCP 写入 ${label}`;
return `当前还没有把这份 GoNavi MCP 写入 ${label}`;
};
const getClientOptionSummary = (status: AIMCPClientInstallStatus | undefined) => {
if (status?.matchesCurrent) {
return '当前 GoNavi MCP 已入到这个客户端。';
return '当前这份 GoNavi MCP 已入到这个客户端。';
}
if (status?.installed) {
return '检测到旧的 GoNavi MCP 记录,建议更新为当前安装路径。';
@@ -88,7 +88,7 @@ const getClientOptionSummary = (status: AIMCPClientInstallStatus | undefined) =>
if (hasStatusIssue(status)) {
return '接入状态读取异常,建议先刷新再处理。';
}
return '尚未入 GoNavi MCP 配置。';
return '尚未入 GoNavi MCP 配置。';
};
const getClientDetectionSummary = (status: AIMCPClientInstallStatus | undefined) => {
@@ -102,7 +102,7 @@ const getClientDetectionSummary = (status: AIMCPClientInstallStatus | undefined)
const getSelectedClientStateLine = (status: AIMCPClientInstallStatus | undefined) => {
if (status?.matchesCurrent) {
return '已入当前 GoNavi无需重复写入';
return '已入当前 GoNavi无需重复写入';
}
if (status?.installed) {
return '已存在旧记录,建议更新到当前 GoNavi 路径';
@@ -115,7 +115,7 @@ const getSelectedClientStateLine = (status: AIMCPClientInstallStatus | undefined
const resolveActionLabel = (status: AIMCPClientInstallStatus | undefined) => {
if (status?.matchesCurrent) {
return '当前已入,无需重复写入';
return '当前已入,无需重复写入';
}
if (status?.installed) {
return '更新已选客户端配置';
@@ -164,17 +164,17 @@ const AIMCPClientInstallPanel: React.FC<AIMCPClientInstallPanelProps> = ({
}}
>
<div style={{ fontWeight: 700, fontSize: 13, color: overlayTheme.titleText }}>
GoNavi MCP GoNavi MCP
GoNavi MCP GoNavi MCP
</div>
<div style={{ fontSize: 12, color: overlayTheme.mutedText, lineHeight: 1.7 }}>
Claude Code Codex GoNavi GoNavi MCP
Claude Code Codex GoNavi GoNavi MCP GoNavi GoNavi
</div>
</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
<div style={{ fontWeight: 700, fontSize: 14, color: overlayTheme.titleText }}></div>
<div style={{ fontWeight: 700, fontSize: 14, color: overlayTheme.titleText }}></div>
<div style={{ fontSize: 12, color: overlayTheme.mutedText, lineHeight: 1.7 }}>
1 GoNavi MCP exe
1 GoNavi MCP exe
</div>
</div>
<div

View File

@@ -95,8 +95,8 @@ describe('AISettingsMCPSection', () => {
/>,
);
expect(markup).toContain('入外部客户端');
expect(markup).toContain('接入到 Claude Code');
expect(markup).toContain('入外部客户端配置');
expect(markup).toContain('尚未写入 GoNavi MCP 配置');
expect(markup).toContain('常见启动方式模板');
expect(markup).toContain('Node 脚本');
expect(markup).toContain('新增 MCP 服务');

View File

@@ -0,0 +1,48 @@
import { describe, expect, it } from 'vitest';
import { buildAppLogSnapshot } from './aiAppLogInsights';
describe('buildAppLogSnapshot', () => {
it('keeps returned lines and computes level breakdown', () => {
const snapshot = buildAppLogSnapshot({
readResult: {
success: true,
data: {
logPath: 'C:/Users/demo/.GoNavi/Logs/gonavi.log',
requestedLineLimit: 50,
matchedLinesTruncated: true,
lines: [
'2026/06/09 10:00:00.000000 [INFO] started',
'2026/06/09 10:00:01.000000 [WARN] slow mcp boot',
'2026/06/09 10:00:02.000000 [ERROR] mysql dial failed',
],
},
},
});
expect(snapshot.logPath).toContain('gonavi.log');
expect(snapshot.returnedLineCount).toBe(3);
expect(snapshot.matchedLinesTruncated).toBe(true);
expect(snapshot.levelBreakdown.INFO).toBe(1);
expect(snapshot.levelBreakdown.WARN).toBe(1);
expect(snapshot.levelBreakdown.ERROR).toBe(1);
expect(snapshot.hasWarnings).toBe(true);
expect(snapshot.hasErrors).toBe(true);
});
it('returns an empty-state message when keyword filtering yields nothing', () => {
const snapshot = buildAppLogSnapshot({
readResult: {
success: true,
data: {
logPath: 'C:/Users/demo/.GoNavi/Logs/gonavi.log',
lines: [],
},
},
keyword: 'mcp',
});
expect(snapshot.returnedLineCount).toBe(0);
expect(snapshot.message).toContain('mcp');
});
});

View File

@@ -0,0 +1,67 @@
const DEFAULT_APP_LOG_LIMIT = 80;
const MAX_APP_LOG_LIMIT = 200;
const normalizeAppLogLimit = (input: unknown): number => {
const value = Math.floor(Number(input) || DEFAULT_APP_LOG_LIMIT);
if (value < 1) return 1;
if (value > MAX_APP_LOG_LIMIT) return MAX_APP_LOG_LIMIT;
return value;
};
const normalizeLogLines = (input: unknown): string[] =>
Array.isArray(input)
? input.map((line) => String(line || '').trim()).filter(Boolean)
: [];
const buildLevelBreakdown = (lines: string[]) => {
const breakdown = {
INFO: 0,
WARN: 0,
ERROR: 0,
OTHER: 0,
};
lines.forEach((line) => {
if (line.includes('[INFO]')) {
breakdown.INFO += 1;
} else if (line.includes('[WARN]')) {
breakdown.WARN += 1;
} else if (line.includes('[ERROR]')) {
breakdown.ERROR += 1;
} else {
breakdown.OTHER += 1;
}
});
return breakdown;
};
export const buildAppLogSnapshot = (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 levelBreakdown = buildLevelBreakdown(lines);
const keyword = String(data.keyword || params.keyword || '').trim();
const requestedLineLimit = normalizeAppLogLimit(data.requestedLineLimit ?? params.lineLimit);
return {
logPath: String(data.logPath || ''),
keyword,
requestedLineLimit,
returnedLineCount: lines.length,
fileWindowTruncated: data.fileWindowTruncated === true,
matchedLinesTruncated: data.matchedLinesTruncated === true,
levelBreakdown,
hasWarnings: levelBreakdown.WARN > 0,
hasErrors: levelBreakdown.ERROR > 0,
lines,
message: lines.length > 0
? ''
: keyword
? `最近日志里没有匹配关键词“${keyword}”的记录`
: '最近日志里暂无可读记录',
};
};

View File

@@ -0,0 +1,53 @@
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_app_logs', () => {
it('returns the recent app-log snapshot so the model can diagnose startup and connection failures from real logs', async () => {
const result = await executeLocalAIToolCall({
toolCall: buildToolCall('inspect_app_logs', {
keyword: 'mysql',
lineLimit: 20,
}),
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: 20,
fileWindowTruncated: false,
matchedLinesTruncated: false,
lines: [
'2026/06/09 10:00:02.000000 [ERROR] mysql dial failed: connect timeout',
],
},
}),
},
});
expect(result.success).toBe(true);
expect(result.content).toContain('"logPath":"C:/Users/demo/.GoNavi/Logs/gonavi.log"');
expect(result.content).toContain('"keyword":"mysql"');
expect(result.content).toContain('"ERROR":1');
expect(result.content).toContain('connect timeout');
});
});

View File

@@ -0,0 +1,52 @@
import { describe, expect, it, vi } from 'vitest';
import type { AIToolCall } from '../../types';
import { executeLocalAIToolCall } from './aiLocalToolExecutor';
import {
cloneShortcutOptions,
DEFAULT_SHORTCUT_OPTIONS,
} from '../../utils/shortcuts';
const buildToolCall = (
name: string,
args: Record<string, unknown>,
): AIToolCall => ({
id: `call-${name}`,
type: 'function',
function: {
name,
arguments: JSON.stringify(args),
},
});
describe('aiLocalToolExecutor inspect_shortcuts', () => {
it('returns the real shortcut snapshot so the model can answer Win/Mac shortcut questions from state', async () => {
const shortcutOptions = cloneShortcutOptions(DEFAULT_SHORTCUT_OPTIONS);
shortcutOptions.toggleQueryResultsPanel.windows = {
combo: 'Ctrl+Shift+Y',
enabled: true,
};
const result = await executeLocalAIToolCall({
toolCall: buildToolCall('inspect_shortcuts', {
keyword: '结果区',
}),
connections: [],
mcpTools: [],
toolContextMap: new Map(),
runtime: {
getDatabases: vi.fn(),
getTables: vi.fn(),
getShortcutOptions: vi.fn().mockResolvedValue(shortcutOptions),
getShortcutPlatform: vi.fn().mockResolvedValue('windows'),
},
});
expect(result.success).toBe(true);
expect(result.content).toContain('"currentPlatform":"windows"');
expect(result.content).toContain('"action":"toggleQueryResultsPanel"');
expect(result.content).toContain('"combo":"Ctrl+Shift+Y"');
expect(result.content).toContain('"defaultCombo":"Ctrl+Shift+M"');
expect(result.content).toContain('"isCustomized":true');
});
});

View File

@@ -1,4 +1,7 @@
import { DBGetAllColumns, DBGetDatabases, DBGetTables, ReadSQLFile } from '../../../wailsjs/go/app/App';
import { DBGetAllColumns, DBGetDatabases, DBGetTables, ReadAppLogTail, ReadSQLFile } from '../../../wailsjs/go/app/App';
import { useStore } from '../../store';
import { isMacLikePlatform } from '../../utils/appearance';
import { getShortcutPlatform } from '../../utils/shortcuts';
import type { AISnapshotInspectionRuntime } from './aiSnapshotInspectionToolTypes';
@@ -12,6 +15,7 @@ export interface AILocalToolRuntime extends AISnapshotInspectionRuntime {
getDatabases: (config: any) => Promise<any>;
getTables: (config: any, dbName: string) => Promise<any>;
getAllColumns: (config: any, dbName: string) => Promise<any>;
readAppLogTail: (lineLimit: number, keyword: string) => Promise<any>;
readSQLFile: (filePath: string) => Promise<any>;
getColumns: (config: any, dbName: string, tableName: string) => Promise<any>;
getIndexes: (config: any, dbName: string, tableName: string) => Promise<any>;
@@ -29,6 +33,7 @@ export const buildDefaultLocalToolRuntime = (): AILocalToolRuntime => ({
getDatabases: DBGetDatabases,
getTables: DBGetTables,
getAllColumns: DBGetAllColumns,
readAppLogTail: (lineLimit, keyword) => ReadAppLogTail(lineLimit, String(keyword || '')),
readSQLFile: ReadSQLFile,
getColumns: async (config, dbName, tableName) => {
const mod = await import('../../../wailsjs/go/app/App');
@@ -100,4 +105,6 @@ export const buildDefaultLocalToolRuntime = (): AILocalToolRuntime => ({
}
return service.AIGetMCPClientInstallStatuses();
},
getShortcutOptions: async () => useStore.getState().shortcutOptions,
getShortcutPlatform: async () => getShortcutPlatform(isMacLikePlatform()),
});

View File

@@ -0,0 +1,47 @@
import { describe, expect, it } from 'vitest';
import { buildShortcutSnapshot } from './aiShortcutInsights';
import {
cloneShortcutOptions,
DEFAULT_SHORTCUT_OPTIONS,
} from '../../utils/shortcuts';
describe('aiShortcutInsights', () => {
it('returns current-platform and cross-platform shortcut bindings with customization markers', () => {
const shortcutOptions = cloneShortcutOptions(DEFAULT_SHORTCUT_OPTIONS);
shortcutOptions.toggleQueryResultsPanel.windows = {
combo: 'Ctrl+Shift+Y',
enabled: true,
};
const snapshot = buildShortcutSnapshot({
shortcutOptions,
currentPlatform: 'windows',
});
const resultPanelShortcut = snapshot.actions.find(
(item) => item?.action === 'toggleQueryResultsPanel',
);
expect(snapshot.currentPlatform).toBe('windows');
expect(snapshot.totalActionCount).toBeGreaterThan(10);
expect(resultPanelShortcut?.currentPlatformBinding.combo).toBe('Ctrl+Shift+Y');
expect(resultPanelShortcut?.currentPlatformBinding.isCustomized).toBe(true);
expect(resultPanelShortcut?.platforms?.mac.combo).toBe('Meta+Shift+M');
});
it('supports filtering by action key or shortcut-related keywords', () => {
const byAction = buildShortcutSnapshot({
currentPlatform: 'windows',
action: 'toggleQueryResultsPanel',
});
const byKeyword = buildShortcutSnapshot({
currentPlatform: 'windows',
keyword: '结果区',
});
expect(byAction.matchedActionCount).toBe(1);
expect(byAction.actions[0]?.action).toBe('toggleQueryResultsPanel');
expect(byKeyword.actions.some((item) => item?.action === 'toggleQueryResultsPanel')).toBe(true);
});
});

View File

@@ -0,0 +1,151 @@
import { isMacLikePlatform } from "../../utils/appearance";
import {
DEFAULT_SHORTCUT_OPTIONS,
SHORTCUT_ACTION_META,
SHORTCUT_ACTION_ORDER,
getShortcutDisplayLabel,
getShortcutPlatform,
resolveShortcutBinding,
type ShortcutAction,
type ShortcutOptions,
type ShortcutPlatform,
} from "../../utils/shortcuts";
interface BuildShortcutSnapshotOptions {
shortcutOptions?: Partial<ShortcutOptions> | null;
currentPlatform?: ShortcutPlatform;
action?: string;
keyword?: string;
includeDisabled?: boolean;
includeAllPlatforms?: boolean;
}
interface ShortcutBindingSnapshot {
platform: ShortcutPlatform;
combo: string;
display: string;
enabled: boolean;
defaultCombo: string;
defaultDisplay: string;
defaultEnabled: boolean;
isCustomized: boolean;
}
const normalizeText = (value: unknown): string =>
String(value || "").trim().toLowerCase();
const buildShortcutBindingSnapshot = (
shortcutOptions: Partial<ShortcutOptions> | null | undefined,
action: ShortcutAction,
platform: ShortcutPlatform,
): ShortcutBindingSnapshot => {
const current = resolveShortcutBinding(shortcutOptions, action, platform);
const defaults = resolveShortcutBinding(DEFAULT_SHORTCUT_OPTIONS, action, platform);
return {
platform,
combo: current.combo,
display: current.enabled ? getShortcutDisplayLabel(current.combo, platform) : "-",
enabled: current.enabled !== false,
defaultCombo: defaults.combo,
defaultDisplay: defaults.enabled ? getShortcutDisplayLabel(defaults.combo, platform) : "-",
defaultEnabled: defaults.enabled !== false,
isCustomized:
current.combo !== defaults.combo || current.enabled !== defaults.enabled,
};
};
const matchesActionFilter = (
action: ShortcutAction,
filter: string,
): boolean => !filter || normalizeText(action) === filter;
const matchesKeywordFilter = (
searchText: string,
filter: string,
): boolean => !filter || searchText.includes(filter);
export const buildShortcutSnapshot = ({
shortcutOptions,
currentPlatform = getShortcutPlatform(isMacLikePlatform()),
action,
keyword,
includeDisabled = true,
includeAllPlatforms = true,
}: BuildShortcutSnapshotOptions) => {
const normalizedAction = normalizeText(action);
const normalizedKeyword = normalizeText(keyword);
const actions = SHORTCUT_ACTION_ORDER
.map((shortcutAction) => {
const meta = SHORTCUT_ACTION_META[shortcutAction];
const windowsBinding = buildShortcutBindingSnapshot(
shortcutOptions,
shortcutAction,
"windows",
);
const macBinding = buildShortcutBindingSnapshot(
shortcutOptions,
shortcutAction,
"mac",
);
const currentBinding =
currentPlatform === "mac" ? macBinding : windowsBinding;
if (!includeDisabled && !currentBinding.enabled) {
return null;
}
if (!matchesActionFilter(shortcutAction, normalizedAction)) {
return null;
}
const searchText = normalizeText([
shortcutAction,
meta.label,
meta.description,
meta.scope || "global",
currentBinding.combo,
currentBinding.defaultCombo,
windowsBinding.combo,
windowsBinding.defaultCombo,
macBinding.combo,
macBinding.defaultCombo,
].join(" "));
if (!matchesKeywordFilter(searchText, normalizedKeyword)) {
return null;
}
return {
action: shortcutAction,
label: meta.label,
description: meta.description,
scope: meta.scope || "global",
allowInEditable: meta.allowInEditable === true,
allowWithoutModifier: meta.allowWithoutModifier === true,
requiredKey: meta.requiredKey || null,
disallowShift: meta.disallowShift === true,
platformOnly: meta.platformOnly || null,
currentPlatformBinding: currentBinding,
platforms: includeAllPlatforms
? {
windows: windowsBinding,
mac: macBinding,
}
: undefined,
};
})
.filter((item): item is NonNullable<typeof item> => Boolean(item));
return {
currentPlatform,
filters: {
action: normalizedAction || null,
keyword: normalizedKeyword || null,
includeDisabled,
includeAllPlatforms,
},
totalActionCount: SHORTCUT_ACTION_ORDER.length,
matchedActionCount: actions.length,
knownActions: SHORTCUT_ACTION_ORDER,
actions,
};
};

View File

@@ -14,6 +14,8 @@ 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 === '/shortcuts')).toBe(true);
expect(commands.some((command) => command.cmd === '/applog')).toBe(true);
});
it('supports filtering by chinese keywords in addition to command prefix', () => {
@@ -23,6 +25,16 @@ describe('aiSlashCommands', () => {
expect(commands.map((command) => command.cmd)).not.toContain('/mcpadd');
});
it('supports filtering shortcut diagnostics by chinese keyword and command prefix', () => {
expect(filterAISlashCommands('快捷键').map((command) => command.cmd)).toContain('/shortcuts');
expect(filterAISlashCommands('/sho').map((command) => command.cmd)).toContain('/shortcuts');
});
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');
});
it('groups commands by configured category order', () => {
const groups = groupAISlashCommands(filterAISlashCommands('/'));
@@ -38,5 +50,6 @@ describe('aiSlashCommands', () => {
expect(featured).toContain('/health');
expect(featured).toContain('/mcp');
expect(featured).toContain('/mcpadd');
expect(featured).not.toContain('/shortcuts');
});
});

View File

@@ -50,6 +50,8 @@ 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: '/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: '/safety', label: '🛡️ 查看写入安全', desc: '确认只读/写入边界和 allowMutating', prompt: '请先调用 inspect_ai_safety告诉我当前 AI 和 GoNavi MCP 的写入边界、是否只读,以及 execute_sql 是否需要 allowMutating。', category: 'diagnose', keywords: ['安全', '只读', 'allowmutating', 'ddl', 'dml'] },
{ cmd: '/activity', label: '🕘 最近 SQL 活动', desc: '总结最近执行、报错和热点', prompt: '请先调用 inspect_recent_sql_activity帮我总结最近 SQL 活动、错误热点和主要读写类型。', category: 'diagnose', keywords: ['activity', 'sql日志', '最近执行', '报错'] },
];

View File

@@ -22,6 +22,7 @@ import {
import { buildSavedConnectionsSnapshot } from './aiSavedConnectionInsights';
import { buildExternalSQLFileSnapshot } from './aiExternalSqlFileInsights';
import { buildExternalSQLDirectoriesSnapshot } from './aiExternalSqlInsights';
import { buildAppLogSnapshot } from './aiAppLogInsights';
import { findBestMatchingExternalSQLDirectory } from './aiExternalSqlPathUtils';
import {
buildRecentSqlActivitySnapshot,
@@ -31,6 +32,7 @@ import {
buildActiveTabSnapshot,
buildWorkspaceTabsSnapshot,
} from './aiWorkspaceInsights';
import { buildShortcutSnapshot } from './aiShortcutInsights';
import { executeAIConfigSnapshotToolCall } from './aiSnapshotInspectionAIConfigToolExecutor';
import type {
AISnapshotInspectionRuntime,
@@ -247,6 +249,25 @@ export async function executeSnapshotInspectionToolCall(
})),
success: true,
};
case 'inspect_app_logs': {
const readResult = typeof runtime?.readAppLogTail === 'function'
? await runtime.readAppLogTail(Number(args.lineLimit) || 80, String(args.keyword || ''))
: { success: false, message: '当前环境暂不支持读取 GoNavi 应用日志' };
if (!readResult?.success) {
return {
content: `读取 GoNavi 应用日志失败: ${readResult?.message || '未知错误'}`,
success: false,
};
}
return {
content: JSON.stringify(buildAppLogSnapshot({
readResult,
keyword: args.keyword,
lineLimit: args.lineLimit,
})),
success: true,
};
}
case 'inspect_saved_queries':
return {
content: JSON.stringify(buildSavedQueriesSnapshot({
@@ -270,6 +291,27 @@ export async function executeSnapshotInspectionToolCall(
})),
success: true,
};
case 'inspect_shortcuts': {
const [shortcutOptions, currentPlatform] = await Promise.all([
typeof runtime?.getShortcutOptions === 'function'
? runtime.getShortcutOptions()
: Promise.resolve(undefined),
typeof runtime?.getShortcutPlatform === 'function'
? runtime.getShortcutPlatform()
: Promise.resolve(undefined),
]);
return {
content: JSON.stringify(buildShortcutSnapshot({
shortcutOptions,
currentPlatform,
action: args.action,
keyword: args.keyword,
includeDisabled: args.includeDisabled !== false,
includeAllPlatforms: args.includeAllPlatforms !== false,
})),
success: true,
};
}
default:
return null;
}
@@ -286,8 +328,10 @@ export async function executeSnapshotInspectionToolCall(
inspect_ai_context: '读取当前 AI 上下文失败',
inspect_recent_sql_logs: '获取最近 SQL 日志失败',
inspect_recent_sql_activity: '汇总最近 SQL 活动失败',
inspect_app_logs: '读取 GoNavi 应用日志失败',
inspect_saved_queries: '读取已保存查询失败',
inspect_sql_snippets: '读取 SQL 片段失败',
inspect_shortcuts: '读取快捷键配置失败',
}[toolName] || '读取本地探针快照失败';
return {
content: `${label}: ${error?.message || error}`,

View File

@@ -4,6 +4,10 @@ import type {
AIProviderConfig,
AISafetyLevel,
} from '../../types';
import type {
ShortcutOptions,
ShortcutPlatform,
} from '../../utils/shortcuts';
export interface AISnapshotInspectionRuntimeState {
providers?: AIProviderConfig[];
@@ -16,6 +20,9 @@ export interface AISnapshotInspectionRuntime {
getAIRuntimeState?: () => Promise<AISnapshotInspectionRuntimeState | undefined>;
getMCPServers?: () => Promise<AIMCPServerConfig[] | undefined>;
getMCPClientInstallStatuses?: () => Promise<AIMCPClientInstallStatus[] | undefined>;
getShortcutOptions?: () => Promise<ShortcutOptions | undefined>;
getShortcutPlatform?: () => Promise<ShortcutPlatform | undefined>;
readAppLogTail?: (lineLimit: number, keyword: string) => Promise<any>;
readSQLFile?: (filePath: string) => Promise<any>;
}

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_saved_queries', 'inspect_ai_sessions', 'inspect_sql_snippets', '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_app_logs', 'inspect_saved_queries', 'inspect_ai_sessions', 'inspect_sql_snippets', 'inspect_shortcuts', 'get_columns'],
skills,
userPromptSettings,
});
@@ -90,9 +90,11 @@ 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_app_logs 读取真实应用日志尾部');
expect(joined).toContain('inspect_saved_queries');
expect(joined).toContain('inspect_ai_sessions');
expect(joined).toContain('inspect_sql_snippets');
expect(joined).toContain('inspect_shortcuts 读取真实快捷键配置和平台差异');
expect(joined).toContain('当前连接');
expect(joined).toContain('以下是当前用户的自定义补充提示词(全局)');
expect(joined).toContain('以下是当前用户的自定义补充提示词(数据库会话)');

View File

@@ -199,6 +199,32 @@ const appendAIGuidanceInspectionGuidance = (
});
};
const appendShortcutInspectionGuidance = (
messages: AISystemContextMessage[],
availableToolNames: string[],
) => {
if (!availableToolNames.includes('inspect_shortcuts')) {
return;
}
messages.push({
role: 'system',
content: '如果用户提到“快捷键是什么”“Win 和 Mac 分别怎么按”“结果区/AI 面板/执行 SQL 的组合键”“我是不是改过默认快捷键”,优先调用 inspect_shortcuts 读取真实快捷键配置和平台差异,不要凭记忆回答默认值。',
});
};
const appendAppLogInspectionGuidance = (
messages: AISystemContextMessage[],
availableToolNames: string[],
) => {
if (!availableToolNames.includes('inspect_app_logs')) {
return;
}
messages.push({
role: 'system',
content: '如果用户提到“gonavi.log”“最近日志”“启动报错”“MCP 拉不起来”“数据库连接为什么失败”,优先调用 inspect_app_logs 读取真实应用日志尾部;必要时再结合关键词继续筛选,不要只凭弹窗或提示文案猜测。',
});
};
const appendConnectionCapabilityInspectionGuidance = (
messages: AISystemContextMessage[],
availableToolNames: string[],
@@ -438,6 +464,8 @@ SELECT * FROM users WHERE status = 1;
appendMCPSetupInspectionGuidance(systemMessages, availableToolNames);
appendMCPAuthoringInspectionGuidance(systemMessages, availableToolNames);
appendAIGuidanceInspectionGuidance(systemMessages, availableToolNames);
appendShortcutInspectionGuidance(systemMessages, availableToolNames);
appendAppLogInspectionGuidance(systemMessages, availableToolNames);
if (availableToolNames.includes('inspect_current_connection')) {
systemMessages.push({
role: 'system',

View File

@@ -51,8 +51,10 @@ const TOOL_ACTION_LABELS: Record<string, string> = {
inspect_workspace_tabs: '盘点当前工作区页签',
inspect_recent_sql_logs: '回看最近 SQL 执行日志',
inspect_recent_sql_activity: '总结最近 SQL 活动',
inspect_app_logs: '回看 GoNavi 应用日志',
inspect_saved_queries: '检索本地已保存查询',
inspect_sql_snippets: '读取 SQL 片段模板',
inspect_shortcuts: '读取当前快捷键配置',
preview_table_rows: '预览真实样例数据',
execute_sql: '执行只读 SQL 验证',
};

View File

@@ -251,6 +251,37 @@ if (typeof window !== 'undefined' && (!(window as any).go?.app?.App || !(window
SelectSQLDirectory: async (currentPath: string) => ({ success: false, message: currentPath ? '已取消' : '已取消' }),
ListSQLDirectory: async () => ({ success: true, data: [] }),
ReadSQLFile: async () => ({ success: false, message: '已取消' }),
ReadAppLogTail: async (lineLimit: number, keyword: string) => {
const allLines = [
'2026/06/09 10:10:00.000000 [INFO] 应用启动完成',
'2026/06/09 10:10:05.000000 [WARN] MCP mock service slow start',
'2026/06/09 10:10:09.000000 [ERROR] MySQL mock dial failed: connect timeout',
];
const normalizedKeyword = String(keyword || '').trim().toLowerCase();
const filtered = normalizedKeyword
? allLines.filter((line) => line.toLowerCase().includes(normalizedKeyword))
: allLines;
const safeLimit = Math.max(1, Math.min(Number(lineLimit) || 80, 200));
const visibleLines = filtered.slice(-safeLimit);
return {
success: true,
data: {
logPath: 'C:/Users/mock/.GoNavi/Logs/gonavi.log',
keyword: String(keyword || ''),
requestedLineLimit: safeLimit,
returnedLineCount: visibleLines.length,
fileWindowTruncated: false,
matchedLinesTruncated: filtered.length > visibleLines.length,
levelBreakdown: {
INFO: visibleLines.filter((line) => line.includes('[INFO]')).length,
WARN: visibleLines.filter((line) => line.includes('[WARN]')).length,
ERROR: visibleLines.filter((line) => line.includes('[ERROR]')).length,
OTHER: visibleLines.filter((line) => !/\[(INFO|WARN|ERROR)\]/.test(line)).length,
},
lines: visibleLines,
},
};
},
CreateSQLFile: async (_directoryPath: string, _name: string) => ({ success: true, data: { filePath: '', name: _name } }),
CreateSQLDirectory: async (directoryPath: string, name: string) => ({ success: true, data: { directoryPath: `${directoryPath}/${name}`, name } }),
DeleteSQLFile: async (_filePath: string) => ({ success: true }),

View File

@@ -0,0 +1,319 @@
import type { AIBuiltinToolInfo } from "./aiBuiltinToolInfo.types";
export const BUILTIN_AI_DATABASE_TOOL_INFO: AIBuiltinToolInfo[] = [
{
name: "get_connections",
icon: "🔗",
desc: "获取所有可用的数据库连接",
detail:
"返回连接 ID、名称、类型 (MySQL/PostgreSQL 等) 和 Host 地址。AI 根据返回信息决定优先探索哪个连接。",
params: "无参数",
tool: {
type: "function",
function: {
name: "get_connections",
description:
"当需要查询、操作数据库但用户没有选择任何连接上下文时获取当前软件中可用的所有数据库连接信息。返回的数据包含连接ID(id)和名称(name)。",
parameters: { type: "object", properties: {} },
},
},
},
{
name: "get_databases",
icon: "🗄️",
desc: "获取指定连接下的所有数据库",
detail: "传入 connectionId返回该连接下的数据库/Schema 名称列表。",
params: "connectionId: 连接 ID",
tool: {
type: "function",
function: {
name: "get_databases",
description: "获取指定连接connectionId下的所有数据库(Database/Schema)名。",
parameters: {
type: "object",
properties: {
connectionId: { type: "string", description: "连接ID (从 get_connections 获取)" },
},
required: ["connectionId"],
},
},
},
},
{
name: "get_tables",
icon: "📋",
desc: "获取指定数据库下的所有表名",
detail:
"传入 connectionId 和 dbName返回表名列表。AI 用它来定位用户提到的目标表。",
params: "connectionId, dbName",
tool: {
type: "function",
function: {
name: "get_tables",
description:
"当已经确定了目标连接和数据库名后,如果用户询问或隐式提到了表但你不知道确切表名,调用此工具获取该数据库下的所有表名列表(只含表名,帮助你推断目标表)。",
parameters: {
type: "object",
properties: {
connectionId: { type: "string", description: "连接ID" },
dbName: { type: "string", description: "数据库名" },
},
required: ["connectionId", "dbName"],
},
},
},
},
{
name: "get_all_columns",
icon: "🧱",
desc: "获取指定数据库下所有表的字段摘要",
detail:
"传入 connectionId 和 dbName返回跨表字段列表表名、字段名、类型、注释。适合用户只知道业务字段、不知道具体在哪张表时快速定位目标表。",
params: "connectionId, dbName",
tool: {
type: "function",
function: {
name: "get_all_columns",
description:
"获取指定数据库下全部表的字段摘要,返回表名、字段名、类型和注释。适用于按字段反查表、跨表梳理相同字段、做数据地图探索。",
parameters: {
type: "object",
properties: {
connectionId: { type: "string", description: "连接ID" },
dbName: { type: "string", description: "数据库名" },
},
required: ["connectionId", "dbName"],
},
},
},
},
{
name: "get_columns",
icon: "🔍",
desc: "获取指定表的字段结构",
detail:
"传入 connectionId、dbName 和 tableName返回每个字段的名称、类型、是否可空、默认值和注释。AI 在生成 SQL 前必须调用此工具确认真实字段名。",
params: "connectionId, dbName, tableName",
tool: {
type: "function",
function: {
name: "get_columns",
description:
"获取指定表的字段列表(字段名、类型、是否可空、默认值、注释等)。在生成 SQL 之前必须先调用此工具确认真实字段名,禁止猜测字段名。",
parameters: {
type: "object",
properties: {
connectionId: { type: "string", description: "连接ID" },
dbName: { type: "string", description: "数据库名" },
tableName: { type: "string", description: "表名" },
},
required: ["connectionId", "dbName", "tableName"],
},
},
},
},
{
name: "get_indexes",
icon: "🧭",
desc: "获取指定表的索引定义",
detail:
"传入 connectionId、dbName 和 tableName返回索引名、索引列、唯一性和索引类型。AI 在做慢 SQL 分析、索引优化和执行计划推断时应优先调用。",
params: "connectionId, dbName, tableName",
tool: {
type: "function",
function: {
name: "get_indexes",
description:
"获取指定表的索引定义,包括索引名、字段顺序、唯一性和索引类型。适用于慢 SQL 分析、索引优化建议和确认现有索引覆盖情况。",
parameters: {
type: "object",
properties: {
connectionId: { type: "string", description: "连接ID" },
dbName: { type: "string", description: "数据库名" },
tableName: { type: "string", description: "表名" },
},
required: ["connectionId", "dbName", "tableName"],
},
},
},
},
{
name: "get_foreign_keys",
icon: "🧬",
desc: "获取指定表的外键关系",
detail:
"传入 connectionId、dbName 和 tableName返回当前表到其他表的外键映射。AI 在推断表关系、生成联表 SQL 和评审数据一致性时可直接使用。",
params: "connectionId, dbName, tableName",
tool: {
type: "function",
function: {
name: "get_foreign_keys",
description:
"获取指定表的外键关系包括本表字段、引用表、引用字段和约束名。适用于联表路径分析、ER 关系梳理和约束检查。",
parameters: {
type: "object",
properties: {
connectionId: { type: "string", description: "连接ID" },
dbName: { type: "string", description: "数据库名" },
tableName: { type: "string", description: "表名" },
},
required: ["connectionId", "dbName", "tableName"],
},
},
},
},
{
name: "get_triggers",
icon: "⏱️",
desc: "获取指定表的触发器定义",
detail:
"传入 connectionId、dbName 和 tableName返回触发器名、触发时机、事件类型和语句体。AI 在分析隐式写入、副作用和审计逻辑时可直接查看。",
params: "connectionId, dbName, tableName",
tool: {
type: "function",
function: {
name: "get_triggers",
description:
"获取指定表的触发器定义,包括触发时机、事件和触发语句。适用于排查隐式数据变更、审计逻辑和表级副作用。",
parameters: {
type: "object",
properties: {
connectionId: { type: "string", description: "连接ID" },
dbName: { type: "string", description: "数据库名" },
tableName: { type: "string", description: "表名" },
},
required: ["connectionId", "dbName", "tableName"],
},
},
},
},
{
name: "get_table_ddl",
icon: "📝",
desc: "获取表的建表语句 (DDL)",
detail:
"传入 connectionId、dbName 和 tableName返回完整的 CREATE TABLE 语句,包含字段定义、索引、约束等信息。",
params: "connectionId, dbName, tableName",
tool: {
type: "function",
function: {
name: "get_table_ddl",
description: "获取指定表的完整建表语句CREATE TABLE DDL包含字段、索引、约束等完整结构信息。",
parameters: {
type: "object",
properties: {
connectionId: { type: "string", description: "连接ID" },
dbName: { type: "string", description: "数据库名" },
tableName: { type: "string", description: "表名" },
},
required: ["connectionId", "dbName", "tableName"],
},
},
},
},
{
name: "preview_table_rows",
icon: "👀",
desc: "抽样预览指定表的前几行数据",
detail:
"传入 connectionId、dbName、tableName 和可选 limit返回该表的前几行真实样例数据。适合先看数据形态、空值分布和枚举值再决定怎么写 SQL。",
params: "connectionId, dbName, tableName, limit?",
tool: {
type: "function",
function: {
name: "preview_table_rows",
description:
"预览指定表的前几行样例数据。适用于快速理解字段取值形态、空值情况、时间格式和状态枚举,减少模型盲写 SQL。",
parameters: {
type: "object",
properties: {
connectionId: { type: "string", description: "连接ID" },
dbName: { type: "string", description: "数据库名" },
tableName: { type: "string", description: "表名" },
limit: { type: "number", description: "可选,预览行数,默认 20最大 100" },
},
required: ["connectionId", "dbName", "tableName"],
},
},
},
},
{
name: "inspect_table_bundle",
icon: "🧰",
desc: "一次抓取指定表的结构快照",
detail:
"传入 connectionId、dbName 和 tableName返回字段、索引、外键、触发器和 DDL还可以附带前几行样例数据。适合在写 SQL、评审表设计或排查副作用前先做完整摸底。",
params: "connectionId, dbName, tableName, includeSampleRows?, sampleLimit?",
tool: {
type: "function",
function: {
name: "inspect_table_bundle",
description:
"一次性获取指定表的结构快照返回字段、索引、外键、触发器、DDL以及可选样例数据。适用于做完整表设计摸底、快速理解表关系和降低模型多次往返调用。",
parameters: {
type: "object",
properties: {
connectionId: { type: "string", description: "连接ID" },
dbName: { type: "string", description: "数据库名" },
tableName: { type: "string", description: "表名" },
includeSampleRows: { type: "boolean", description: "可选,是否附带前几行样例数据" },
sampleLimit: { type: "number", description: "可选,样例行数,默认 10最大 100" },
},
required: ["connectionId", "dbName", "tableName"],
},
},
},
},
{
name: "inspect_database_bundle",
icon: "🗂️",
desc: "一次抓取指定数据库的结构总览",
detail:
"传入 connectionId 和 dbName返回库内表清单、表数量、总字段数以及按表聚合的字段摘要预览。适合刚接手陌生库时先做全局摸底再决定深入哪张表。",
params: "connectionId, dbName, includeColumns?, tableLimit?, perTableColumnLimit?",
tool: {
type: "function",
function: {
name: "inspect_database_bundle",
description:
"一次性获取指定数据库的结构总览,返回表名列表、总字段数,以及按表聚合的字段摘要预览。适用于陌生数据库摸底、做数据地图和快速选择下一步要深入分析的表。",
parameters: {
type: "object",
properties: {
connectionId: { type: "string", description: "连接ID" },
dbName: { type: "string", description: "数据库名" },
includeColumns: { type: "boolean", description: "可选,是否附带按表聚合的字段摘要,默认 true" },
tableLimit: { type: "number", description: "可选,最多返回多少张表,默认 80最大 200" },
perTableColumnLimit: { type: "number", description: "可选,每张表最多返回多少个字段摘要,默认 8最大 30" },
},
required: ["connectionId", "dbName"],
},
},
},
},
{
name: "execute_sql",
icon: "▶️",
desc: "执行 SQL 查询并返回结果",
detail:
"传入 connectionId、dbName 和 sql在目标数据库上执行 SQL 并返回结果(最多 50 行)。受安全级别控制,只读模式下仅允许 SELECT/SHOW/DESCRIBE。",
params: "connectionId, dbName, sql",
tool: {
type: "function",
function: {
name: "execute_sql",
description:
"在指定连接和数据库上执行 SQL 查询并返回结果。受安全级别控制,只读模式下只能执行 SELECT/SHOW/DESCRIBE 等查询操作。结果最多返回 50 行。",
parameters: {
type: "object",
properties: {
connectionId: { type: "string", description: "连接ID" },
dbName: { type: "string", description: "数据库名" },
sql: { type: "string", description: "要执行的 SQL 语句" },
},
required: ["connectionId", "dbName", "sql"],
},
},
},
},
];

View File

@@ -0,0 +1,503 @@
import type { AIBuiltinToolInfo } from "./aiBuiltinToolInfo.types";
export const BUILTIN_AI_INSPECTION_TOOL_INFO: AIBuiltinToolInfo[] = [
{
name: "inspect_ai_setup_health",
icon: "🩺",
desc: "一键体检当前 AI 配置健康度",
detail:
"汇总当前 AI 供应商、聊天发送前置、MCP 服务与外部客户端接入、提示词与 Skills、上下文挂载情况并给出阻塞项、告警项和下一步建议。适合用户说“AI 为什么不好用”“帮我看下 AI 整体有没有问题”“现在这套 AI 配置还缺什么”时先做一次总览诊断。",
params: "无参数",
tool: {
type: "function",
function: {
name: "inspect_ai_setup_health",
description:
"体检当前 AI 配置健康度返回供应商、模型、聊天发送前置、MCP 接入、提示词与 Skills、表结构上下文挂载等整体快照并给出阻塞项、建议项和下一步动作。适用于用户提到 AI 为什么不好用、当前 AI 配置哪里还缺、是否已经能稳定工作时,优先读取这份总览诊断,不要拆成多次猜测。",
parameters: { type: "object", properties: {} },
},
},
},
{
name: "inspect_ai_runtime",
icon: "🎛️",
desc: "查看当前 AI 自身运行状态",
detail:
"返回当前启用的模型供应商、模型名、安全级别、上下文级别、启用的 Skills以及当前已暴露的内置工具和 MCP 工具。适合用户问“你现在能调用什么”“当前用的哪个模型”“为什么不能执行写操作”时,先读真实运行状态再回答。",
params: "无参数",
tool: {
type: "function",
function: {
name: "inspect_ai_runtime",
description:
"读取当前 AI 运行时快照,包括当前供应商、模型、安全级别、上下文级别、启用的 Skills、当前可用的内置工具与 MCP 工具。适用于用户询问当前 AI 能力边界、当前使用哪个模型、为什么不能执行某些操作时,先读取真实运行状态,避免模型猜测。",
parameters: { type: "object", properties: {} },
},
},
},
{
name: "inspect_ai_safety",
icon: "🛡️",
desc: "查看当前 AI 写入安全边界",
detail:
"返回当前 AI 安全级别对应的 SQL 允许范围、非只读语句是否仍需确认 / allowMutating以及当前活动连接、页签或 JVM 诊断权限是否还叠加了只读限制。适合用户问“为什么现在不能写”“DDL 能不能执行”“allowMutating 要不要传”时先读真实边界。",
params: "无参数",
tool: {
type: "function",
function: {
name: "inspect_ai_safety",
description:
"读取当前 AI 安全边界快照,包括当前安全级别允许的 SQL 范围、非查询语句的确认要求、MCP execute_sql 对 allowMutating 的要求,以及当前活动连接、结果页签或 JVM 诊断权限是否额外处于只读限制。适用于用户提到为什么现在不能写、当前是不是只读、DDL 能不能执行、allowMutating 是否必须传时,先读取真实边界再回答。",
parameters: { type: "object", properties: {} },
},
},
},
{
name: "inspect_ai_providers",
icon: "🪪",
desc: "查看当前 AI 供应商与模型配置",
detail:
"返回当前配置了哪些 AI 供应商、哪个正在生效、各自的 baseUrl、已选模型、声明模型列表、密钥是否存在、自定义请求头 key以及缺少密钥/模型/地址等待检查项。适合用户问“为什么没有模型”“API Key 有没有配”“当前到底配了哪些供应商”时先读真实配置。",
params: "无参数",
tool: {
type: "function",
function: {
name: "inspect_ai_providers",
description:
"读取当前 AI 供应商配置快照,包括供应商列表、活动供应商、接口地址、已选模型、声明模型列表、是否存在密钥、自定义请求头 key以及缺少密钥/模型/地址等待检查项。适用于用户提到当前供应商、模型列表为空、API Key 是否配置、为什么 AI 不能正常发起请求时,先读取真实配置再解释。",
parameters: { type: "object", properties: {} },
},
},
},
{
name: "inspect_ai_chat_readiness",
icon: "🚦",
desc: "查看当前 AI 聊天是否具备发送条件",
detail:
"返回当前聊天输入区是否已经具备发送条件,包括有没有活动供应商、当前供应商是否缺密钥或接口地址、是否已选模型、当前连接/表结构上下文是否已挂载,以及下一步建议动作。适合用户问“为什么现在不能发送”“输入框到底缺什么配置”“当前 AI 聊天准备好了没有”时先读真实状态。",
params: "无参数",
tool: {
type: "function",
function: {
name: "inspect_ai_chat_readiness",
description:
"读取当前 AI 聊天输入区的发送前置状态,包括活动供应商、密钥和接口地址是否完整、是否已选模型、当前连接上下文和已挂载表结构数量,以及建议的下一步动作。适用于用户提到为什么现在不能发送、为什么输入区还没准备好、当前到底缺什么配置时,先读取真实状态再回答。",
parameters: { type: "object", properties: {} },
},
},
},
{
name: "inspect_mcp_setup",
icon: "🪛",
desc: "查看当前 MCP 配置与外部接入状态",
detail:
"返回当前本地配置了哪些 MCP 服务、哪些已启用、每个服务声明了什么启动命令,以及 Claude Code / Codex 这类外部客户端的写入状态与命令检测结果。适合用户问“我现在配了哪些 MCP”“为什么外部客户端还用不了”“MCP 到底写没写进去”时先读真实状态。",
params: "无参数",
tool: {
type: "function",
function: {
name: "inspect_mcp_setup",
description:
"读取当前本地 MCP 配置快照,包括 MCP 服务列表、启用状态、启动命令、环境变量 key、已发现工具以及外部客户端的 GoNavi MCP 写入状态与本机 CLI 检测结果。适用于用户提到 MCP 服务配置、Claude/Codex 是否已接入、为什么外部客户端用不了、当前到底启用了哪些 MCP 时,先读取真实配置再回答。",
parameters: { type: "object", properties: {} },
},
},
},
{
name: "inspect_mcp_authoring_guide",
icon: "🧭",
desc: "查看新增 MCP 的填写指引",
detail:
"返回新增 MCP 表单里各字段的作用、推荐填写顺序、完整命令自动拆分规则,以及 Node / uvx / Python / EXE 模板样例。适合用户问“command/args/env 到底怎么填”“给我一个 node / uvx / python 示例”“为什么启动命令不能整行填”时,先读这份真实接入指引。",
params: "无参数",
tool: {
type: "function",
function: {
name: "inspect_mcp_authoring_guide",
description:
"读取 GoNavi 当前内置的 MCP 新增指引,包括推荐填写顺序、字段作用、常见命令示例、完整命令自动拆分规则,以及 Node / uvx / Python / EXE 模板样例。适用于用户提到新增 MCP 不知道 command、args、env、timeout 怎么填,或想要一个最接近的模板时,先读取这份真实前端接入指南,不要凭记忆口述。",
parameters: { type: "object", properties: {} },
},
},
},
{
name: "inspect_ai_guidance",
icon: "🧠",
desc: "查看当前 AI 提示词与 Skills 配置",
detail:
"返回当前用户自定义的全局/数据库/JVM 提示词,以及当前启用的 Skills、作用域、依赖工具和 skill prompt 内容。适合用户问“你现在到底带了哪些提示词”“为什么你会这样回答”“当前有哪些 Skills 在生效”时先读真实配置。",
params: "无参数",
tool: {
type: "function",
function: {
name: "inspect_ai_guidance",
description:
"读取当前 AI 的提示与技能配置快照,包括用户自定义提示词、当前启用的 Skills、作用域、依赖工具和各自的 system prompt。适用于用户提到当前提示词、当前 Skill、为什么 AI 当前会这样回答、当前有哪些规则在生效时,先读取真实配置再解释。",
parameters: { type: "object", properties: {} },
},
},
},
{
name: "inspect_ai_context",
icon: "🧷",
desc: "查看当前 AI 已关联的表结构上下文",
detail:
"返回当前对话已经挂载到 AI 上下文里的表清单、所属连接与数据库,以及每张表的 DDL 预览。适合用户说“看看我现在带了哪些表结构”“当前 AI 上下文是什么”时,先读取真实挂载状态再继续分析。",
params: "includeDDL?(默认 false), ddlLimit?(默认 4000)",
tool: {
type: "function",
function: {
name: "inspect_ai_context",
description:
"读取当前对话已经关联到 AI 上下文里的表结构快照,包括连接、数据库、表名,以及可选的 DDL 内容。适用于用户提到当前 AI 上下文、当前关联表、当前挂载的表结构时,先读取真实状态,避免模型凭记忆复述。",
parameters: {
type: "object",
properties: {
includeDDL: { type: "boolean", description: "可选,是否附带每张表的 DDL 内容,默认 false" },
ddlLimit: { type: "number", description: "可选DDL 截断长度,默认 4000最大 12000" },
},
},
},
},
},
{
name: "inspect_current_connection",
icon: "🛰️",
desc: "查看当前活动连接/数据源摘要",
detail:
"返回当前活动连接的类型、地址、端口、当前数据库、是否启用 SSH/代理/HTTP 隧道,以及当前活动页签绑定的表信息。适合用户问“我现在连的是哪个库”“这个连接走没走 SSH”“当前数据源是什么类型”时先读取真实连接状态。",
params: "无参数",
tool: {
type: "function",
function: {
name: "inspect_current_connection",
description:
"读取当前活动连接或当前页签对应数据源的真实摘要包括连接类型、地址、端口、当前数据库、SSH/代理/HTTP 隧道状态,以及当前页签绑定的表上下文。适用于用户提到当前连接、当前数据源、当前库地址、是否走 SSH、当前连的是哪种数据库时先读取真实界面上下文避免模型猜测。",
parameters: { type: "object", properties: {} },
},
},
},
{
name: "inspect_connection_capabilities",
icon: "🧱",
desc: "查看当前连接支持哪些前端能力",
detail:
"返回当前或指定连接的数据源能力矩阵包括是否支持查询编辑器、SQL 导出、复制 INSERT、新建/重命名/删除数据库、结果是否强制只读,以及是否倾向手动总数或近似计数。适合用户问“为什么这里不能建库/删库”“这个数据源为什么结果不能编辑”“这个类型支持哪些操作”时,先读取真实能力边界。",
params: "connectionId?(默认取当前活动连接)",
tool: {
type: "function",
function: {
name: "inspect_connection_capabilities",
description:
"读取当前活动连接或指定 saved connection 的前端能力矩阵包括是否支持查询编辑器、SQL 导出、复制 INSERT、新建/重命名/删除数据库、结果是否强制只读,以及是否适合手动总数或近似计数。适用于用户提到当前连接为什么不能建库、为什么结果集不能编辑、某种数据库类型到底支持哪些前端动作时,先读取真实能力配置,避免模型凭经验猜测。",
parameters: {
type: "object",
properties: {
connectionId: { type: "string", description: "可选,指定要查看的连接 ID不传时默认读取当前活动连接" },
},
},
},
},
},
{
name: "inspect_saved_connections",
icon: "🧭",
desc: "查看本地已保存连接清单",
detail:
"可按关键词或数据库类型过滤返回本地保存的数据源列表、连接类型分布以及每条连接的地址、当前库、SSH/代理/HTTP 隧道状态。适合用户问“我本地存了哪些连接”“帮我找 mysql / postgres 连接”“哪条连接配置了 SSH”时先读真实本地连接资产。",
params: "keyword?, type?, limit?",
tool: {
type: "function",
function: {
name: "inspect_saved_connections",
description:
"读取本地已保存连接清单可按关键词和数据库类型过滤并返回每条连接的类型、地址、当前库、SSH/代理/HTTP 隧道等摘要。适用于用户提到本地保存了哪些连接、要找哪条 mysql/postgres 连接、哪条连接启用了 SSH 或代理时,先读取真实本地连接资产再回答。",
parameters: {
type: "object",
properties: {
keyword: { type: "string", description: "可选按连接名、ID、类型、主机、数据库名或 SSH/代理地址做关键词筛选" },
type: { type: "string", description: "可选,只看某种数据库类型,例如 mysql、postgres、redis、mongodb" },
limit: { type: "number", description: "可选,最多返回多少条连接,默认 20最大 100" },
},
},
},
},
},
{
name: "inspect_external_sql_directories",
icon: "🗂️",
desc: "查看本地外部 SQL 目录资产",
detail:
"可按关键词、连接或数据库过滤,返回本地配置的外部 SQL 目录、目录路径、绑定连接/数据库,以及当前是否已经打开这些目录里的 SQL 文件。适合用户提到“外部 SQL 目录”“某个脚本在哪个目录”“现在打开的 SQL 文件来自哪个外部目录”时,先读真实资产。",
params: "keyword?, connectionId?, dbName?, limit?",
tool: {
type: "function",
function: {
name: "inspect_external_sql_directories",
description:
"读取本地配置的外部 SQL 目录清单,可按关键词、连接和数据库过滤,并返回目录路径、绑定连接/数据库,以及当前打开的外部 SQL 文件页签摘要。适用于用户提到外部 SQL 目录、某个 SQL 文件放在哪、当前打开的脚本来自哪个目录时,先读取真实本地资产再回答。",
parameters: {
type: "object",
properties: {
keyword: { type: "string", description: "可选,按目录名、路径、连接名或数据库名做关键词筛选" },
connectionId: { type: "string", description: "可选,只看绑定到某个连接的外部 SQL 目录" },
dbName: { type: "string", description: "可选,只看绑定到某个数据库的外部 SQL 目录" },
limit: { type: "number", description: "可选,最多返回多少条目录,默认 20最大 100" },
},
},
},
},
},
{
name: "inspect_external_sql_file",
icon: "📄",
desc: "读取外部 SQL 文件内容",
detail:
"传入具体 filePath读取已配置外部 SQL 目录中的 SQL 文件内容,并返回所属目录、绑定连接/数据库、是否已有打开页签,以及截断后的正文预览。适合用户提到“看一下这个目录里的某个脚本”“帮我解释 report.sql 在写什么”时,先读取真实文件内容再分析。",
params: "filePath, previewCharLimit?",
tool: {
type: "function",
function: {
name: "inspect_external_sql_file",
description:
"读取指定外部 SQL 文件的内容预览,仅用于已配置外部 SQL 目录中的 SQL 文件。返回文件路径、所属目录、绑定连接/数据库、是否已在工作区打开,以及截断后的正文内容。适用于用户提到某个目录中的具体 SQL 脚本、想让 AI 直接解释脚本逻辑、或想确认某个外部 SQL 文件内容时,先读真实文件再回答。",
parameters: {
type: "object",
properties: {
filePath: { type: "string", description: "必填,要读取的 SQL 文件绝对路径,通常先通过 inspect_external_sql_directories 找到" },
previewCharLimit: { type: "number", description: "可选,正文预览最多返回多少字符,默认 12000最大 40000" },
},
required: ["filePath"],
},
},
},
},
{
name: "inspect_active_tab",
icon: "📍",
desc: "查看当前活动页签上下文",
detail:
"返回当前活动页签的类型、连接、数据库、表名,以及当前 SQL / 命令页签里的草稿内容(超长会截断)。适合用户说“看我当前这条 SQL”“优化这个编辑器里的语句”时先让 AI 直接读取当前工作区上下文。",
params: "includeContent?(默认 true)",
tool: {
type: "function",
function: {
name: "inspect_active_tab",
description:
"获取当前活动页签的上下文快照,包括页签类型、连接、数据库、表名,以及当前 SQL / 命令页签里的草稿内容。适用于用户提到当前页签、当前 SQL、当前编辑器、这条语句时先读取真实界面上下文避免让模型猜测。",
parameters: {
type: "object",
properties: {
includeContent: { type: "boolean", description: "可选,是否附带页签中的 SQL / 命令草稿内容,默认 true" },
},
},
},
},
},
{
name: "inspect_workspace_tabs",
icon: "🗃️",
desc: "查看当前工作区打开的页签总览",
detail:
"返回当前工作区里打开的页签列表、哪个是活动页签,以及每个页签对应的连接、数据库、表名等上下文。适合用户说“我现在开了哪些 SQL”“看看我工作区里有哪些页签”“帮我对比这几个查询页签”时先读取真实工作区布局再继续分析。",
params: "limit?(默认 12), includeContent?(默认 false)",
tool: {
type: "function",
function: {
name: "inspect_workspace_tabs",
description:
"获取当前工作区已打开页签的总览,包括活动页签、页签类型、连接、数据库、表名,以及可选的 SQL / 命令草稿内容。适用于用户提到当前工作区、打开了哪些页签、哪几个查询页签、想对比多个编辑器内容时,先读取真实界面状态,避免模型猜测。",
parameters: {
type: "object",
properties: {
limit: { type: "number", description: "可选,最多返回多少个页签,默认 12最大 30" },
includeContent: { type: "boolean", description: "可选,是否附带页签中的 SQL / 命令草稿内容,默认 false" },
},
},
},
},
},
{
name: "inspect_recent_sql_logs",
icon: "🧾",
desc: "查看最近 SQL 执行日志",
detail:
"传入可选 limit 和 status返回最近 SQL 执行记录,包括数据库、耗时、成功/失败、报错、受影响行数和 SQL 文本。适合追查刚执行失败的语句、定位慢查询,并让 AI 基于真实执行历史给出解释或优化建议。",
params: "limit?, status?(all|success|error)",
tool: {
type: "function",
function: {
name: "inspect_recent_sql_logs",
description:
"获取最近 SQL 执行日志摘要,可按成功/失败过滤。适用于回看刚执行过的 SQL、排查失败原因、定位慢查询以及让 AI 基于真实执行历史给出解释和优化建议。",
parameters: {
type: "object",
properties: {
limit: { type: "number", description: "可选,返回多少条日志,默认 20最大 100" },
status: {
type: "string",
description: "可选,按执行状态过滤,支持 all、success、error默认 all",
enum: ["all", "success", "error"],
},
},
},
},
},
},
{
name: "inspect_recent_sql_activity",
icon: "📊",
desc: "总结最近 SQL 活动分布",
detail:
"可按 status、activityKind、dbName 和 keyword 过滤,返回最近 SQL 活动的结构化总结,包括读写/DDL 比例、语句类型分布、数据库分布、最近报错、最近写操作和最慢语句。适合用户提到“最近都执行了什么”“是不是刚删过数据”“哪个库最近报错最多”“最近主要在跑查询还是写入”时先读真实执行画像。",
params: "limit?, status?(all|success|error), activityKind?(all|read|write|ddl|transaction|session|other), dbName?, keyword?",
tool: {
type: "function",
function: {
name: "inspect_recent_sql_activity",
description:
"汇总最近 SQL 活动的结构化画像,可按执行状态、活动类型、数据库名和关键词过滤。适用于排查最近主要在执行哪些读写操作、某个库近期错误是否集中、是否发生过删除或 DDL、以及让 AI 基于真实执行现场先做全局判断。",
parameters: {
type: "object",
properties: {
limit: { type: "number", description: "可选,最近活动样例最多返回多少条,默认 30最大 100" },
status: {
type: "string",
description: "可选,按执行状态过滤,支持 all、success、error默认 all",
enum: ["all", "success", "error"],
},
activityKind: {
type: "string",
description: "可选,按活动类型过滤,支持 all、read、write、ddl、transaction、session、other默认 all",
enum: ["all", "read", "write", "ddl", "transaction", "session", "other"],
},
dbName: { type: "string", description: "可选,只看数据库名里包含该关键词的日志" },
keyword: { type: "string", description: "可选,按 SQL 文本、报错信息、语句类型或数据库名做关键词筛选" },
},
},
},
},
},
{
name: "inspect_app_logs",
icon: "🪵",
desc: "查看 GoNavi 应用日志尾部",
detail:
"可按关键词过滤,返回最近一段 GoNavi 应用日志里的 INFO/WARN/ERROR 行、级别分布、日志文件路径以及当前是否发生了日志窗口截断。适合用户提到“gonavi.log”“启动报错”“MCP 拉不起来”“数据库连接为什么失败”时,先读真实日志尾部再继续定位。",
params: "keyword?, lineLimit?(默认 80)",
tool: {
type: "function",
function: {
name: "inspect_app_logs",
description:
"读取 GoNavi 应用日志尾部,可按关键词过滤,并返回最近日志行、级别分布、日志路径和截断状态。适用于用户提到 gonavi.log、应用启动异常、MCP 启动失败、数据库连接报错或要求“看一下最近日志”时,优先读取真实应用日志,不要只凭界面现象推测。",
parameters: {
type: "object",
properties: {
keyword: { type: "string", description: "可选,按日志内容关键词过滤,例如 mcp、mysql、timeout、error" },
lineLimit: { type: "number", description: "可选,最多返回多少行日志,默认 80最大 200" },
},
},
},
},
},
{
name: "inspect_saved_queries",
icon: "💾",
desc: "查看本地已保存的 SQL 查询",
detail:
"可按关键词、连接或数据库过滤,返回保存查询的名称、所属连接、数据库和 SQL 预览。适合用户提到“我之前保存过的查询”“帮我找那条历史 SQL”时先从真实本地收藏里检索。",
params: "keyword?, connectionId?, dbName?, limit?, includeSql?(默认 true)",
tool: {
type: "function",
function: {
name: "inspect_saved_queries",
description:
"读取本地已保存的 SQL 查询列表,可按关键词、连接和数据库过滤,并返回每条查询的名称、所属连接、数据库与 SQL 预览。适用于用户想找历史查询、复用旧 SQL、核对保存脚本时先读取真实本地记录。",
parameters: {
type: "object",
properties: {
keyword: { type: "string", description: "可选按查询名称、SQL 文本、连接名或数据库名做关键词筛选" },
connectionId: { type: "string", description: "可选,只看某个连接下保存的查询" },
dbName: { type: "string", description: "可选,只看某个数据库下保存的查询" },
limit: { type: "number", description: "可选,最多返回多少条,默认 12最大 50" },
includeSql: { type: "boolean", description: "可选,是否附带 SQL 预览,默认 true" },
},
},
},
},
},
{
name: "inspect_ai_sessions",
icon: "🗂️",
desc: "查看本地 AI 历史会话清单",
detail:
"可按关键词过滤,返回本地 AI 会话标题、更新时间、消息数量、是否是当前会话,以及首条用户提问和最近一条消息预览。适合用户提到“之前那条 AI 对话”“帮我找上次聊过的记录”“最近哪个会话讲过这个问题”时先读真实会话资产。",
params: "keyword?, limit?, includePreview?(默认 true)",
tool: {
type: "function",
function: {
name: "inspect_ai_sessions",
description:
"读取本地 AI 历史会话清单,可按关键词过滤,并返回会话标题、更新时间、消息数量、是否是当前活动会话,以及首条用户问题和最近消息预览。适用于用户提到之前的 AI 对话、上次聊过的记录、最近哪个会话讲过某个问题时,先读取真实会话清单再继续定位。",
parameters: {
type: "object",
properties: {
keyword: { type: "string", description: "可选,按会话标题、会话 ID、首条用户问题或最近消息内容做关键词筛选" },
limit: { type: "number", description: "可选,最多返回多少条会话,默认 10最大 50" },
includePreview: { type: "boolean", description: "可选,是否附带首条用户问题和最近消息预览,默认 true" },
},
},
},
},
},
{
name: "inspect_sql_snippets",
icon: "🧩",
desc: "查看 SQL 片段模板",
detail:
"返回本地 SQL 片段的 prefix、名称、说明和模板预览可按关键词过滤。适合用户想找现成模板、补全片段、团队约定 SQL 模板时先读取真实片段库。",
params: "keyword?, limit?, includeBody?(默认 true)",
tool: {
type: "function",
function: {
name: "inspect_sql_snippets",
description:
"读取本地 SQL 片段模板列表,可按关键词过滤,并返回 prefix、名称、说明和模板预览。适用于用户想找 snippet、复用模板、核对 SQL 片段配置时,先读取真实本地片段库。",
parameters: {
type: "object",
properties: {
keyword: { type: "string", description: "可选,按 prefix、名称、描述或模板内容做关键词筛选" },
limit: { type: "number", description: "可选,最多返回多少条,默认 20最大 80" },
includeBody: { type: "boolean", description: "可选,是否附带模板内容预览,默认 true" },
},
},
},
},
},
{
name: "inspect_shortcuts",
icon: "⌨️",
desc: "查看当前快捷键配置与平台差异",
detail:
"返回当前快捷键动作、当前平台绑定、Win/Mac 双平台组合键、是否被用户改过以及默认值对照。适合用户问“当前这个快捷键是什么”“Win 和 Mac 分别怎么按”“我是不是改过默认快捷键”时先读真实配置。",
params: "action?, keyword?, includeDisabled?(默认 true), includeAllPlatforms?(默认 true)",
tool: {
type: "function",
function: {
name: "inspect_shortcuts",
description:
"读取当前 GoNavi 快捷键配置快照可按动作名或关键词过滤并返回当前平台绑定、Win/Mac 双平台组合键、默认值和是否被用户改过。适用于用户提到快捷键、Win/Mac 键位差异、当前结果区/AI/查询相关快捷键是什么时,先读取真实配置,不要凭记忆回答默认值。",
parameters: {
type: "object",
properties: {
action: { type: "string", description: "可选,按动作 key 精确过滤,例如 toggleQueryResultsPanel、sendAIChatMessage、toggleAIPanel" },
keyword: { type: "string", description: "可选,按动作名、说明、作用域、组合键或默认值做关键词筛选" },
includeDisabled: { type: "boolean", description: "可选,是否包含当前被禁用的快捷键,默认 true" },
includeAllPlatforms: { type: "boolean", description: "可选,是否同时返回 Windows 和 macOS 两个平台绑定,默认 true" },
},
},
},
},
},
];

View File

@@ -1,786 +1,12 @@
export interface AIChatToolDefinition {
type: "function";
function: {
name: string;
description: string;
parameters: Record<string, any>;
};
}
import { BUILTIN_AI_DATABASE_TOOL_INFO } from "./aiBuiltinDatabaseToolInfo";
import { BUILTIN_AI_INSPECTION_TOOL_INFO } from "./aiBuiltinInspectionToolInfo";
export interface AIBuiltinToolInfo {
name: string;
icon: string;
desc: string;
detail: string;
params: string;
tool: AIChatToolDefinition;
}
export type {
AIChatToolDefinition,
AIBuiltinToolInfo,
} from "./aiBuiltinToolInfo.types";
export const BUILTIN_AI_TOOL_INFO: AIBuiltinToolInfo[] = [
{
name: "get_connections",
icon: "🔗",
desc: "获取所有可用的数据库连接",
detail:
"返回连接 ID、名称、类型 (MySQL/PostgreSQL 等) 和 Host 地址。AI 根据返回信息决定优先探索哪个连接。",
params: "无参数",
tool: {
type: "function",
function: {
name: "get_connections",
description:
"当需要查询、操作数据库但用户没有选择任何连接上下文时获取当前软件中可用的所有数据库连接信息。返回的数据包含连接ID(id)和名称(name)。",
parameters: { type: "object", properties: {} },
},
},
},
{
name: "get_databases",
icon: "🗄️",
desc: "获取指定连接下的所有数据库",
detail: "传入 connectionId返回该连接下的数据库/Schema 名称列表。",
params: "connectionId: 连接 ID",
tool: {
type: "function",
function: {
name: "get_databases",
description: "获取指定连接connectionId下的所有数据库(Database/Schema)名。",
parameters: {
type: "object",
properties: {
connectionId: { type: "string", description: "连接ID (从 get_connections 获取)" },
},
required: ["connectionId"],
},
},
},
},
{
name: "get_tables",
icon: "📋",
desc: "获取指定数据库下的所有表名",
detail:
"传入 connectionId 和 dbName返回表名列表。AI 用它来定位用户提到的目标表。",
params: "connectionId, dbName",
tool: {
type: "function",
function: {
name: "get_tables",
description:
"当已经确定了目标连接和数据库名后,如果用户询问或隐式提到了表但你不知道确切表名,调用此工具获取该数据库下的所有表名列表(只含表名,帮助你推断目标表)。",
parameters: {
type: "object",
properties: {
connectionId: { type: "string", description: "连接ID" },
dbName: { type: "string", description: "数据库名" },
},
required: ["connectionId", "dbName"],
},
},
},
},
{
name: "get_all_columns",
icon: "🧱",
desc: "获取指定数据库下所有表的字段摘要",
detail:
"传入 connectionId 和 dbName返回跨表字段列表表名、字段名、类型、注释。适合用户只知道业务字段、不知道具体在哪张表时快速定位目标表。",
params: "connectionId, dbName",
tool: {
type: "function",
function: {
name: "get_all_columns",
description:
"获取指定数据库下全部表的字段摘要,返回表名、字段名、类型和注释。适用于按字段反查表、跨表梳理相同字段、做数据地图探索。",
parameters: {
type: "object",
properties: {
connectionId: { type: "string", description: "连接ID" },
dbName: { type: "string", description: "数据库名" },
},
required: ["connectionId", "dbName"],
},
},
},
},
{
name: "get_columns",
icon: "🔍",
desc: "获取指定表的字段结构",
detail:
"传入 connectionId、dbName 和 tableName返回每个字段的名称、类型、是否可空、默认值和注释。AI 在生成 SQL 前必须调用此工具确认真实字段名。",
params: "connectionId, dbName, tableName",
tool: {
type: "function",
function: {
name: "get_columns",
description:
"获取指定表的字段列表(字段名、类型、是否可空、默认值、注释等)。在生成 SQL 之前必须先调用此工具确认真实字段名,禁止猜测字段名。",
parameters: {
type: "object",
properties: {
connectionId: { type: "string", description: "连接ID" },
dbName: { type: "string", description: "数据库名" },
tableName: { type: "string", description: "表名" },
},
required: ["connectionId", "dbName", "tableName"],
},
},
},
},
{
name: "get_indexes",
icon: "🧭",
desc: "获取指定表的索引定义",
detail:
"传入 connectionId、dbName 和 tableName返回索引名、索引列、唯一性和索引类型。AI 在做慢 SQL 分析、索引优化和执行计划推断时应优先调用。",
params: "connectionId, dbName, tableName",
tool: {
type: "function",
function: {
name: "get_indexes",
description:
"获取指定表的索引定义,包括索引名、字段顺序、唯一性和索引类型。适用于慢 SQL 分析、索引优化建议和确认现有索引覆盖情况。",
parameters: {
type: "object",
properties: {
connectionId: { type: "string", description: "连接ID" },
dbName: { type: "string", description: "数据库名" },
tableName: { type: "string", description: "表名" },
},
required: ["connectionId", "dbName", "tableName"],
},
},
},
},
{
name: "get_foreign_keys",
icon: "🧬",
desc: "获取指定表的外键关系",
detail:
"传入 connectionId、dbName 和 tableName返回当前表到其他表的外键映射。AI 在推断表关系、生成联表 SQL 和评审数据一致性时可直接使用。",
params: "connectionId, dbName, tableName",
tool: {
type: "function",
function: {
name: "get_foreign_keys",
description:
"获取指定表的外键关系包括本表字段、引用表、引用字段和约束名。适用于联表路径分析、ER 关系梳理和约束检查。",
parameters: {
type: "object",
properties: {
connectionId: { type: "string", description: "连接ID" },
dbName: { type: "string", description: "数据库名" },
tableName: { type: "string", description: "表名" },
},
required: ["connectionId", "dbName", "tableName"],
},
},
},
},
{
name: "get_triggers",
icon: "⏱️",
desc: "获取指定表的触发器定义",
detail:
"传入 connectionId、dbName 和 tableName返回触发器名、触发时机、事件类型和语句体。AI 在分析隐式写入、副作用和审计逻辑时可直接查看。",
params: "connectionId, dbName, tableName",
tool: {
type: "function",
function: {
name: "get_triggers",
description:
"获取指定表的触发器定义,包括触发时机、事件和触发语句。适用于排查隐式数据变更、审计逻辑和表级副作用。",
parameters: {
type: "object",
properties: {
connectionId: { type: "string", description: "连接ID" },
dbName: { type: "string", description: "数据库名" },
tableName: { type: "string", description: "表名" },
},
required: ["connectionId", "dbName", "tableName"],
},
},
},
},
{
name: "get_table_ddl",
icon: "📝",
desc: "获取表的建表语句 (DDL)",
detail:
"传入 connectionId、dbName 和 tableName返回完整的 CREATE TABLE 语句,包含字段定义、索引、约束等信息。",
params: "connectionId, dbName, tableName",
tool: {
type: "function",
function: {
name: "get_table_ddl",
description: "获取指定表的完整建表语句CREATE TABLE DDL包含字段、索引、约束等完整结构信息。",
parameters: {
type: "object",
properties: {
connectionId: { type: "string", description: "连接ID" },
dbName: { type: "string", description: "数据库名" },
tableName: { type: "string", description: "表名" },
},
required: ["connectionId", "dbName", "tableName"],
},
},
},
},
{
name: "preview_table_rows",
icon: "👀",
desc: "抽样预览指定表的前几行数据",
detail:
"传入 connectionId、dbName、tableName 和可选 limit返回该表的前几行真实样例数据。适合先看数据形态、空值分布和枚举值再决定怎么写 SQL。",
params: "connectionId, dbName, tableName, limit?",
tool: {
type: "function",
function: {
name: "preview_table_rows",
description:
"预览指定表的前几行样例数据。适用于快速理解字段取值形态、空值情况、时间格式和状态枚举,减少模型盲写 SQL。",
parameters: {
type: "object",
properties: {
connectionId: { type: "string", description: "连接ID" },
dbName: { type: "string", description: "数据库名" },
tableName: { type: "string", description: "表名" },
limit: { type: "number", description: "可选,预览行数,默认 20最大 100" },
},
required: ["connectionId", "dbName", "tableName"],
},
},
},
},
{
name: "inspect_table_bundle",
icon: "🧰",
desc: "一次抓取指定表的结构快照",
detail:
"传入 connectionId、dbName 和 tableName返回字段、索引、外键、触发器和 DDL还可以附带前几行样例数据。适合在写 SQL、评审表设计或排查副作用前先做完整摸底。",
params: "connectionId, dbName, tableName, includeSampleRows?, sampleLimit?",
tool: {
type: "function",
function: {
name: "inspect_table_bundle",
description:
"一次性获取指定表的结构快照返回字段、索引、外键、触发器、DDL以及可选样例数据。适用于做完整表设计摸底、快速理解表关系和降低模型多次往返调用。",
parameters: {
type: "object",
properties: {
connectionId: { type: "string", description: "连接ID" },
dbName: { type: "string", description: "数据库名" },
tableName: { type: "string", description: "表名" },
includeSampleRows: { type: "boolean", description: "可选,是否附带前几行样例数据" },
sampleLimit: { type: "number", description: "可选,样例行数,默认 10最大 100" },
},
required: ["connectionId", "dbName", "tableName"],
},
},
},
},
{
name: "inspect_database_bundle",
icon: "🗂️",
desc: "一次抓取指定数据库的结构总览",
detail:
"传入 connectionId 和 dbName返回库内表清单、表数量、总字段数以及按表聚合的字段摘要预览。适合刚接手陌生库时先做全局摸底再决定深入哪张表。",
params: "connectionId, dbName, includeColumns?, tableLimit?, perTableColumnLimit?",
tool: {
type: "function",
function: {
name: "inspect_database_bundle",
description:
"一次性获取指定数据库的结构总览,返回表名列表、总字段数,以及按表聚合的字段摘要预览。适用于陌生数据库摸底、做数据地图和快速选择下一步要深入分析的表。",
parameters: {
type: "object",
properties: {
connectionId: { type: "string", description: "连接ID" },
dbName: { type: "string", description: "数据库名" },
includeColumns: { type: "boolean", description: "可选,是否附带按表聚合的字段摘要,默认 true" },
tableLimit: { type: "number", description: "可选,最多返回多少张表,默认 80最大 200" },
perTableColumnLimit: { type: "number", description: "可选,每张表最多返回多少个字段摘要,默认 8最大 30" },
},
required: ["connectionId", "dbName"],
},
},
},
},
{
name: "inspect_ai_setup_health",
icon: "🩺",
desc: "一键体检当前 AI 配置健康度",
detail:
"汇总当前 AI 供应商、聊天发送前置、MCP 服务与外部客户端接入、提示词与 Skills、上下文挂载情况并给出阻塞项、告警项和下一步建议。适合用户说“AI 为什么不好用”“帮我看下 AI 整体有没有问题”“现在这套 AI 配置还缺什么”时先做一次总览诊断。",
params: "无参数",
tool: {
type: "function",
function: {
name: "inspect_ai_setup_health",
description:
"体检当前 AI 配置健康度返回供应商、模型、聊天发送前置、MCP 接入、提示词与 Skills、表结构上下文挂载等整体快照并给出阻塞项、建议项和下一步动作。适用于用户提到 AI 为什么不好用、当前 AI 配置哪里还缺、是否已经能稳定工作时,优先读取这份总览诊断,不要拆成多次猜测。",
parameters: { type: "object", properties: {} },
},
},
},
{
name: "inspect_ai_runtime",
icon: "🎛️",
desc: "查看当前 AI 自身运行状态",
detail:
"返回当前启用的模型供应商、模型名、安全级别、上下文级别、启用的 Skills以及当前已暴露的内置工具和 MCP 工具。适合用户问“你现在能调用什么”“当前用的哪个模型”“为什么不能执行写操作”时,先读真实运行状态再回答。",
params: "无参数",
tool: {
type: "function",
function: {
name: "inspect_ai_runtime",
description:
"读取当前 AI 运行时快照,包括当前供应商、模型、安全级别、上下文级别、启用的 Skills、当前可用的内置工具与 MCP 工具。适用于用户询问当前 AI 能力边界、当前使用哪个模型、为什么不能执行某些操作时,先读取真实运行状态,避免模型猜测。",
parameters: { type: "object", properties: {} },
},
},
},
{
name: "inspect_ai_safety",
icon: "🛡️",
desc: "查看当前 AI 写入安全边界",
detail:
"返回当前 AI 安全级别对应的 SQL 允许范围、非只读语句是否仍需确认 / allowMutating以及当前活动连接、页签或 JVM 诊断权限是否还叠加了只读限制。适合用户问“为什么现在不能写”“DDL 能不能执行”“allowMutating 要不要传”时先读真实边界。",
params: "无参数",
tool: {
type: "function",
function: {
name: "inspect_ai_safety",
description:
"读取当前 AI 安全边界快照,包括当前安全级别允许的 SQL 范围、非查询语句的确认要求、MCP execute_sql 对 allowMutating 的要求,以及当前活动连接、结果页签或 JVM 诊断权限是否额外处于只读限制。适用于用户提到为什么现在不能写、当前是不是只读、DDL 能不能执行、allowMutating 是否必须传时,先读取真实边界再回答。",
parameters: { type: "object", properties: {} },
},
},
},
{
name: "inspect_ai_providers",
icon: "🪪",
desc: "查看当前 AI 供应商与模型配置",
detail:
"返回当前配置了哪些 AI 供应商、哪个正在生效、各自的 baseUrl、已选模型、声明模型列表、密钥是否存在、自定义请求头 key以及缺少密钥/模型/地址等待检查项。适合用户问“为什么没有模型”“API Key 有没有配”“当前到底配了哪些供应商”时先读真实配置。",
params: "无参数",
tool: {
type: "function",
function: {
name: "inspect_ai_providers",
description:
"读取当前 AI 供应商配置快照,包括供应商列表、活动供应商、接口地址、已选模型、声明模型列表、是否存在密钥、自定义请求头 key以及缺少密钥/模型/地址等待检查项。适用于用户提到当前供应商、模型列表为空、API Key 是否配置、为什么 AI 不能正常发起请求时,先读取真实配置再解释。",
parameters: { type: "object", properties: {} },
},
},
},
{
name: "inspect_ai_chat_readiness",
icon: "🚦",
desc: "查看当前 AI 聊天是否具备发送条件",
detail:
"返回当前聊天输入区是否已经具备发送条件,包括有没有活动供应商、当前供应商是否缺密钥或接口地址、是否已选模型、当前连接/表结构上下文是否已挂载,以及下一步建议动作。适合用户问“为什么现在不能发送”“输入框到底缺什么配置”“当前 AI 聊天准备好了没有”时先读真实状态。",
params: "无参数",
tool: {
type: "function",
function: {
name: "inspect_ai_chat_readiness",
description:
"读取当前 AI 聊天输入区的发送前置状态,包括活动供应商、密钥和接口地址是否完整、是否已选模型、当前连接上下文和已挂载表结构数量,以及建议的下一步动作。适用于用户提到为什么现在不能发送、为什么输入区还没准备好、当前到底缺什么配置时,先读取真实状态再回答。",
parameters: { type: "object", properties: {} },
},
},
},
{
name: "inspect_mcp_setup",
icon: "🪛",
desc: "查看当前 MCP 配置与外部接入状态",
detail:
"返回当前本地配置了哪些 MCP 服务、哪些已启用、每个服务声明了什么启动命令,以及 Claude Code / Codex 这类外部客户端的写入状态与命令检测结果。适合用户问“我现在配了哪些 MCP”“为什么外部客户端还用不了”“MCP 到底写没写进去”时先读真实状态。",
params: "无参数",
tool: {
type: "function",
function: {
name: "inspect_mcp_setup",
description:
"读取当前本地 MCP 配置快照,包括 MCP 服务列表、启用状态、启动命令、环境变量 key、已发现工具以及外部客户端的 GoNavi MCP 写入状态与本机 CLI 检测结果。适用于用户提到 MCP 服务配置、Claude/Codex 是否已接入、为什么外部客户端用不了、当前到底启用了哪些 MCP 时,先读取真实配置再回答。",
parameters: { type: "object", properties: {} },
},
},
},
{
name: "inspect_mcp_authoring_guide",
icon: "🧭",
desc: "查看新增 MCP 的填写指引",
detail:
"返回新增 MCP 表单里各字段的作用、推荐填写顺序、完整命令自动拆分规则,以及 Node / uvx / Python / EXE 模板样例。适合用户问“command/args/env 到底怎么填”“给我一个 node / uvx / python 示例”“为什么启动命令不能整行填”时,先读这份真实接入指引。",
params: "无参数",
tool: {
type: "function",
function: {
name: "inspect_mcp_authoring_guide",
description:
"读取 GoNavi 当前内置的 MCP 新增指引,包括推荐填写顺序、字段作用、常见命令示例、完整命令自动拆分规则,以及 Node / uvx / Python / EXE 模板样例。适用于用户提到新增 MCP 不知道 command、args、env、timeout 怎么填,或想要一个最接近的模板时,先读取这份真实前端接入指南,不要凭记忆口述。",
parameters: { type: "object", properties: {} },
},
},
},
{
name: "inspect_ai_guidance",
icon: "🧠",
desc: "查看当前 AI 提示词与 Skills 配置",
detail:
"返回当前用户自定义的全局/数据库/JVM 提示词,以及当前启用的 Skills、作用域、依赖工具和 skill prompt 内容。适合用户问“你现在到底带了哪些提示词”“为什么你会这样回答”“当前有哪些 Skills 在生效”时先读真实配置。",
params: "无参数",
tool: {
type: "function",
function: {
name: "inspect_ai_guidance",
description:
"读取当前 AI 的提示与技能配置快照,包括用户自定义提示词、当前启用的 Skills、作用域、依赖工具和各自的 system prompt。适用于用户提到当前提示词、当前 Skill、为什么 AI 当前会这样回答、当前有哪些规则在生效时,先读取真实配置再解释。",
parameters: { type: "object", properties: {} },
},
},
},
{
name: "inspect_ai_context",
icon: "🧷",
desc: "查看当前 AI 已关联的表结构上下文",
detail:
"返回当前对话已经挂载到 AI 上下文里的表清单、所属连接与数据库,以及每张表的 DDL 预览。适合用户说“看看我现在带了哪些表结构”“当前 AI 上下文是什么”时,先读取真实挂载状态再继续分析。",
params: "includeDDL?(默认 false), ddlLimit?(默认 4000)",
tool: {
type: "function",
function: {
name: "inspect_ai_context",
description:
"读取当前对话已经关联到 AI 上下文里的表结构快照,包括连接、数据库、表名,以及可选的 DDL 内容。适用于用户提到当前 AI 上下文、当前关联表、当前挂载的表结构时,先读取真实状态,避免模型凭记忆复述。",
parameters: {
type: "object",
properties: {
includeDDL: { type: "boolean", description: "可选,是否附带每张表的 DDL 内容,默认 false" },
ddlLimit: { type: "number", description: "可选DDL 截断长度,默认 4000最大 12000" },
},
},
},
},
},
{
name: "inspect_current_connection",
icon: "🛰️",
desc: "查看当前活动连接/数据源摘要",
detail:
"返回当前活动连接的类型、地址、端口、当前数据库、是否启用 SSH/代理/HTTP 隧道,以及当前活动页签绑定的表信息。适合用户问“我现在连的是哪个库”“这个连接走没走 SSH”“当前数据源是什么类型”时先读取真实连接状态。",
params: "无参数",
tool: {
type: "function",
function: {
name: "inspect_current_connection",
description:
"读取当前活动连接或当前页签对应数据源的真实摘要包括连接类型、地址、端口、当前数据库、SSH/代理/HTTP 隧道状态,以及当前页签绑定的表上下文。适用于用户提到当前连接、当前数据源、当前库地址、是否走 SSH、当前连的是哪种数据库时先读取真实界面上下文避免模型猜测。",
parameters: { type: "object", properties: {} },
},
},
},
{
name: "inspect_connection_capabilities",
icon: "🧱",
desc: "查看当前连接支持哪些前端能力",
detail:
"返回当前或指定连接的数据源能力矩阵包括是否支持查询编辑器、SQL 导出、复制 INSERT、新建/重命名/删除数据库、结果是否强制只读,以及是否倾向手动总数或近似计数。适合用户问“为什么这里不能建库/删库”“这个数据源为什么结果不能编辑”“这个类型支持哪些操作”时,先读取真实能力边界。",
params: "connectionId?(默认取当前活动连接)",
tool: {
type: "function",
function: {
name: "inspect_connection_capabilities",
description:
"读取当前活动连接或指定 saved connection 的前端能力矩阵包括是否支持查询编辑器、SQL 导出、复制 INSERT、新建/重命名/删除数据库、结果是否强制只读,以及是否适合手动总数或近似计数。适用于用户提到当前连接为什么不能建库、为什么结果集不能编辑、某种数据库类型到底支持哪些前端动作时,先读取真实能力配置,避免模型凭经验猜测。",
parameters: {
type: "object",
properties: {
connectionId: { type: "string", description: "可选,指定要查看的连接 ID不传时默认读取当前活动连接" },
},
},
},
},
},
{
name: "inspect_saved_connections",
icon: "🧭",
desc: "查看本地已保存连接清单",
detail:
"可按关键词或数据库类型过滤返回本地保存的数据源列表、连接类型分布以及每条连接的地址、当前库、SSH/代理/HTTP 隧道状态。适合用户问“我本地存了哪些连接”“帮我找 mysql / postgres 连接”“哪条连接配置了 SSH”时先读真实本地连接资产。",
params: "keyword?, type?, limit?",
tool: {
type: "function",
function: {
name: "inspect_saved_connections",
description:
"读取本地已保存连接清单可按关键词和数据库类型过滤并返回每条连接的类型、地址、当前库、SSH/代理/HTTP 隧道等摘要。适用于用户提到本地保存了哪些连接、要找哪条 mysql/postgres 连接、哪条连接启用了 SSH 或代理时,先读取真实本地连接资产再回答。",
parameters: {
type: "object",
properties: {
keyword: { type: "string", description: "可选按连接名、ID、类型、主机、数据库名或 SSH/代理地址做关键词筛选" },
type: { type: "string", description: "可选,只看某种数据库类型,例如 mysql、postgres、redis、mongodb" },
limit: { type: "number", description: "可选,最多返回多少条连接,默认 20最大 100" },
},
},
},
},
},
{
name: "inspect_external_sql_directories",
icon: "🗂️",
desc: "查看本地外部 SQL 目录资产",
detail:
"可按关键词、连接或数据库过滤,返回本地配置的外部 SQL 目录、目录路径、绑定连接/数据库,以及当前是否已经打开这些目录里的 SQL 文件。适合用户提到“外部 SQL 目录”“某个脚本在哪个目录”“现在打开的 SQL 文件来自哪个外部目录”时,先读真实资产。",
params: "keyword?, connectionId?, dbName?, limit?",
tool: {
type: "function",
function: {
name: "inspect_external_sql_directories",
description:
"读取本地配置的外部 SQL 目录清单,可按关键词、连接和数据库过滤,并返回目录路径、绑定连接/数据库,以及当前打开的外部 SQL 文件页签摘要。适用于用户提到外部 SQL 目录、某个 SQL 文件放在哪、当前打开的脚本来自哪个目录时,先读取真实本地资产再回答。",
parameters: {
type: "object",
properties: {
keyword: { type: "string", description: "可选,按目录名、路径、连接名或数据库名做关键词筛选" },
connectionId: { type: "string", description: "可选,只看绑定到某个连接的外部 SQL 目录" },
dbName: { type: "string", description: "可选,只看绑定到某个数据库的外部 SQL 目录" },
limit: { type: "number", description: "可选,最多返回多少条目录,默认 20最大 100" },
},
},
},
},
},
{
name: "inspect_external_sql_file",
icon: "📄",
desc: "读取外部 SQL 文件内容",
detail:
"传入具体 filePath读取已配置外部 SQL 目录中的 SQL 文件内容,并返回所属目录、绑定连接/数据库、是否已有打开页签,以及截断后的正文预览。适合用户提到“看一下这个目录里的某个脚本”“帮我解释 report.sql 在写什么”时,先读取真实文件内容再分析。",
params: "filePath, previewCharLimit?",
tool: {
type: "function",
function: {
name: "inspect_external_sql_file",
description:
"读取指定外部 SQL 文件的内容预览,仅用于已配置外部 SQL 目录中的 SQL 文件。返回文件路径、所属目录、绑定连接/数据库、是否已在工作区打开,以及截断后的正文内容。适用于用户提到某个目录中的具体 SQL 脚本、想让 AI 直接解释脚本逻辑、或想确认某个外部 SQL 文件内容时,先读真实文件再回答。",
parameters: {
type: "object",
properties: {
filePath: { type: "string", description: "必填,要读取的 SQL 文件绝对路径,通常先通过 inspect_external_sql_directories 找到" },
previewCharLimit: { type: "number", description: "可选,正文预览最多返回多少字符,默认 12000最大 40000" },
},
required: ["filePath"],
},
},
},
},
{
name: "inspect_active_tab",
icon: "📍",
desc: "查看当前活动页签上下文",
detail:
"返回当前活动页签的类型、连接、数据库、表名,以及当前 SQL / 命令页签里的草稿内容(超长会截断)。适合用户说“看我当前这条 SQL”“优化这个编辑器里的语句”时先让 AI 直接读取当前工作区上下文。",
params: "includeContent?(默认 true)",
tool: {
type: "function",
function: {
name: "inspect_active_tab",
description:
"获取当前活动页签的上下文快照,包括页签类型、连接、数据库、表名,以及当前 SQL / 命令页签里的草稿内容。适用于用户提到当前页签、当前 SQL、当前编辑器、这条语句时先读取真实界面上下文避免让模型猜测。",
parameters: {
type: "object",
properties: {
includeContent: { type: "boolean", description: "可选,是否附带页签中的 SQL / 命令草稿内容,默认 true" },
},
},
},
},
},
{
name: "inspect_workspace_tabs",
icon: "🗃️",
desc: "查看当前工作区打开的页签总览",
detail:
"返回当前工作区里打开的页签列表、哪个是活动页签,以及每个页签对应的连接、数据库、表名等上下文。适合用户说“我现在开了哪些 SQL”“看看我工作区里有哪些页签”“帮我对比这几个查询页签”时先读取真实工作区布局再继续分析。",
params: "limit?(默认 12), includeContent?(默认 false)",
tool: {
type: "function",
function: {
name: "inspect_workspace_tabs",
description:
"获取当前工作区已打开页签的总览,包括活动页签、页签类型、连接、数据库、表名,以及可选的 SQL / 命令草稿内容。适用于用户提到当前工作区、打开了哪些页签、哪几个查询页签、想对比多个编辑器内容时,先读取真实界面状态,避免模型猜测。",
parameters: {
type: "object",
properties: {
limit: { type: "number", description: "可选,最多返回多少个页签,默认 12最大 30" },
includeContent: { type: "boolean", description: "可选,是否附带页签中的 SQL / 命令草稿内容,默认 false" },
},
},
},
},
},
{
name: "inspect_recent_sql_logs",
icon: "🧾",
desc: "查看最近 SQL 执行日志",
detail:
"传入可选 limit 和 status返回最近 SQL 执行记录,包括数据库、耗时、成功/失败、报错、受影响行数和 SQL 文本。适合追查刚执行失败的语句、定位慢查询,并让 AI 基于真实执行历史给出解释或优化建议。",
params: "limit?, status?(all|success|error)",
tool: {
type: "function",
function: {
name: "inspect_recent_sql_logs",
description:
"获取最近 SQL 执行日志摘要,可按成功/失败过滤。适用于回看刚执行过的 SQL、排查失败原因、定位慢查询以及让 AI 基于真实执行历史给出解释和优化建议。",
parameters: {
type: "object",
properties: {
limit: { type: "number", description: "可选,返回多少条日志,默认 20最大 100" },
status: {
type: "string",
description: "可选,按执行状态过滤,支持 all、success、error默认 all",
enum: ["all", "success", "error"],
},
},
},
},
},
},
{
name: "inspect_recent_sql_activity",
icon: "📊",
desc: "总结最近 SQL 活动分布",
detail:
"可按 status、activityKind、dbName 和 keyword 过滤,返回最近 SQL 活动的结构化总结,包括读写/DDL 比例、语句类型分布、数据库分布、最近报错、最近写操作和最慢语句。适合用户提到“最近都执行了什么”“是不是刚删过数据”“哪个库最近报错最多”“最近主要在跑查询还是写入”时先读真实执行画像。",
params: "limit?, status?(all|success|error), activityKind?(all|read|write|ddl|transaction|session|other), dbName?, keyword?",
tool: {
type: "function",
function: {
name: "inspect_recent_sql_activity",
description:
"汇总最近 SQL 活动的结构化画像,可按执行状态、活动类型、数据库名和关键词过滤。适用于排查最近主要在执行哪些读写操作、某个库近期错误是否集中、是否发生过删除或 DDL、以及让 AI 基于真实执行现场先做全局判断。",
parameters: {
type: "object",
properties: {
limit: { type: "number", description: "可选,最近活动样例最多返回多少条,默认 30最大 100" },
status: {
type: "string",
description: "可选,按执行状态过滤,支持 all、success、error默认 all",
enum: ["all", "success", "error"],
},
activityKind: {
type: "string",
description: "可选,按活动类型过滤,支持 all、read、write、ddl、transaction、session、other默认 all",
enum: ["all", "read", "write", "ddl", "transaction", "session", "other"],
},
dbName: { type: "string", description: "可选,只看数据库名里包含该关键词的日志" },
keyword: { type: "string", description: "可选,按 SQL 文本、报错信息、语句类型或数据库名做关键词筛选" },
},
},
},
},
},
{
name: "inspect_saved_queries",
icon: "💾",
desc: "查看本地已保存的 SQL 查询",
detail:
"可按关键词、连接或数据库过滤,返回保存查询的名称、所属连接、数据库和 SQL 预览。适合用户提到“我之前保存过的查询”“帮我找那条历史 SQL”时先从真实本地收藏里检索。",
params: "keyword?, connectionId?, dbName?, limit?, includeSql?(默认 true)",
tool: {
type: "function",
function: {
name: "inspect_saved_queries",
description:
"读取本地已保存的 SQL 查询列表,可按关键词、连接和数据库过滤,并返回每条查询的名称、所属连接、数据库与 SQL 预览。适用于用户想找历史查询、复用旧 SQL、核对保存脚本时先读取真实本地记录。",
parameters: {
type: "object",
properties: {
keyword: { type: "string", description: "可选按查询名称、SQL 文本、连接名或数据库名做关键词筛选" },
connectionId: { type: "string", description: "可选,只看某个连接下保存的查询" },
dbName: { type: "string", description: "可选,只看某个数据库下保存的查询" },
limit: { type: "number", description: "可选,最多返回多少条,默认 12最大 50" },
includeSql: { type: "boolean", description: "可选,是否附带 SQL 预览,默认 true" },
},
},
},
},
},
{
name: "inspect_ai_sessions",
icon: "🗂️",
desc: "查看本地 AI 历史会话清单",
detail:
"可按关键词过滤,返回本地 AI 会话标题、更新时间、消息数量、是否是当前会话,以及首条用户提问和最近一条消息预览。适合用户提到“之前那条 AI 对话”“帮我找上次聊过的记录”“最近哪个会话讲过这个问题”时先读真实会话资产。",
params: "keyword?, limit?, includePreview?(默认 true)",
tool: {
type: "function",
function: {
name: "inspect_ai_sessions",
description:
"读取本地 AI 历史会话清单,可按关键词过滤,并返回会话标题、更新时间、消息数量、是否是当前活动会话,以及首条用户问题和最近消息预览。适用于用户提到之前的 AI 对话、上次聊过的记录、最近哪个会话讲过某个问题时,先读取真实会话清单再继续定位。",
parameters: {
type: "object",
properties: {
keyword: { type: "string", description: "可选,按会话标题、会话 ID、首条用户问题或最近消息内容做关键词筛选" },
limit: { type: "number", description: "可选,最多返回多少条会话,默认 10最大 50" },
includePreview: { type: "boolean", description: "可选,是否附带首条用户问题和最近消息预览,默认 true" },
},
},
},
},
},
{
name: "inspect_sql_snippets",
icon: "🧩",
desc: "查看 SQL 片段模板",
detail:
"返回本地 SQL 片段的 prefix、名称、说明和模板预览可按关键词过滤。适合用户想找现成模板、补全片段、团队约定 SQL 模板时先读取真实片段库。",
params: "keyword?, limit?, includeBody?(默认 true)",
tool: {
type: "function",
function: {
name: "inspect_sql_snippets",
description:
"读取本地 SQL 片段模板列表,可按关键词过滤,并返回 prefix、名称、说明和模板预览。适用于用户想找 snippet、复用模板、核对 SQL 片段配置时,先读取真实本地片段库。",
parameters: {
type: "object",
properties: {
keyword: { type: "string", description: "可选,按 prefix、名称、描述或模板内容做关键词筛选" },
limit: { type: "number", description: "可选,最多返回多少条,默认 20最大 80" },
includeBody: { type: "boolean", description: "可选,是否附带模板内容预览,默认 true" },
},
},
},
},
},
{
name: "execute_sql",
icon: "▶️",
desc: "执行 SQL 查询并返回结果",
detail:
"传入 connectionId、dbName 和 sql在目标数据库上执行 SQL 并返回结果(最多 50 行)。受安全级别控制,只读模式下仅允许 SELECT/SHOW/DESCRIBE。",
params: "connectionId, dbName, sql",
tool: {
type: "function",
function: {
name: "execute_sql",
description:
"在指定连接和数据库上执行 SQL 查询并返回结果。受安全级别控制,只读模式下只能执行 SELECT/SHOW/DESCRIBE 等查询操作。结果最多返回 50 行。",
parameters: {
type: "object",
properties: {
connectionId: { type: "string", description: "连接ID" },
dbName: { type: "string", description: "数据库名" },
sql: { type: "string", description: "要执行的 SQL 语句" },
},
required: ["connectionId", "dbName", "sql"],
},
},
},
},
export const BUILTIN_AI_TOOL_INFO = [
...BUILTIN_AI_DATABASE_TOOL_INFO,
...BUILTIN_AI_INSPECTION_TOOL_INFO,
];

View File

@@ -0,0 +1,17 @@
export interface AIChatToolDefinition {
type: "function";
function: {
name: string;
description: string;
parameters: Record<string, any>;
};
}
export interface AIBuiltinToolInfo {
name: string;
icon: string;
desc: string;
detail: string;
params: string;
tool: AIChatToolDefinition;
}

View File

@@ -94,14 +94,31 @@ describe('aiToolRegistry', () => {
expect(info?.tool.function.description).toContain('目录中的具体 SQL 脚本');
});
it('registers the shortcut inspector as a builtin tool', () => {
const info = BUILTIN_AI_TOOL_INFO.find((item) => item.name === 'inspect_shortcuts');
expect(info).toBeTruthy();
expect(info?.desc).toContain('快捷键配置');
expect(info?.tool.function.description).toContain('Win/Mac');
});
it('registers the app-log inspector as a builtin tool', () => {
const info = BUILTIN_AI_TOOL_INFO.find((item) => item.name === 'inspect_app_logs');
expect(info).toBeTruthy();
expect(info?.desc).toContain('应用日志');
expect(info?.tool.function.description).toContain('gonavi.log');
});
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 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');
const snippetTool = BUILTIN_AI_TOOL_INFO.find((item) => item.name === 'inspect_sql_snippets');
expect(recentActivityTool?.desc).toContain('最近 SQL 活动');
expect(recentActivityTool?.tool.function.description).toContain('最近 SQL 活动');
expect(appLogTool?.desc).toContain('GoNavi 应用日志');
expect(appLogTool?.tool.function.description).toContain('应用日志');
expect(savedQueryTool?.desc).toContain('已保存的 SQL 查询');
expect(savedQueryTool?.tool.function.description).toContain('历史查询');
expect(aiSessionsTool?.desc).toContain('AI 历史会话');
@@ -140,9 +157,11 @@ describe('aiToolRegistry', () => {
expect(tools.some((item) => item.function.name === 'inspect_external_sql_directories')).toBe(true);
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_saved_queries')).toBe(true);
expect(tools.some((item) => item.function.name === 'inspect_ai_sessions')).toBe(true);
expect(tools.some((item) => item.function.name === 'inspect_sql_snippets')).toBe(true);
expect(tools.some((item) => item.function.name === 'inspect_shortcuts')).toBe(true);
expect(tools.some((item) => item.function.name === 'custom_probe')).toBe(true);
});
});

View File

@@ -83,6 +83,31 @@ describe('mcpClientInstallStatus helpers', () => {
expect(pickPreferredMCPClient(statuses)).toBe('codex');
});
it('prefers a client that already matches current GoNavi over another client with a stale config', () => {
const statuses: AIMCPClientInstallStatus[] = [
{
client: 'claude-code',
displayName: 'Claude Code',
installed: true,
matchesCurrent: true,
clientDetected: true,
clientCommand: 'claude',
message: '已检测到 Claude Code 用户级 GoNavi MCP 配置,且与当前 GoNavi 安装路径一致',
},
{
client: 'codex',
displayName: 'Codex',
installed: true,
matchesCurrent: false,
clientDetected: true,
clientCommand: 'codex',
message: '已检测到 Codex 中的 GoNavi MCP 记录,但与当前 GoNavi 安装路径不一致,建议更新',
},
];
expect(pickPreferredMCPClient(statuses)).toBe('claude-code');
});
it('keeps the user-selected client when it is still present in the latest status list', () => {
expect(pickPreferredMCPClient(EMPTY_MCP_CLIENT_STATUSES, 'codex')).toBe('codex');
});

View File

@@ -40,10 +40,10 @@ const hasStatusError = (status: AIMCPClientInstallStatus): boolean =>
/|||/u.test(String(status.message || ''));
const getMCPClientPriority = (status: AIMCPClientInstallStatus): number => {
if (status.installed && !status.matchesCurrent) {
if (status.matchesCurrent) {
return 0;
}
if (status.matchesCurrent) {
if (status.installed && !status.matchesCurrent) {
return 1;
}
if (status.clientDetected) {

View File

@@ -159,6 +159,18 @@ describe('shortcut defaults', () => {
});
});
it('registers query results panel toggle as a query editor shortcut', () => {
expect(DEFAULT_SHORTCUT_OPTIONS.toggleQueryResultsPanel).toEqual({
mac: { combo: 'Meta+Shift+M', enabled: true },
windows: { combo: 'Ctrl+Shift+M', enabled: true },
});
expect(SHORTCUT_ACTION_META.toggleQueryResultsPanel).toMatchObject({
label: '切换结果区',
scope: 'queryEditor',
allowInEditable: true,
});
});
// Windows 任务栏恢复后字体异常变大的兜底入口(方案 3
// 自动 fix 路径9848b8b2刻意不再 toggle 以避免可见动画,由该快捷键给用户主动触发的修复入口。
it('registers reset window zoom shortcut with default Ctrl+Shift+0', () => {

View File

@@ -4,6 +4,7 @@ export type ShortcutAction =
| 'runQuery'
| 'selectCurrentStatement'
| 'saveQuery'
| 'toggleQueryResultsPanel'
| 'sendAIChatMessage'
| 'focusSidebarSearch'
| 'newQueryTab'
@@ -91,6 +92,7 @@ export const SHORTCUT_ACTION_ORDER: ShortcutAction[] = [
'runQuery',
'selectCurrentStatement',
'saveQuery',
'toggleQueryResultsPanel',
'sendAIChatMessage',
'focusSidebarSearch',
'newQueryTab',
@@ -121,6 +123,12 @@ export const SHORTCUT_ACTION_META: Record<ShortcutAction, ShortcutActionMeta> =
scope: 'queryEditor',
allowInEditable: true,
},
toggleQueryResultsPanel: {
label: '切换结果区',
description: '在查询编辑器中显示或隐藏下方结果区域',
scope: 'queryEditor',
allowInEditable: true,
},
sendAIChatMessage: {
label: 'AI 聊天发送',
description: '在 AI 输入框中发送当前消息Shift+Enter 始终换行',
@@ -196,6 +204,10 @@ export const DEFAULT_SHORTCUT_OPTIONS: ShortcutOptions = {
mac: { combo: 'Meta+S', enabled: true },
windows: { combo: 'Ctrl+S', enabled: true },
},
toggleQueryResultsPanel: {
mac: { combo: 'Meta+Shift+M', enabled: true },
windows: { combo: 'Ctrl+Shift+M', enabled: true },
},
sendAIChatMessage: {
mac: { combo: 'Enter', enabled: true },
windows: { combo: 'Enter', enabled: true },

View File

@@ -202,6 +202,8 @@ export function PreviewImportFile(arg1:string):Promise<connection.QueryResult>;
export function ReadSQLFile(arg1:string):Promise<connection.QueryResult>;
export function ReadAppLogTail(arg1:number,arg2:string):Promise<connection.QueryResult>;
export function RedisConnect(arg1:connection.ConnectionConfig):Promise<connection.QueryResult>;
export function RedisDeleteHashField(arg1:connection.ConnectionConfig,arg2:string,arg3:any):Promise<connection.QueryResult>;

View File

@@ -394,6 +394,10 @@ export function ReadSQLFile(arg1) {
return window['go']['app']['App']['ReadSQLFile'](arg1);
}
export function ReadAppLogTail(arg1, arg2) {
return window['go']['app']['App']['ReadAppLogTail'](arg1, arg2);
}
export function RedisConnect(arg1) {
return window['go']['app']['App']['RedisConnect'](arg1);
}

View File

@@ -34,6 +34,9 @@ const sqlFileBatchMaxStatements = 1000
const sqlFileBatchMaxBytes = 4 * 1024 * 1024
const sqlFileProgressStatementInterval = 100
const sqlFileProgressTimeInterval = time.Second
const defaultAppLogTailLineLimit = 80
const maxAppLogTailLineLimit = 200
const appLogTailReadWindowBytes int64 = 256 * 1024
var mysqlCreateViewPrefixPattern = regexp.MustCompile(`(?is)^\s*create\s+(?:algorithm\s*=\s*\w+\s+)?(?:definer\s*=\s*(?:` + "`[^`]+`" + `|\S+)\s*@\s*(?:` + "`[^`]+`" + `|\S+)\s+)?(?:sql\s+security\s+(?:definer|invoker)\s+)?view\s+`)
@@ -84,6 +87,17 @@ type SQLDirectoryEntry struct {
Children []SQLDirectoryEntry `json:"children,omitempty"`
}
type appLogTailSnapshot struct {
LogPath string `json:"logPath"`
Keyword string `json:"keyword,omitempty"`
RequestedLineLimit int `json:"requestedLineLimit"`
ReturnedLineCount int `json:"returnedLineCount"`
FileWindowTruncated bool `json:"fileWindowTruncated"`
MatchedLinesTruncated bool `json:"matchedLinesTruncated"`
LevelBreakdown map[string]int `json:"levelBreakdown"`
Lines []string `json:"lines"`
}
func normalizeSQLFileName(rawName string) (string, error) {
name := strings.TrimSpace(rawName)
if name == "" {
@@ -556,10 +570,137 @@ func (a *App) ReadSQLFile(filePath string) connection.QueryResult {
return readSQLFileByPath(filePath)
}
func (a *App) ReadAppLogTail(lineLimit int, keyword string) connection.QueryResult {
return readAppLogTailByPath(logger.Path(), lineLimit, keyword)
}
func (a *App) WriteSQLFile(filePath string, content string) connection.QueryResult {
return writeSQLFileByPath(filePath, content)
}
func normalizeAppLogTailLineLimit(input int) int {
if input <= 0 {
return defaultAppLogTailLineLimit
}
if input > maxAppLogTailLineLimit {
return maxAppLogTailLineLimit
}
return input
}
func readAppLogTailWindow(filePath string, maxBytes int64) ([]byte, bool, error) {
f, err := os.Open(filePath)
if err != nil {
return nil, false, err
}
defer f.Close()
fi, err := f.Stat()
if err != nil {
return nil, false, err
}
size := fi.Size()
if size <= 0 {
return []byte{}, false, nil
}
offset := int64(0)
truncated := false
if maxBytes > 0 && size > maxBytes {
offset = size - maxBytes
truncated = true
}
buf := make([]byte, size-offset)
if _, err := f.ReadAt(buf, offset); err != nil && err != io.EOF {
return nil, false, err
}
if !truncated {
return buf, false, nil
}
text := string(buf)
if idx := strings.IndexByte(text, '\n'); idx >= 0 && idx+1 < len(text) {
return []byte(text[idx+1:]), true, nil
}
return []byte{}, true, nil
}
func buildAppLogLevelBreakdown(lines []string) map[string]int {
breakdown := map[string]int{
"INFO": 0,
"WARN": 0,
"ERROR": 0,
"OTHER": 0,
}
for _, line := range lines {
switch {
case strings.Contains(line, "[INFO]"):
breakdown["INFO"]++
case strings.Contains(line, "[WARN]"):
breakdown["WARN"]++
case strings.Contains(line, "[ERROR]"):
breakdown["ERROR"]++
default:
breakdown["OTHER"]++
}
}
return breakdown
}
func readAppLogTailByPath(filePath string, lineLimit int, keyword string) connection.QueryResult {
target := strings.TrimSpace(filePath)
if target == "" {
return connection.QueryResult{Success: false, Message: "当前未找到 GoNavi 日志文件"}
}
if _, err := os.Stat(target); err != nil {
return connection.QueryResult{Success: false, Message: err.Error()}
}
windowBytes, fileWindowTruncated, err := readAppLogTailWindow(target, appLogTailReadWindowBytes)
if err != nil {
return connection.QueryResult{Success: false, Message: err.Error()}
}
normalizedKeyword := strings.ToLower(strings.TrimSpace(keyword))
normalizedLineLimit := normalizeAppLogTailLineLimit(lineLimit)
rawLines := strings.Split(strings.ReplaceAll(string(windowBytes), "\r\n", "\n"), "\n")
lines := make([]string, 0, len(rawLines))
for _, rawLine := range rawLines {
line := strings.TrimSpace(rawLine)
if line == "" {
continue
}
lines = append(lines, line)
}
filteredLines := make([]string, 0, len(lines))
for _, line := range lines {
if normalizedKeyword != "" && !strings.Contains(strings.ToLower(line), normalizedKeyword) {
continue
}
filteredLines = append(filteredLines, line)
}
matchedLinesTruncated := len(filteredLines) > normalizedLineLimit
if matchedLinesTruncated {
filteredLines = filteredLines[len(filteredLines)-normalizedLineLimit:]
}
snapshot := appLogTailSnapshot{
LogPath: target,
Keyword: strings.TrimSpace(keyword),
RequestedLineLimit: normalizedLineLimit,
ReturnedLineCount: len(filteredLines),
FileWindowTruncated: fileWindowTruncated,
MatchedLinesTruncated: matchedLinesTruncated,
LevelBreakdown: buildAppLogLevelBreakdown(filteredLines),
Lines: filteredLines,
}
return connection.QueryResult{Success: true, Data: snapshot}
}
func (a *App) CreateSQLFile(directoryPath string, name string) connection.QueryResult {
return createSQLFileInDirectory(directoryPath, name)
}

View File

@@ -0,0 +1,75 @@
package app
import (
"os"
"path/filepath"
"testing"
)
func TestReadAppLogTailByPathReturnsLatestLinesAndLevelBreakdown(t *testing.T) {
dir := t.TempDir()
logPath := filepath.Join(dir, "gonavi.log")
content := "" +
"2026/06/09 10:00:00.000000 [INFO] boot ok\n" +
"2026/06/09 10:00:01.000000 [WARN] slow mcp start\n" +
"2026/06/09 10:00:02.000000 [ERROR] mysql dial failed\n"
if err := os.WriteFile(logPath, []byte(content), 0o644); err != nil {
t.Fatalf("write log failed: %v", err)
}
result := readAppLogTailByPath(logPath, 2, "")
if !result.Success {
t.Fatalf("expected success, got failure: %s", result.Message)
}
snapshot, ok := result.Data.(appLogTailSnapshot)
if !ok {
t.Fatalf("expected appLogTailSnapshot, got %T", result.Data)
}
if snapshot.ReturnedLineCount != 2 {
t.Fatalf("expected 2 returned lines, got %d", snapshot.ReturnedLineCount)
}
if !snapshot.MatchedLinesTruncated {
t.Fatal("expected matched lines to be truncated when requesting fewer lines than available")
}
if snapshot.LevelBreakdown["WARN"] != 1 || snapshot.LevelBreakdown["ERROR"] != 1 {
t.Fatalf("unexpected level breakdown: %#v", snapshot.LevelBreakdown)
}
if snapshot.Lines[0] != "2026/06/09 10:00:01.000000 [WARN] slow mcp start" {
t.Fatalf("unexpected first returned line: %s", snapshot.Lines[0])
}
if snapshot.Lines[1] != "2026/06/09 10:00:02.000000 [ERROR] mysql dial failed" {
t.Fatalf("unexpected second returned line: %s", snapshot.Lines[1])
}
}
func TestReadAppLogTailByPathFiltersByKeywordCaseInsensitively(t *testing.T) {
dir := t.TempDir()
logPath := filepath.Join(dir, "gonavi.log")
content := "" +
"2026/06/09 10:00:00.000000 [INFO] bootstrap ok\n" +
"2026/06/09 10:00:01.000000 [ERROR] MCP start failed\n" +
"2026/06/09 10:00:02.000000 [WARN] retry mcp connection\n"
if err := os.WriteFile(logPath, []byte(content), 0o644); err != nil {
t.Fatalf("write log failed: %v", err)
}
result := readAppLogTailByPath(logPath, 10, "mCp")
if !result.Success {
t.Fatalf("expected success, got failure: %s", result.Message)
}
snapshot, ok := result.Data.(appLogTailSnapshot)
if !ok {
t.Fatalf("expected appLogTailSnapshot, got %T", result.Data)
}
if snapshot.ReturnedLineCount != 2 {
t.Fatalf("expected 2 matched lines, got %d", snapshot.ReturnedLineCount)
}
if snapshot.Keyword != "mCp" {
t.Fatalf("expected original keyword to be preserved, got %q", snapshot.Keyword)
}
if snapshot.LevelBreakdown["ERROR"] != 1 || snapshot.LevelBreakdown["WARN"] != 1 {
t.Fatalf("unexpected level breakdown after keyword filter: %#v", snapshot.LevelBreakdown)
}
}