mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-07-01 23:31:22 +08:00
✨ feat(ai-tools): 新增日志与快捷键探针并完善 MCP 配置引导
This commit is contained in:
@@ -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 历史对话');
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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,无需重复写入');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 服务');
|
||||
|
||||
48
frontend/src/components/ai/aiAppLogInsights.test.ts
Normal file
48
frontend/src/components/ai/aiAppLogInsights.test.ts
Normal 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');
|
||||
});
|
||||
});
|
||||
67
frontend/src/components/ai/aiAppLogInsights.ts
Normal file
67
frontend/src/components/ai/aiAppLogInsights.ts
Normal 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}”的记录`
|
||||
: '最近日志里暂无可读记录',
|
||||
};
|
||||
};
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
@@ -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()),
|
||||
});
|
||||
|
||||
47
frontend/src/components/ai/aiShortcutInsights.test.ts
Normal file
47
frontend/src/components/ai/aiShortcutInsights.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
151
frontend/src/components/ai/aiShortcutInsights.ts
Normal file
151
frontend/src/components/ai/aiShortcutInsights.ts
Normal 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,
|
||||
};
|
||||
};
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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日志', '最近执行', '报错'] },
|
||||
];
|
||||
|
||||
@@ -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}`,
|
||||
|
||||
@@ -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>;
|
||||
}
|
||||
|
||||
|
||||
@@ -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('以下是当前用户的自定义补充提示词(数据库会话)');
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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 验证',
|
||||
};
|
||||
|
||||
@@ -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 }),
|
||||
|
||||
319
frontend/src/utils/aiBuiltinDatabaseToolInfo.ts
Normal file
319
frontend/src/utils/aiBuiltinDatabaseToolInfo.ts
Normal 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"],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
503
frontend/src/utils/aiBuiltinInspectionToolInfo.ts
Normal file
503
frontend/src/utils/aiBuiltinInspectionToolInfo.ts
Normal 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" },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -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,
|
||||
];
|
||||
|
||||
17
frontend/src/utils/aiBuiltinToolInfo.types.ts
Normal file
17
frontend/src/utils/aiBuiltinToolInfo.types.ts
Normal 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;
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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 },
|
||||
|
||||
2
frontend/wailsjs/go/app/App.d.ts
vendored
2
frontend/wailsjs/go/app/App.d.ts
vendored
@@ -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>;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
75
internal/app/methods_file_app_log_test.go
Normal file
75
internal/app/methods_file_app_log_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user