diff --git a/frontend/src/components/ai/AIBuiltinToolsCatalog.test.tsx b/frontend/src/components/ai/AIBuiltinToolsCatalog.test.tsx index 8e44884..a82b34f 100644 --- a/frontend/src/components/ai/AIBuiltinToolsCatalog.test.tsx +++ b/frontend/src/components/ai/AIBuiltinToolsCatalog.test.tsx @@ -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 历史对话'); diff --git a/frontend/src/components/ai/AIBuiltinToolsCatalog.tsx b/frontend/src/components/ai/AIBuiltinToolsCatalog.tsx index 6a68abf..a65df2f 100644 --- a/frontend/src/components/ai/AIBuiltinToolsCatalog.tsx +++ b/frontend/src/components/ai/AIBuiltinToolsCatalog.tsx @@ -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', diff --git a/frontend/src/components/ai/AIMCPClientInstallPanel.test.tsx b/frontend/src/components/ai/AIMCPClientInstallPanel.test.tsx index e2c727a..6fe49ed 100644 --- a/frontend/src/components/ai/AIMCPClientInstallPanel.test.tsx +++ b/frontend/src/components/ai/AIMCPClientInstallPanel.test.tsx @@ -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( + {}} + onRefreshStatus={() => {}} + onCopyConfigPath={() => {}} + onCopyLaunchCommand={() => {}} + onInstall={() => {}} + />, + ); + + expect(markup).toContain('Claude Code 状态'); + expect(markup).toContain('当前状态:已写入当前 GoNavi,无需重复写入'); + }); }); diff --git a/frontend/src/components/ai/AIMCPClientInstallPanel.tsx b/frontend/src/components/ai/AIMCPClientInstallPanel.tsx index 4b16974..4f5fc7c 100644 --- a/frontend/src/components/ai/AIMCPClientInstallPanel.tsx +++ b/frontend/src/components/ai/AIMCPClientInstallPanel.tsx @@ -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 = ({ }} >
- 这里是在给外部客户端接入 GoNavi MCP,不是给 GoNavi 自己安装 MCP。 + 这里是在把当前 GoNavi 的 MCP 启动配置写给外部客户端,不是给 GoNavi 自己安装 MCP。
- 你只需要选中 Claude Code 或 Codex 其中一个目标,GoNavi 就会把“如何启动当前这份 GoNavi MCP”的配置写入那个客户端的用户级配置文件。 + 你只需要选中 Claude Code 或 Codex 其中一个目标,GoNavi 就会把“如何启动当前这份 GoNavi MCP”的配置写入那个客户端的用户级配置文件,不会重装 GoNavi,也不会替换 GoNavi 自己的程序文件。
-
接入外部客户端
+
写入外部客户端配置
- 先选择 1 个目标客户端,再执行写入或更新。GoNavi 会自动把当前安装路径写入它的用户级 MCP 配置文件,不需要你自己找本机 exe 或手动改配置。 + 先选择 1 个目标客户端,再执行写入或更新。GoNavi 会自动把当前安装路径写入它的用户级 MCP 配置文件,不需要你自己找本机 exe,也不需要手动改配置。
{ />, ); - 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 服务'); diff --git a/frontend/src/components/ai/aiAppLogInsights.test.ts b/frontend/src/components/ai/aiAppLogInsights.test.ts new file mode 100644 index 0000000..0216f5d --- /dev/null +++ b/frontend/src/components/ai/aiAppLogInsights.test.ts @@ -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'); + }); +}); diff --git a/frontend/src/components/ai/aiAppLogInsights.ts b/frontend/src/components/ai/aiAppLogInsights.ts new file mode 100644 index 0000000..aa47b7f --- /dev/null +++ b/frontend/src/components/ai/aiAppLogInsights.ts @@ -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 + : {}; + 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}”的记录` + : '最近日志里暂无可读记录', + }; +}; diff --git a/frontend/src/components/ai/aiLocalToolExecutor.appLogInspection.test.ts b/frontend/src/components/ai/aiLocalToolExecutor.appLogInspection.test.ts new file mode 100644 index 0000000..0f588dc --- /dev/null +++ b/frontend/src/components/ai/aiLocalToolExecutor.appLogInspection.test.ts @@ -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, +): 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'); + }); +}); diff --git a/frontend/src/components/ai/aiLocalToolExecutor.shortcutInspection.test.ts b/frontend/src/components/ai/aiLocalToolExecutor.shortcutInspection.test.ts new file mode 100644 index 0000000..f16b540 --- /dev/null +++ b/frontend/src/components/ai/aiLocalToolExecutor.shortcutInspection.test.ts @@ -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, +): 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'); + }); +}); diff --git a/frontend/src/components/ai/aiLocalToolRuntime.ts b/frontend/src/components/ai/aiLocalToolRuntime.ts index 47dbeb4..3009a1c 100644 --- a/frontend/src/components/ai/aiLocalToolRuntime.ts +++ b/frontend/src/components/ai/aiLocalToolRuntime.ts @@ -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; getTables: (config: any, dbName: string) => Promise; getAllColumns: (config: any, dbName: string) => Promise; + readAppLogTail: (lineLimit: number, keyword: string) => Promise; readSQLFile: (filePath: string) => Promise; getColumns: (config: any, dbName: string, tableName: string) => Promise; getIndexes: (config: any, dbName: string, tableName: string) => Promise; @@ -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()), }); diff --git a/frontend/src/components/ai/aiShortcutInsights.test.ts b/frontend/src/components/ai/aiShortcutInsights.test.ts new file mode 100644 index 0000000..b21c286 --- /dev/null +++ b/frontend/src/components/ai/aiShortcutInsights.test.ts @@ -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); + }); +}); diff --git a/frontend/src/components/ai/aiShortcutInsights.ts b/frontend/src/components/ai/aiShortcutInsights.ts new file mode 100644 index 0000000..dc2a139 --- /dev/null +++ b/frontend/src/components/ai/aiShortcutInsights.ts @@ -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 | 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 | 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 => 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, + }; +}; diff --git a/frontend/src/components/ai/aiSlashCommands.test.ts b/frontend/src/components/ai/aiSlashCommands.test.ts index 9359ba3..31c59db 100644 --- a/frontend/src/components/ai/aiSlashCommands.test.ts +++ b/frontend/src/components/ai/aiSlashCommands.test.ts @@ -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'); }); }); diff --git a/frontend/src/components/ai/aiSlashCommands.ts b/frontend/src/components/ai/aiSlashCommands.ts index 9e6da24..382ff28 100644 --- a/frontend/src/components/ai/aiSlashCommands.ts +++ b/frontend/src/components/ai/aiSlashCommands.ts @@ -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日志', '最近执行', '报错'] }, ]; diff --git a/frontend/src/components/ai/aiSnapshotInspectionToolExecutor.ts b/frontend/src/components/ai/aiSnapshotInspectionToolExecutor.ts index 2752214..b2a38d4 100644 --- a/frontend/src/components/ai/aiSnapshotInspectionToolExecutor.ts +++ b/frontend/src/components/ai/aiSnapshotInspectionToolExecutor.ts @@ -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}`, diff --git a/frontend/src/components/ai/aiSnapshotInspectionToolTypes.ts b/frontend/src/components/ai/aiSnapshotInspectionToolTypes.ts index 433fd79..cea9c90 100644 --- a/frontend/src/components/ai/aiSnapshotInspectionToolTypes.ts +++ b/frontend/src/components/ai/aiSnapshotInspectionToolTypes.ts @@ -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; getMCPServers?: () => Promise; getMCPClientInstallStatuses?: () => Promise; + getShortcutOptions?: () => Promise; + getShortcutPlatform?: () => Promise; + readAppLogTail?: (lineLimit: number, keyword: string) => Promise; readSQLFile?: (filePath: string) => Promise; } diff --git a/frontend/src/components/ai/aiSystemContextMessages.test.ts b/frontend/src/components/ai/aiSystemContextMessages.test.ts index 24e74ea..de8e5d7 100644 --- a/frontend/src/components/ai/aiSystemContextMessages.test.ts +++ b/frontend/src/components/ai/aiSystemContextMessages.test.ts @@ -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('以下是当前用户的自定义补充提示词(数据库会话)'); diff --git a/frontend/src/components/ai/aiSystemContextMessages.ts b/frontend/src/components/ai/aiSystemContextMessages.ts index 42a25f2..6671948 100644 --- a/frontend/src/components/ai/aiSystemContextMessages.ts +++ b/frontend/src/components/ai/aiSystemContextMessages.ts @@ -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', diff --git a/frontend/src/components/ai/messageBubble/AIMessageStatusBlocks.tsx b/frontend/src/components/ai/messageBubble/AIMessageStatusBlocks.tsx index af85932..58d32ef 100644 --- a/frontend/src/components/ai/messageBubble/AIMessageStatusBlocks.tsx +++ b/frontend/src/components/ai/messageBubble/AIMessageStatusBlocks.tsx @@ -51,8 +51,10 @@ const TOOL_ACTION_LABELS: Record = { 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 验证', }; diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx index e19c368..26509c6 100644 --- a/frontend/src/main.tsx +++ b/frontend/src/main.tsx @@ -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 }), diff --git a/frontend/src/utils/aiBuiltinDatabaseToolInfo.ts b/frontend/src/utils/aiBuiltinDatabaseToolInfo.ts new file mode 100644 index 0000000..8636600 --- /dev/null +++ b/frontend/src/utils/aiBuiltinDatabaseToolInfo.ts @@ -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"], + }, + }, + }, + }, +]; diff --git a/frontend/src/utils/aiBuiltinInspectionToolInfo.ts b/frontend/src/utils/aiBuiltinInspectionToolInfo.ts new file mode 100644 index 0000000..39959bd --- /dev/null +++ b/frontend/src/utils/aiBuiltinInspectionToolInfo.ts @@ -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" }, + }, + }, + }, + }, + }, +]; diff --git a/frontend/src/utils/aiBuiltinToolInfo.ts b/frontend/src/utils/aiBuiltinToolInfo.ts index 465a567..44ea2c2 100644 --- a/frontend/src/utils/aiBuiltinToolInfo.ts +++ b/frontend/src/utils/aiBuiltinToolInfo.ts @@ -1,786 +1,12 @@ -export interface AIChatToolDefinition { - type: "function"; - function: { - name: string; - description: string; - parameters: Record; - }; -} +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, ]; diff --git a/frontend/src/utils/aiBuiltinToolInfo.types.ts b/frontend/src/utils/aiBuiltinToolInfo.types.ts new file mode 100644 index 0000000..9c6034f --- /dev/null +++ b/frontend/src/utils/aiBuiltinToolInfo.types.ts @@ -0,0 +1,17 @@ +export interface AIChatToolDefinition { + type: "function"; + function: { + name: string; + description: string; + parameters: Record; + }; +} + +export interface AIBuiltinToolInfo { + name: string; + icon: string; + desc: string; + detail: string; + params: string; + tool: AIChatToolDefinition; +} diff --git a/frontend/src/utils/aiToolRegistry.test.ts b/frontend/src/utils/aiToolRegistry.test.ts index be16e6f..d1f1f99 100644 --- a/frontend/src/utils/aiToolRegistry.test.ts +++ b/frontend/src/utils/aiToolRegistry.test.ts @@ -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); }); }); diff --git a/frontend/src/utils/mcpClientInstallStatus.test.ts b/frontend/src/utils/mcpClientInstallStatus.test.ts index 6c8171a..1b4dafc 100644 --- a/frontend/src/utils/mcpClientInstallStatus.test.ts +++ b/frontend/src/utils/mcpClientInstallStatus.test.ts @@ -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'); }); diff --git a/frontend/src/utils/mcpClientInstallStatus.ts b/frontend/src/utils/mcpClientInstallStatus.ts index 6a2edba..52698de 100644 --- a/frontend/src/utils/mcpClientInstallStatus.ts +++ b/frontend/src/utils/mcpClientInstallStatus.ts @@ -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) { diff --git a/frontend/src/utils/shortcuts.test.ts b/frontend/src/utils/shortcuts.test.ts index c896a3e..e9c280a 100644 --- a/frontend/src/utils/shortcuts.test.ts +++ b/frontend/src/utils/shortcuts.test.ts @@ -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', () => { diff --git a/frontend/src/utils/shortcuts.ts b/frontend/src/utils/shortcuts.ts index 937b609..338e631 100644 --- a/frontend/src/utils/shortcuts.ts +++ b/frontend/src/utils/shortcuts.ts @@ -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 = 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 }, diff --git a/frontend/wailsjs/go/app/App.d.ts b/frontend/wailsjs/go/app/App.d.ts index 6a76148..9dc835f 100755 --- a/frontend/wailsjs/go/app/App.d.ts +++ b/frontend/wailsjs/go/app/App.d.ts @@ -202,6 +202,8 @@ export function PreviewImportFile(arg1:string):Promise; export function ReadSQLFile(arg1:string):Promise; +export function ReadAppLogTail(arg1:number,arg2:string):Promise; + export function RedisConnect(arg1:connection.ConnectionConfig):Promise; export function RedisDeleteHashField(arg1:connection.ConnectionConfig,arg2:string,arg3:any):Promise; diff --git a/frontend/wailsjs/go/app/App.js b/frontend/wailsjs/go/app/App.js index 0cfd05e..5bf55fe 100755 --- a/frontend/wailsjs/go/app/App.js +++ b/frontend/wailsjs/go/app/App.js @@ -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); } diff --git a/internal/app/methods_file.go b/internal/app/methods_file.go index 0accc15..92bc690 100644 --- a/internal/app/methods_file.go +++ b/internal/app/methods_file.go @@ -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) } diff --git a/internal/app/methods_file_app_log_test.go b/internal/app/methods_file_app_log_test.go new file mode 100644 index 0000000..e06c216 --- /dev/null +++ b/internal/app/methods_file_app_log_test.go @@ -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) + } +}