feat(ai-tools): 新增最近 SQL 活动总结探针

- 新增 inspect_recent_sql_activity 总结最近 SQL 活动的读写与报错分布
- 抽离 SQL 日志洞察模块并复用 recent logs 快照逻辑
- 补齐工具目录、系统提示、状态文案与测试覆盖
This commit is contained in:
Syngnat
2026-06-09 04:22:31 +08:00
parent b4f46aeecd
commit 6e7b8ceb39
12 changed files with 572 additions and 49 deletions

View File

@@ -56,6 +56,8 @@ describe('AIBuiltinToolsCatalog', () => {
expect(markup).toContain('inspect_workspace_tabs');
expect(markup).toContain('回看最近执行记录');
expect(markup).toContain('inspect_recent_sql_logs');
expect(markup).toContain('总结最近 SQL 活动');
expect(markup).toContain('inspect_recent_sql_activity');
expect(markup).toContain('复用历史 SQL');
expect(markup).toContain('inspect_saved_queries');
expect(markup).toContain('回看 AI 历史对话');

View File

@@ -112,6 +112,11 @@ const BUILTIN_TOOL_FLOWS = [
steps: 'inspect_recent_sql_logs → get_columns / get_indexes / execute_sql',
description: '适合追查刚刚执行失败的 SQL、慢查询耗时或基于真实执行历史继续让 AI 给解释和优化建议。',
},
{
title: '总结最近 SQL 活动',
steps: 'inspect_recent_sql_activity → inspect_recent_sql_logs → inspect_current_connection',
description: '适合先看最近到底以读还是写为主、有没有 DDL 或删除、哪个库最近报错最多,再决定继续下钻哪条日志或哪个连接。',
},
{
title: '复用历史 SQL',
steps: 'inspect_saved_queries → get_columns / execute_sql',

View File

@@ -988,6 +988,70 @@ describe('aiLocalToolExecutor', () => {
expect(result.content).not.toContain('SELECT * FROM users LIMIT 10');
});
it('returns a recent sql activity summary so the model can quickly spot writes, ddl, and repeated failures', async () => {
const result = await executeLocalAIToolCall({
toolCall: buildToolCall('inspect_recent_sql_activity', {
limit: 3,
activityKind: 'write',
dbName: 'crm',
}),
connections: [buildConnection()],
mcpTools: [],
toolContextMap: new Map(),
sqlLogs: [
{
id: 'log-1',
timestamp: 4,
sql: 'DELETE FROM users WHERE id = 9',
status: 'error',
duration: 120,
message: 'permission denied',
dbName: 'crm',
},
{
id: 'log-2',
timestamp: 3,
sql: 'UPDATE orders SET status = \'paid\' WHERE id = 1',
status: 'error',
duration: 95,
message: 'row lock timeout',
dbName: 'crm',
},
{
id: 'log-3',
timestamp: 2,
sql: 'ALTER TABLE orders ADD COLUMN note varchar(32)',
status: 'success',
duration: 160,
dbName: 'crm',
},
{
id: 'log-4',
timestamp: 1,
sql: 'SELECT * FROM users LIMIT 10',
status: 'success',
duration: 18,
dbName: 'crm',
affectedRows: 10,
},
],
runtime: {
getDatabases: vi.fn(),
getTables: vi.fn(),
},
});
expect(result.success).toBe(true);
expect(result.content).toContain('"activityKind":"write"');
expect(result.content).toContain('"totalMatched":2');
expect(result.content).toContain('"writeCount":2');
expect(result.content).toContain('"statementTypeBreakdown":{"delete":1,"update":1}');
expect(result.content).toContain('permission denied');
expect(result.content).toContain('row lock timeout');
expect(result.content).not.toContain('ALTER TABLE orders');
expect(result.content).not.toContain('SELECT * FROM users LIMIT 10');
});
it('returns local saved queries so the model can reuse historical sql scripts', async () => {
const result = await executeLocalAIToolCall({
toolCall: buildToolCall('inspect_saved_queries', {

View File

@@ -35,8 +35,11 @@ import { buildExternalSQLFileSnapshot } from './aiExternalSqlFileInsights';
import { buildExternalSQLDirectoriesSnapshot } from './aiExternalSqlInsights';
import { findBestMatchingExternalSQLDirectory } from './aiExternalSqlPathUtils';
import {
buildActiveTabSnapshot,
buildRecentSqlActivitySnapshot,
buildRecentSqlLogsSnapshot,
} from './aiSqlLogInsights';
import {
buildActiveTabSnapshot,
buildWorkspaceTabsSnapshot,
} from './aiWorkspaceInsights';
@@ -328,6 +331,18 @@ export async function executeSnapshotInspectionToolCall(
})),
success: true,
};
case 'inspect_recent_sql_activity':
return {
content: JSON.stringify(buildRecentSqlActivitySnapshot({
sqlLogs,
limit: args.limit,
status: args.status,
keyword: args.keyword,
dbName: args.dbName,
activityKind: args.activityKind,
})),
success: true,
};
case 'inspect_saved_queries':
return {
content: JSON.stringify(buildSavedQueriesSnapshot({
@@ -372,6 +387,7 @@ export async function executeSnapshotInspectionToolCall(
inspect_workspace_tabs: '读取当前工作区页签失败',
inspect_ai_context: '读取当前 AI 上下文失败',
inspect_recent_sql_logs: '获取最近 SQL 日志失败',
inspect_recent_sql_activity: '汇总最近 SQL 活动失败',
inspect_saved_queries: '读取已保存查询失败',
inspect_sql_snippets: '读取 SQL 片段失败',
}[toolName] || '读取本地探针快照失败';

View File

@@ -0,0 +1,120 @@
import { describe, expect, it } from 'vitest';
import { buildRecentSqlActivitySnapshot, buildRecentSqlLogsSnapshot } from './aiSqlLogInsights';
describe('aiSqlLogInsights', () => {
it('keeps recent sql logs as structured previews with inferred statement metadata', () => {
const snapshot = buildRecentSqlLogsSnapshot({
status: 'all',
limit: 2,
sqlLogs: [
{
id: 'log-1',
timestamp: 2,
sql: '/* note */ SELECT * FROM users',
status: 'success',
duration: 10,
dbName: 'crm',
affectedRows: 2,
},
{
id: 'log-2',
timestamp: 1,
sql: 'DELETE FROM users WHERE id = 9',
status: 'error',
duration: 50,
dbName: 'crm',
message: 'permission denied',
},
],
});
expect(snapshot.totalMatched).toBe(2);
expect(snapshot.logs[0]).toMatchObject({
statementType: 'select',
activityKind: 'read',
});
expect(snapshot.logs[1]).toMatchObject({
statementType: 'delete',
activityKind: 'write',
});
});
it('builds a recent sql activity summary with filters, breakdowns, slowest statements, and top errors', () => {
const snapshot = buildRecentSqlActivitySnapshot({
status: 'all',
activityKind: 'all',
keyword: 'orders',
dbName: 'crm',
limit: 5,
sqlLogs: [
{
id: 'log-1',
timestamp: 5,
sql: 'UPDATE orders SET status = \'paid\' WHERE id = 1',
status: 'error',
duration: 90,
dbName: 'crm',
message: 'row lock timeout',
},
{
id: 'log-2',
timestamp: 4,
sql: 'ALTER TABLE orders ADD COLUMN note varchar(32)',
status: 'success',
duration: 120,
dbName: 'crm',
},
{
id: 'log-3',
timestamp: 3,
sql: 'WITH recent AS (SELECT * FROM orders) SELECT * FROM recent',
status: 'success',
duration: 18,
dbName: 'crm',
},
{
id: 'log-4',
timestamp: 2,
sql: 'SET search_path TO analytics',
status: 'success',
duration: 5,
dbName: 'crm',
},
{
id: 'log-5',
timestamp: 1,
sql: 'SELECT * FROM users',
status: 'success',
duration: 12,
dbName: 'crm',
},
],
});
expect(snapshot.totalMatched).toBe(3);
expect(snapshot.writeCount).toBe(1);
expect(snapshot.ddlCount).toBe(1);
expect(snapshot.readCount).toBe(1);
expect(snapshot.statementTypeBreakdown).toEqual({
alter: 1,
update: 1,
with: 1,
});
expect(snapshot.dbBreakdown).toEqual({
crm: 3,
});
expect(snapshot.topErrorMessages).toEqual([
{ message: 'row lock timeout', count: 1 },
]);
expect(snapshot.slowestStatements[0]).toMatchObject({
statementType: 'alter',
activityKind: 'ddl',
});
expect(snapshot.recentMutations).toHaveLength(2);
expect(snapshot.recentErrors[0]).toMatchObject({
statementType: 'update',
activityKind: 'write',
});
});
});

View File

@@ -0,0 +1,316 @@
import type { SqlLog } from '../../store';
type SqlLogStatusFilter = 'all' | 'success' | 'error';
type SqlActivityKind = 'read' | 'write' | 'ddl' | 'transaction' | 'session' | 'other';
type SqlActivityKindFilter = 'all' | SqlActivityKind;
type SqlStatementType =
| 'select'
| 'insert'
| 'update'
| 'delete'
| 'replace'
| 'merge'
| 'create'
| 'alter'
| 'drop'
| 'truncate'
| 'rename'
| 'show'
| 'describe'
| 'explain'
| 'use'
| 'set'
| 'begin'
| 'commit'
| 'rollback'
| 'with'
| 'other';
const MAX_SQL_LOG_LIMIT = 100;
const DEFAULT_SQL_LOG_LIMIT = 20;
const DEFAULT_SQL_ACTIVITY_LIMIT = 30;
const normalizeSqlLogLimit = (input: unknown, fallback = DEFAULT_SQL_LOG_LIMIT): number => {
const value = Math.floor(Number(input) || fallback);
if (value < 1) return 1;
if (value > MAX_SQL_LOG_LIMIT) return MAX_SQL_LOG_LIMIT;
return value;
};
const normalizeSqlLogStatus = (input: unknown): SqlLogStatusFilter => {
const value = String(input || 'all').trim().toLowerCase();
if (value === 'success' || value === 'error') {
return value;
}
return 'all';
};
const normalizeSqlActivityKind = (input: unknown): SqlActivityKindFilter => {
const value = String(input || 'all').trim().toLowerCase();
if (
value === 'read'
|| value === 'write'
|| value === 'ddl'
|| value === 'transaction'
|| value === 'session'
|| value === 'other'
) {
return value;
}
return 'all';
};
const stripLeadingSqlComments = (input: string): string => {
let text = String(input || '');
while (true) {
const trimmedStart = text.trimStart();
if (!trimmedStart) {
return '';
}
if (trimmedStart.startsWith('--') || trimmedStart.startsWith('#')) {
const lineEnd = trimmedStart.indexOf('\n');
text = lineEnd >= 0 ? trimmedStart.slice(lineEnd + 1) : '';
continue;
}
if (trimmedStart.startsWith('/*')) {
const blockEnd = trimmedStart.indexOf('*/');
if (blockEnd < 0) {
return '';
}
text = trimmedStart.slice(blockEnd + 2);
continue;
}
return trimmedStart;
}
};
const resolveWithStatementType = (normalizedSql: string): SqlStatementType => {
const writePatterns: Array<{ keyword: SqlStatementType; regex: RegExp }> = [
{ keyword: 'insert', regex: /\binsert\s+into\b/u },
{ keyword: 'update', regex: /\bupdate\b/u },
{ keyword: 'delete', regex: /\bdelete\s+from\b/u },
{ keyword: 'replace', regex: /\breplace\s+into\b/u },
{ keyword: 'merge', regex: /\bmerge\s+into\b/u },
];
const ddlPatterns: Array<{ keyword: SqlStatementType; regex: RegExp }> = [
{ keyword: 'create', regex: /\bcreate\s+(table|view|index|schema|database)\b/u },
{ keyword: 'alter', regex: /\balter\s+(table|view|index|schema|database)\b/u },
{ keyword: 'drop', regex: /\bdrop\s+(table|view|index|schema|database)\b/u },
{ keyword: 'truncate', regex: /\btruncate\s+table\b/u },
{ keyword: 'rename', regex: /\brename\s+(table|to)\b/u },
];
const writeMatch = writePatterns.find((item) => item.regex.test(normalizedSql));
if (writeMatch) {
return writeMatch.keyword;
}
const ddlMatch = ddlPatterns.find((item) => item.regex.test(normalizedSql));
if (ddlMatch) {
return ddlMatch.keyword;
}
return /\bselect\b/u.test(normalizedSql) ? 'with' : 'other';
};
const classifySqlStatement = (sql: string): { statementType: SqlStatementType; activityKind: SqlActivityKind } => {
const normalizedSql = stripLeadingSqlComments(sql).toLowerCase();
if (!normalizedSql) {
return { statementType: 'other', activityKind: 'other' };
}
const firstKeyword = normalizedSql.match(/^[a-z]+/u)?.[0] || 'other';
const statementType: SqlStatementType = (() => {
switch (firstKeyword) {
case 'select':
case 'insert':
case 'update':
case 'delete':
case 'replace':
case 'merge':
case 'create':
case 'alter':
case 'drop':
case 'truncate':
case 'rename':
case 'show':
case 'use':
case 'set':
case 'begin':
case 'commit':
case 'rollback':
return firstKeyword;
case 'desc':
case 'describe':
return 'describe';
case 'explain':
return 'explain';
case 'with':
return resolveWithStatementType(normalizedSql);
default:
return 'other';
}
})();
switch (statementType) {
case 'select':
case 'show':
case 'describe':
case 'explain':
case 'with':
return { statementType, activityKind: 'read' };
case 'insert':
case 'update':
case 'delete':
case 'replace':
case 'merge':
return { statementType, activityKind: 'write' };
case 'create':
case 'alter':
case 'drop':
case 'truncate':
case 'rename':
return { statementType, activityKind: 'ddl' };
case 'begin':
case 'commit':
case 'rollback':
return { statementType, activityKind: 'transaction' };
case 'use':
case 'set':
return { statementType, activityKind: 'session' };
default:
return { statementType: 'other', activityKind: 'other' };
}
};
const buildCountBreakdown = (items: string[]): Record<string, number> =>
Object.fromEntries(
Array.from(
items.reduce((map, item) => {
const key = String(item || 'unknown').trim() || 'unknown';
map.set(key, (map.get(key) || 0) + 1);
return map;
}, new Map<string, number>()).entries(),
).sort((left, right) => right[1] - left[1] || left[0].localeCompare(right[0])),
);
const buildLogPreview = (log: SqlLog) => {
const classification = classifySqlStatement(log.sql);
return {
id: log.id,
timestamp: log.timestamp,
status: log.status,
duration: log.duration,
dbName: log.dbName || '',
affectedRows: typeof log.affectedRows === 'number' ? log.affectedRows : null,
statementType: classification.statementType,
activityKind: classification.activityKind,
sql: log.sql,
message: log.message || '',
};
};
export const buildRecentSqlLogsSnapshot = (params: {
sqlLogs?: SqlLog[];
limit?: unknown;
status?: unknown;
}) => {
const { sqlLogs = [], limit, status } = params;
const safeStatus = normalizeSqlLogStatus(status);
const safeLimit = normalizeSqlLogLimit(limit);
const filteredLogs = sqlLogs.filter((log) => safeStatus === 'all' || log.status === safeStatus);
return {
status: safeStatus,
limit: safeLimit,
totalMatched: filteredLogs.length,
successCount: filteredLogs.filter((log) => log.status === 'success').length,
errorCount: filteredLogs.filter((log) => log.status === 'error').length,
logs: filteredLogs.slice(0, safeLimit).map(buildLogPreview),
};
};
export const buildRecentSqlActivitySnapshot = (params: {
sqlLogs?: SqlLog[];
limit?: unknown;
status?: unknown;
keyword?: unknown;
dbName?: unknown;
activityKind?: unknown;
}) => {
const { sqlLogs = [], limit, status, keyword, dbName, activityKind } = params;
const safeLimit = normalizeSqlLogLimit(limit, DEFAULT_SQL_ACTIVITY_LIMIT);
const safeStatus = normalizeSqlLogStatus(status);
const safeKeyword = String(keyword || '').trim().toLowerCase();
const safeDbName = String(dbName || '').trim().toLowerCase();
const safeActivityKind = normalizeSqlActivityKind(activityKind);
const enrichedLogs = sqlLogs.map(buildLogPreview);
const filteredLogs = enrichedLogs.filter((log) => {
if (safeStatus !== 'all' && log.status !== safeStatus) {
return false;
}
if (safeActivityKind !== 'all' && log.activityKind !== safeActivityKind) {
return false;
}
if (safeDbName && !String(log.dbName || '').toLowerCase().includes(safeDbName)) {
return false;
}
if (safeKeyword) {
const haystack = [
log.dbName,
log.statementType,
log.activityKind,
log.sql,
log.message,
].join('\n').toLowerCase();
if (!haystack.includes(safeKeyword)) {
return false;
}
}
return true;
});
const statementTypeBreakdown = buildCountBreakdown(filteredLogs.map((log) => log.statementType));
const dbBreakdown = buildCountBreakdown(filteredLogs.map((log) => log.dbName || '(未指定数据库)'));
const errorMessageBreakdown = buildCountBreakdown(
filteredLogs
.filter((log) => log.status === 'error' && String(log.message || '').trim())
.map((log) => String(log.message || '').trim()),
);
const recentExamples = filteredLogs.slice(0, safeLimit);
const recentMutations = filteredLogs
.filter((log) => log.activityKind === 'write' || log.activityKind === 'ddl')
.slice(0, 5);
const recentErrors = filteredLogs
.filter((log) => log.status === 'error')
.slice(0, 5);
const slowestStatements = [...filteredLogs]
.sort((left, right) => right.duration - left.duration || right.timestamp - left.timestamp)
.slice(0, 5);
return {
status: safeStatus,
activityKind: safeActivityKind,
keyword: safeKeyword,
dbName: safeDbName,
limit: safeLimit,
totalMatched: filteredLogs.length,
successCount: filteredLogs.filter((log) => log.status === 'success').length,
errorCount: filteredLogs.filter((log) => log.status === 'error').length,
readCount: filteredLogs.filter((log) => log.activityKind === 'read').length,
writeCount: filteredLogs.filter((log) => log.activityKind === 'write').length,
ddlCount: filteredLogs.filter((log) => log.activityKind === 'ddl').length,
transactionCount: filteredLogs.filter((log) => log.activityKind === 'transaction').length,
sessionCount: filteredLogs.filter((log) => log.activityKind === 'session').length,
otherCount: filteredLogs.filter((log) => log.activityKind === 'other').length,
statementTypeBreakdown,
dbBreakdown,
topErrorMessages: Object.entries(errorMessageBreakdown)
.slice(0, 5)
.map(([message, count]) => ({ message, count })),
recentMutations,
recentErrors,
slowestStatements,
recentExamples,
};
};

View File

@@ -68,7 +68,7 @@ describe('buildAISystemContextMessages', () => {
connections: [connections[0]],
tabs: [],
activeTabId: null,
availableToolNames: ['inspect_workspace_tabs', 'inspect_ai_runtime', 'inspect_ai_safety', 'inspect_ai_providers', 'inspect_ai_chat_readiness', 'inspect_mcp_setup', 'inspect_ai_guidance', 'inspect_ai_context', 'inspect_current_connection', 'inspect_connection_capabilities', 'inspect_saved_connections', 'inspect_external_sql_directories', 'inspect_external_sql_file', 'inspect_saved_queries', 'inspect_ai_sessions', 'inspect_sql_snippets', 'get_columns'],
availableToolNames: ['inspect_workspace_tabs', 'inspect_ai_runtime', 'inspect_ai_safety', 'inspect_ai_providers', 'inspect_ai_chat_readiness', 'inspect_mcp_setup', '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'],
skills,
userPromptSettings,
});
@@ -87,6 +87,7 @@ describe('buildAISystemContextMessages', () => {
expect(joined).toContain('inspect_saved_connections');
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_saved_queries');
expect(joined).toContain('inspect_ai_sessions');
expect(joined).toContain('inspect_sql_snippets');

View File

@@ -435,6 +435,12 @@ SELECT * FROM users WHERE status = 1;
content: '如果用户已经给出了某个外部 SQL 文件路径,或明确提到“帮我看看这个目录里的 report.sql / job.sql 在写什么”,优先调用 inspect_external_sql_file 读取真实文件内容;如果这个文件已经在编辑器中打开,再结合 inspect_active_tab 看当前草稿。',
});
}
if (availableToolNames.includes('inspect_recent_sql_activity')) {
systemMessages.push({
role: 'system',
content: '如果用户提到“最近都执行了什么”“是不是刚删过数据”“最近主要在查还是在改”“哪个库最近报错最多”,优先调用 inspect_recent_sql_activity 先读最近 SQL 活动总结,再决定是否继续下钻 inspect_recent_sql_logs 看具体语句。',
});
}
if (availableToolNames.includes('inspect_saved_queries')) {
systemMessages.push({
role: 'system',

View File

@@ -1,4 +1,3 @@
import type { SqlLog } from '../../store';
import type { SavedConnection, TabData } from '../../types';
const ACTIVE_TAB_CONTENT_LIMIT = 12000;
@@ -11,21 +10,6 @@ const normalizeWorkspaceTabLimit = (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 resolveContentKind = (tab: TabData, includeContent: boolean, trimmedContent: string): 'sql' | 'command' | 'text' | 'none' => {
if (!includeContent || !trimmedContent) {
return 'none';
@@ -160,33 +144,3 @@ export const buildWorkspaceTabsSnapshot = (params: {
})),
};
};
export const buildRecentSqlLogsSnapshot = (params: {
sqlLogs?: SqlLog[];
limit?: unknown;
status?: unknown;
}) => {
const { sqlLogs = [], limit, status } = params;
const safeStatus = normalizeSqlLogStatus(status);
const safeLimit = normalizeRecentSqlLogLimit(limit);
const filteredLogs = sqlLogs.filter((log) => safeStatus === 'all' || log.status === safeStatus);
const visibleLogs = filteredLogs.slice(0, safeLimit).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 || '',
}));
return {
status: safeStatus,
limit: safeLimit,
totalMatched: filteredLogs.length,
successCount: filteredLogs.filter((log) => log.status === 'success').length,
errorCount: filteredLogs.filter((log) => log.status === 'error').length,
logs: visibleLogs,
};
};

View File

@@ -49,6 +49,7 @@ const TOOL_ACTION_LABELS: Record<string, string> = {
inspect_active_tab: '读取当前活动页签',
inspect_workspace_tabs: '盘点当前工作区页签',
inspect_recent_sql_logs: '回看最近 SQL 执行日志',
inspect_recent_sql_activity: '总结最近 SQL 活动',
inspect_saved_queries: '检索本地已保存查询',
inspect_sql_snippets: '读取 SQL 片段模板',
preview_table_rows: '预览真实样例数据',

View File

@@ -616,6 +616,40 @@ export const BUILTIN_AI_TOOL_INFO: AIBuiltinToolInfo[] = [
},
},
},
{
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: "💾",

View File

@@ -80,11 +80,14 @@ describe('aiToolRegistry', () => {
expect(info?.tool.function.description).toContain('目录中的具体 SQL 脚本');
});
it('registers the saved-query and sql-snippet inspectors as builtin tools', () => {
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 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(savedQueryTool?.desc).toContain('已保存的 SQL 查询');
expect(savedQueryTool?.tool.function.description).toContain('历史查询');
expect(aiSessionsTool?.desc).toContain('AI 历史会话');
@@ -120,6 +123,7 @@ describe('aiToolRegistry', () => {
expect(tools.some((item) => item.function.name === 'inspect_saved_connections')).toBe(true);
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_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);