From 8a1e65640e310c423133bd0c8d6de797ab8cdbb0 Mon Sep 17 00:00:00 2001 From: Syngnat Date: Mon, 8 Jun 2026 16:10:09 +0800 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20feat(ai-tools):=20=E6=96=B0?= =?UTF-8?q?=E5=A2=9E=E6=9C=80=E8=BF=91SQL=E6=97=A5=E5=BF=97=E6=8E=A2?= =?UTF-8?q?=E9=92=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 inspect_recent_sql_logs 内置工具用于回看最近 SQL 执行历史 - 接入本地工具执行链,支持按成功或失败状态筛选日志 - 更新 AI 设置内置工具目录、流程说明和工具状态文案 - 完成 vitest、生产构建与预览页内置工具目录验证 --- frontend/src/components/AIChatPanel.tsx | 1 + .../ai/AIBuiltinToolsCatalog.test.tsx | 4 +- .../components/ai/AIBuiltinToolsCatalog.tsx | 5 ++ .../components/ai/aiLocalToolExecutor.test.ts | 52 +++++++++++++++++++ .../src/components/ai/aiLocalToolExecutor.ts | 48 +++++++++++++++++ .../messageBubble/AIMessageStatusBlocks.tsx | 1 + frontend/src/utils/aiToolRegistry.ts | 27 ++++++++++ 7 files changed, 137 insertions(+), 1 deletion(-) diff --git a/frontend/src/components/AIChatPanel.tsx b/frontend/src/components/AIChatPanel.tsx index b835706..072305b 100644 --- a/frontend/src/components/AIChatPanel.tsx +++ b/frontend/src/components/AIChatPanel.tsx @@ -1264,6 +1264,7 @@ SELECT * FROM users WHERE status = 1; connections: currentConnections, mcpTools, toolContextMap: toolContextMapRef.current, + sqlLogs: useStore.getState().sqlLogs, }); const toolResultMsg: AIChatMessage = buildToolResultMessage({ id: genId(), diff --git a/frontend/src/components/ai/AIBuiltinToolsCatalog.test.tsx b/frontend/src/components/ai/AIBuiltinToolsCatalog.test.tsx index b03ca2a..1e91c3e 100644 --- a/frontend/src/components/ai/AIBuiltinToolsCatalog.test.tsx +++ b/frontend/src/components/ai/AIBuiltinToolsCatalog.test.tsx @@ -6,7 +6,7 @@ import AIBuiltinToolsCatalog from './AIBuiltinToolsCatalog'; import { buildOverlayWorkbenchTheme } from '../../utils/overlayWorkbenchTheme'; describe('AIBuiltinToolsCatalog', () => { - it('renders the field-to-table flow and both table-level and database-level snapshot tools', () => { + it('renders the field-to-table flow, sql log replay flow, and both snapshot tools', () => { const markup = renderToStaticMarkup( { expect(markup).toContain('inspect_table_bundle'); expect(markup).toContain('全库快速摸底'); expect(markup).toContain('inspect_database_bundle'); + expect(markup).toContain('回看最近执行记录'); + expect(markup).toContain('inspect_recent_sql_logs'); expect(markup).toContain('理解样例数据'); expect(markup).toContain('preview_table_rows'); }); diff --git a/frontend/src/components/ai/AIBuiltinToolsCatalog.tsx b/frontend/src/components/ai/AIBuiltinToolsCatalog.tsx index 7b62c45..f1e9ef4 100644 --- a/frontend/src/components/ai/AIBuiltinToolsCatalog.tsx +++ b/frontend/src/components/ai/AIBuiltinToolsCatalog.tsx @@ -37,6 +37,11 @@ const BUILTIN_TOOL_FLOWS = [ steps: 'inspect_database_bundle → inspect_table_bundle', description: '适合先看整库有哪些表、每张表大概有哪些字段,再对目标表继续做深挖快照。', }, + { + title: '回看最近执行记录', + steps: 'inspect_recent_sql_logs → get_columns / get_indexes / execute_sql', + description: '适合追查刚刚执行失败的 SQL、慢查询耗时,或基于真实执行历史继续让 AI 给解释和优化建议。', + }, { title: '理解样例数据', steps: 'get_columns → preview_table_rows', diff --git a/frontend/src/components/ai/aiLocalToolExecutor.test.ts b/frontend/src/components/ai/aiLocalToolExecutor.test.ts index 5477931..c0e3dc2 100644 --- a/frontend/src/components/ai/aiLocalToolExecutor.test.ts +++ b/frontend/src/components/ai/aiLocalToolExecutor.test.ts @@ -245,6 +245,58 @@ describe('aiLocalToolExecutor', () => { expect(result.content).toContain('"status":"paid"'); }); + it('returns recent sql logs and supports filtering only failed statements', async () => { + const result = await executeLocalAIToolCall({ + toolCall: buildToolCall('inspect_recent_sql_logs', { + limit: 2, + status: 'error', + }), + connections: [buildConnection()], + mcpTools: [], + toolContextMap: new Map(), + sqlLogs: [ + { + id: 'log-1', + timestamp: 3, + sql: 'DELETE FROM users WHERE id = 9', + status: 'error', + duration: 120, + message: 'permission denied', + dbName: 'crm', + }, + { + id: 'log-2', + timestamp: 2, + sql: 'SELECT * FROM users LIMIT 10', + status: 'success', + duration: 18, + dbName: 'crm', + affectedRows: 10, + }, + { + id: 'log-3', + timestamp: 1, + sql: 'UPDATE orders SET status = \'paid\' WHERE id = 1', + status: 'error', + duration: 95, + message: 'row lock timeout', + dbName: 'crm', + }, + ], + runtime: { + getDatabases: vi.fn(), + getTables: vi.fn(), + }, + }); + + expect(result.success).toBe(true); + expect(result.content).toContain('"status":"error"'); + expect(result.content).toContain('"totalMatched":2'); + expect(result.content).toContain('permission denied'); + expect(result.content).toContain('row lock timeout'); + expect(result.content).not.toContain('SELECT * FROM users LIMIT 10'); + }); + it('returns a database overview bundle with per-table column previews in one tool call', async () => { const result = await executeLocalAIToolCall({ toolCall: buildToolCall('inspect_database_bundle', { diff --git a/frontend/src/components/ai/aiLocalToolExecutor.ts b/frontend/src/components/ai/aiLocalToolExecutor.ts index a766d1c..4dae668 100644 --- a/frontend/src/components/ai/aiLocalToolExecutor.ts +++ b/frontend/src/components/ai/aiLocalToolExecutor.ts @@ -1,5 +1,6 @@ import { DBGetAllColumns, DBGetDatabases, DBGetTables } from '../../../wailsjs/go/app/App'; +import type { SqlLog } from '../../store'; import type { AIChatMessage, AIMCPToolDescriptor, AIToolCall, SavedConnection } from '../../types'; import { buildRpcConnectionConfig } from '../../utils/connectionRpcConfig'; import { buildAIReadonlyPreviewSQL } from '../../utils/aiSqlLimit'; @@ -31,6 +32,7 @@ export interface ExecuteLocalAIToolCallOptions { connections: SavedConnection[]; mcpTools: AIMCPToolDescriptor[]; toolContextMap: Map; + sqlLogs?: SqlLog[]; runtime?: Partial; } @@ -137,6 +139,21 @@ const normalizePerTableColumnLimit = (input: unknown): number => { return value; }; +const normalizeRecentSqlLogLimit = (input: unknown): number => { + const value = Math.floor(Number(input) || 20); + if (value < 1) return 1; + if (value > 100) return 100; + return value; +}; + +const normalizeSqlLogStatus = (input: unknown): 'all' | 'success' | 'error' => { + const value = String(input || 'all').trim().toLowerCase(); + if (value === 'success' || value === 'error') { + return value; + } + return 'all'; +}; + const buildPreviewSQLForTable = (connection: SavedConnection, tableName: string, limit: number): string => { const dbType = String(connection.config?.type || '').trim(); return buildPaginatedSelectSQL( @@ -153,6 +170,7 @@ export async function executeLocalAIToolCall({ connections, mcpTools, toolContextMap, + sqlLogs = [], runtime, }: ExecuteLocalAIToolCallOptions): Promise { const mergedRuntime = { ...buildDefaultRuntime(), ...(runtime || {}) }; @@ -556,6 +574,36 @@ export async function executeLocalAIToolCall({ } break; } + case 'inspect_recent_sql_logs': { + try { + const status = normalizeSqlLogStatus(args.status); + const limit = normalizeRecentSqlLogLimit(args.limit); + const filteredLogs = sqlLogs.filter((log) => status === 'all' || log.status === status); + const visibleLogs = filteredLogs.slice(0, limit).map((log) => ({ + id: log.id, + timestamp: log.timestamp, + status: log.status, + duration: log.duration, + dbName: log.dbName || '', + affectedRows: typeof log.affectedRows === 'number' ? log.affectedRows : null, + sql: log.sql, + message: log.message || '', + })); + + content = JSON.stringify({ + status, + limit, + totalMatched: filteredLogs.length, + successCount: filteredLogs.filter((log) => log.status === 'success').length, + errorCount: filteredLogs.filter((log) => log.status === 'error').length, + logs: visibleLogs, + }); + success = true; + } catch (error: any) { + content = `获取最近 SQL 日志失败: ${error?.message || error}`; + } + break; + } case 'preview_table_rows': { const connection = findConnection(connections, args.connectionId); if (!connection) { diff --git a/frontend/src/components/ai/messageBubble/AIMessageStatusBlocks.tsx b/frontend/src/components/ai/messageBubble/AIMessageStatusBlocks.tsx index 4dd7c6b..81cffa4 100644 --- a/frontend/src/components/ai/messageBubble/AIMessageStatusBlocks.tsx +++ b/frontend/src/components/ai/messageBubble/AIMessageStatusBlocks.tsx @@ -34,6 +34,7 @@ const TOOL_ACTION_LABELS: Record = { get_table_ddl: '提取建表语句', inspect_table_bundle: '抓取完整表结构快照', inspect_database_bundle: '抓取数据库结构总览', + inspect_recent_sql_logs: '回看最近 SQL 执行日志', preview_table_rows: '预览真实样例数据', execute_sql: '执行只读 SQL 验证', }; diff --git a/frontend/src/utils/aiToolRegistry.ts b/frontend/src/utils/aiToolRegistry.ts index c4a3834..73e233a 100644 --- a/frontend/src/utils/aiToolRegistry.ts +++ b/frontend/src/utils/aiToolRegistry.ts @@ -309,6 +309,33 @@ export const BUILTIN_AI_TOOL_INFO: AIBuiltinToolInfo[] = [ }, }, }, + { + 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: "execute_sql", icon: "▶️",