feat(ai-tools): 新增最近SQL日志探针

- 新增 inspect_recent_sql_logs 内置工具用于回看最近 SQL 执行历史
- 接入本地工具执行链,支持按成功或失败状态筛选日志
- 更新 AI 设置内置工具目录、流程说明和工具状态文案
- 完成 vitest、生产构建与预览页内置工具目录验证
This commit is contained in:
Syngnat
2026-06-08 16:10:09 +08:00
parent 0312dfbb16
commit 8a1e65640e
7 changed files with 137 additions and 1 deletions

View File

@@ -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(),

View File

@@ -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(
<AIBuiltinToolsCatalog
darkMode={false}
@@ -26,6 +26,8 @@ describe('AIBuiltinToolsCatalog', () => {
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');
});

View File

@@ -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',

View File

@@ -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', {

View File

@@ -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<string, AIToolContextEntry>;
sqlLogs?: SqlLog[];
runtime?: Partial<AILocalToolRuntime>;
}
@@ -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<ExecuteLocalAIToolCallResult> {
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) {

View File

@@ -34,6 +34,7 @@ const TOOL_ACTION_LABELS: Record<string, string> = {
get_table_ddl: '提取建表语句',
inspect_table_bundle: '抓取完整表结构快照',
inspect_database_bundle: '抓取数据库结构总览',
inspect_recent_sql_logs: '回看最近 SQL 执行日志',
preview_table_rows: '预览真实样例数据',
execute_sql: '执行只读 SQL 验证',
};

View File

@@ -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: "▶️",