mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-06-15 02:49:49 +08:00
✨ feat(ai-tools): 新增工作区页签探针并拆分界面洞察模块
This commit is contained in:
@@ -30,6 +30,10 @@ describe('AIChatPanel message render isolation', () => {
|
||||
|
||||
it('teaches the runtime to use deeper schema tools when analyzing structure details', () => {
|
||||
expect(source).toContain('get_indexes、get_foreign_keys、get_triggers、get_table_ddl');
|
||||
expect(source).toContain('inspect_active_tab 读取当前活动页签上下文');
|
||||
expect(source).toContain('inspect_workspace_tabs 盘点当前工作区');
|
||||
expect(source).toContain('tabs: useStore.getState().tabs');
|
||||
expect(source).toContain('activeTabId: useStore.getState().activeTabId');
|
||||
expect(source).toContain('toolContextMap: toolContextMapRef.current');
|
||||
expect(source).toContain('buildToolResultMessage');
|
||||
});
|
||||
|
||||
@@ -1209,12 +1209,14 @@ ${resourcePath ? `当前资源路径:${resourcePath}` : '当前未选中具体
|
||||
6. 如果是常规问答(不涉及数据库查询)则正常作答即可。
|
||||
|
||||
SQL 生成规则(极重要,必须严格遵守):
|
||||
7. 【字段精确性 - 绝对红线】生成 SQL 之前,必须先调用 get_columns 获取目标表的真实字段列表。SQL 中的每一个字段名必须与 get_columns 返回的 field 字段完全一致(区分大小写)。不得自行拼凑、缩写或联想字段名(例如字段是 channel 就必须写 channel,不得写成 pay_channel)。
|
||||
8. 如果用户在问索引优化、联表关系、触发器副作用、约束或 DDL 细节,在 get_columns 之后继续按需调用 get_indexes、get_foreign_keys、get_triggers、get_table_ddl,再给结论。
|
||||
9. 生成 SQL 时禁止使用 "database.table" 格式的限定前缀,只写表名本身。
|
||||
10. 报告结果时,连接名/ID 和数据库名必须严格来自同一个 get_tables 调用的实际参数。禁止将 A 连接的 connectionId 与 B 连接的 dbName 混搭。
|
||||
11. 如果有多个名称相似的数据库,请明确告诉用户目标表具体位于哪个数据库。
|
||||
12. 【关键】每个 SQL 代码块的第一行必须添加上下文声明注释,格式严格为:-- @context connectionId=<连接ID> dbName=<数据库名>。connectionId 和 dbName 必须来自同一个成功的 get_tables 调用(即你在该调用中传入的实际参数值)。示例:
|
||||
7. 如果用户提到“当前页签”“当前 SQL”“当前编辑器”“这条语句”,但消息里没有贴出具体内容,优先调用 inspect_active_tab 读取当前活动页签上下文,不要猜测当前工作区里打开的内容。
|
||||
8. 如果用户提到“当前开了哪些页签”“工作区里有哪些 tab”“我现在打开了哪些查询”,优先调用 inspect_workspace_tabs 盘点当前工作区,再决定深入哪个页签。
|
||||
9. 【字段精确性 - 绝对红线】生成 SQL 之前,必须先调用 get_columns 获取目标表的真实字段列表。SQL 中的每一个字段名必须与 get_columns 返回的 field 字段完全一致(区分大小写)。不得自行拼凑、缩写或联想字段名(例如字段是 channel 就必须写 channel,不得写成 pay_channel)。
|
||||
10. 如果用户在问索引优化、联表关系、触发器副作用、约束或 DDL 细节,在 get_columns 之后继续按需调用 get_indexes、get_foreign_keys、get_triggers、get_table_ddl,再给结论。
|
||||
11. 生成 SQL 时禁止使用 "database.table" 格式的限定前缀,只写表名本身。
|
||||
12. 报告结果时,连接名/ID 和数据库名必须严格来自同一个 get_tables 调用的实际参数。禁止将 A 连接的 connectionId 与 B 连接的 dbName 混搭。
|
||||
13. 如果有多个名称相似的数据库,请明确告诉用户目标表具体位于哪个数据库。
|
||||
14. 【关键】每个 SQL 代码块的第一行必须添加上下文声明注释,格式严格为:-- @context connectionId=<连接ID> dbName=<数据库名>。connectionId 和 dbName 必须来自同一个成功的 get_tables 调用(即你在该调用中传入的实际参数值)。示例:
|
||||
\`\`\`sql
|
||||
-- @context connectionId=1770778676549 dbName=mkefu_test
|
||||
SELECT * FROM users WHERE status = 1;
|
||||
@@ -1262,6 +1264,8 @@ SELECT * FROM users WHERE status = 1;
|
||||
const execution = await executeLocalAIToolCall({
|
||||
toolCall: tc,
|
||||
connections: currentConnections,
|
||||
tabs: useStore.getState().tabs,
|
||||
activeTabId: useStore.getState().activeTabId,
|
||||
mcpTools,
|
||||
toolContextMap: toolContextMapRef.current,
|
||||
sqlLogs: useStore.getState().sqlLogs,
|
||||
|
||||
@@ -6,7 +6,7 @@ import AIBuiltinToolsCatalog from './AIBuiltinToolsCatalog';
|
||||
import { buildOverlayWorkbenchTheme } from '../../utils/overlayWorkbenchTheme';
|
||||
|
||||
describe('AIBuiltinToolsCatalog', () => {
|
||||
it('renders the field-to-table flow, sql log replay flow, and both snapshot tools', () => {
|
||||
it('renders the workspace-tab flow, active-tab flow, sql log replay flow, and both snapshot tools', () => {
|
||||
const markup = renderToStaticMarkup(
|
||||
<AIBuiltinToolsCatalog
|
||||
darkMode={false}
|
||||
@@ -26,6 +26,10 @@ describe('AIBuiltinToolsCatalog', () => {
|
||||
expect(markup).toContain('inspect_table_bundle');
|
||||
expect(markup).toContain('全库快速摸底');
|
||||
expect(markup).toContain('inspect_database_bundle');
|
||||
expect(markup).toContain('读取当前页签');
|
||||
expect(markup).toContain('inspect_active_tab');
|
||||
expect(markup).toContain('盘点当前工作区');
|
||||
expect(markup).toContain('inspect_workspace_tabs');
|
||||
expect(markup).toContain('回看最近执行记录');
|
||||
expect(markup).toContain('inspect_recent_sql_logs');
|
||||
expect(markup).toContain('理解样例数据');
|
||||
|
||||
@@ -37,6 +37,16 @@ const BUILTIN_TOOL_FLOWS = [
|
||||
steps: 'inspect_database_bundle → inspect_table_bundle',
|
||||
description: '适合先看整库有哪些表、每张表大概有哪些字段,再对目标表继续做深挖快照。',
|
||||
},
|
||||
{
|
||||
title: '读取当前页签',
|
||||
steps: 'inspect_active_tab → get_columns / get_indexes / execute_sql',
|
||||
description: '适合先读取当前编辑器里的 SQL 草稿或当前表页签,再继续做字段核对、索引分析和只读验证。',
|
||||
},
|
||||
{
|
||||
title: '盘点当前工作区',
|
||||
steps: 'inspect_workspace_tabs → inspect_active_tab → get_columns / execute_sql',
|
||||
description: '适合先看当前打开了哪些 SQL / 表 / 命令页签,再切到目标页签继续做字段核对、对比分析和只读验证。',
|
||||
},
|
||||
{
|
||||
title: '回看最近执行记录',
|
||||
steps: 'inspect_recent_sql_logs → get_columns / get_indexes / execute_sql',
|
||||
|
||||
@@ -49,6 +49,90 @@ describe('aiLocalToolExecutor', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('returns the current active tab snapshot so the model can inspect the editor draft directly', async () => {
|
||||
const result = await executeLocalAIToolCall({
|
||||
toolCall: buildToolCall('inspect_active_tab', {
|
||||
includeContent: true,
|
||||
}),
|
||||
connections: [buildConnection()],
|
||||
tabs: [{
|
||||
id: 'tab-query-1',
|
||||
title: '订单查询',
|
||||
type: 'query',
|
||||
connectionId: 'conn-1',
|
||||
dbName: 'crm',
|
||||
query: 'SELECT id, status FROM orders WHERE status = \'paid\'',
|
||||
filePath: 'D:/sql/orders.sql',
|
||||
}],
|
||||
activeTabId: 'tab-query-1',
|
||||
mcpTools: [],
|
||||
toolContextMap: new Map(),
|
||||
runtime: {
|
||||
getDatabases: vi.fn(),
|
||||
getTables: vi.fn(),
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.content).toContain('"hasActiveTab":true');
|
||||
expect(result.content).toContain('"type":"query"');
|
||||
expect(result.content).toContain('"connectionName":"主库"');
|
||||
expect(result.content).toContain('"contentKind":"sql"');
|
||||
expect(result.content).toContain('SELECT id, status FROM orders');
|
||||
});
|
||||
|
||||
it('returns a workspace tab overview so the model can inspect which editors are currently open', async () => {
|
||||
const result = await executeLocalAIToolCall({
|
||||
toolCall: buildToolCall('inspect_workspace_tabs', {
|
||||
limit: 2,
|
||||
includeContent: true,
|
||||
}),
|
||||
connections: [buildConnection()],
|
||||
tabs: [
|
||||
{
|
||||
id: 'tab-query-1',
|
||||
title: '订单查询',
|
||||
type: 'query',
|
||||
connectionId: 'conn-1',
|
||||
dbName: 'crm',
|
||||
query: 'SELECT * FROM orders WHERE status = \'paid\'',
|
||||
},
|
||||
{
|
||||
id: 'tab-table-1',
|
||||
title: 'users',
|
||||
type: 'table',
|
||||
connectionId: 'conn-1',
|
||||
dbName: 'crm',
|
||||
tableName: 'users',
|
||||
},
|
||||
{
|
||||
id: 'tab-redis-1',
|
||||
title: '缓存命令',
|
||||
type: 'redis-command',
|
||||
connectionId: 'conn-1',
|
||||
query: 'GET order:1',
|
||||
redisDB: 2,
|
||||
},
|
||||
],
|
||||
activeTabId: 'tab-query-1',
|
||||
mcpTools: [],
|
||||
toolContextMap: new Map(),
|
||||
runtime: {
|
||||
getDatabases: vi.fn(),
|
||||
getTables: vi.fn(),
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.content).toContain('"activeTabId":"tab-query-1"');
|
||||
expect(result.content).toContain('"totalTabs":3');
|
||||
expect(result.content).toContain('"returnedTabs":2');
|
||||
expect(result.content).toContain('"truncated":true');
|
||||
expect(result.content).toContain('"isActive":true');
|
||||
expect(result.content).toContain('"title":"订单查询"');
|
||||
expect(result.content).toContain('SELECT * FROM orders');
|
||||
});
|
||||
|
||||
it('blocks execute_sql when the AI safety check rejects the statement', async () => {
|
||||
const query = vi.fn();
|
||||
const result = await executeLocalAIToolCall({
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
import { DBGetAllColumns, DBGetDatabases, DBGetTables } from '../../../wailsjs/go/app/App';
|
||||
|
||||
import type { SqlLog } from '../../store';
|
||||
import type { AIChatMessage, AIMCPToolDescriptor, AIToolCall, SavedConnection } from '../../types';
|
||||
import type { AIChatMessage, AIMCPToolDescriptor, AIToolCall, SavedConnection, TabData } from '../../types';
|
||||
import { buildRpcConnectionConfig } from '../../utils/connectionRpcConfig';
|
||||
import { buildAIReadonlyPreviewSQL } from '../../utils/aiSqlLimit';
|
||||
import { buildPaginatedSelectSQL, quoteQualifiedIdent } from '../../utils/sql';
|
||||
import { resolveAITableSchemaToolResult } from '../../utils/aiTableSchemaTool';
|
||||
import {
|
||||
buildActiveTabSnapshot,
|
||||
buildRecentSqlLogsSnapshot,
|
||||
buildWorkspaceTabsSnapshot,
|
||||
} from './aiWorkspaceInsights';
|
||||
|
||||
export interface AIToolContextEntry {
|
||||
connectionId: string;
|
||||
@@ -30,6 +35,8 @@ interface AILocalToolRuntime {
|
||||
export interface ExecuteLocalAIToolCallOptions {
|
||||
toolCall: AIToolCall;
|
||||
connections: SavedConnection[];
|
||||
tabs?: TabData[];
|
||||
activeTabId?: string | null;
|
||||
mcpTools: AIMCPToolDescriptor[];
|
||||
toolContextMap: Map<string, AIToolContextEntry>;
|
||||
sqlLogs?: SqlLog[];
|
||||
@@ -139,21 +146,6 @@ 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(
|
||||
@@ -168,6 +160,8 @@ const buildPreviewSQLForTable = (connection: SavedConnection, tableName: string,
|
||||
export async function executeLocalAIToolCall({
|
||||
toolCall,
|
||||
connections,
|
||||
tabs = [],
|
||||
activeTabId = null,
|
||||
mcpTools,
|
||||
toolContextMap,
|
||||
sqlLogs = [],
|
||||
@@ -181,6 +175,35 @@ export async function executeLocalAIToolCall({
|
||||
try {
|
||||
const args = JSON.parse(toolCall.function.arguments || '{}');
|
||||
switch (toolCall.function.name) {
|
||||
case 'inspect_active_tab': {
|
||||
try {
|
||||
content = JSON.stringify(buildActiveTabSnapshot({
|
||||
tabs,
|
||||
activeTabId,
|
||||
connections,
|
||||
includeContent: args.includeContent !== false,
|
||||
}));
|
||||
success = true;
|
||||
} catch (error: any) {
|
||||
content = `读取当前活动页签失败: ${error?.message || error}`;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'inspect_workspace_tabs': {
|
||||
try {
|
||||
content = JSON.stringify(buildWorkspaceTabsSnapshot({
|
||||
tabs,
|
||||
activeTabId,
|
||||
connections,
|
||||
includeContent: args.includeContent === true,
|
||||
limit: args.limit,
|
||||
}));
|
||||
success = true;
|
||||
} catch (error: any) {
|
||||
content = `读取当前工作区页签失败: ${error?.message || error}`;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'get_connections': {
|
||||
const availableConnections = connections.map((connection) => ({
|
||||
id: connection.id,
|
||||
@@ -576,28 +599,11 @@ export async function executeLocalAIToolCall({
|
||||
}
|
||||
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(buildRecentSqlLogsSnapshot({
|
||||
sqlLogs,
|
||||
limit: args.limit,
|
||||
status: args.status,
|
||||
}));
|
||||
|
||||
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}`;
|
||||
|
||||
192
frontend/src/components/ai/aiWorkspaceInsights.ts
Normal file
192
frontend/src/components/ai/aiWorkspaceInsights.ts
Normal file
@@ -0,0 +1,192 @@
|
||||
import type { SqlLog } from '../../store';
|
||||
import type { SavedConnection, TabData } from '../../types';
|
||||
|
||||
const ACTIVE_TAB_CONTENT_LIMIT = 12000;
|
||||
const WORKSPACE_TAB_CONTENT_LIMIT = 4000;
|
||||
|
||||
const normalizeWorkspaceTabLimit = (input: unknown): number => {
|
||||
const value = Math.floor(Number(input) || 12);
|
||||
if (value < 1) return 1;
|
||||
if (value > 30) return 30;
|
||||
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';
|
||||
}
|
||||
if (tab.type === 'query') {
|
||||
return 'sql';
|
||||
}
|
||||
if (tab.type === 'redis-command') {
|
||||
return 'command';
|
||||
}
|
||||
return 'text';
|
||||
};
|
||||
|
||||
const buildTabSnapshot = (params: {
|
||||
tab: TabData;
|
||||
activeTabId?: string | null;
|
||||
connections: SavedConnection[];
|
||||
includeContent: boolean;
|
||||
contentLimit: number;
|
||||
}) => {
|
||||
const { tab, activeTabId = null, connections, includeContent, contentLimit } = params;
|
||||
const activeConnection = connections.find((connection) => connection.id === tab.connectionId);
|
||||
const rawContent =
|
||||
tab.type === 'query' || tab.type === 'redis-command'
|
||||
? String(tab.query || '')
|
||||
: '';
|
||||
const trimmedContent = rawContent.trim();
|
||||
const visibleContent = includeContent ? trimmedContent.slice(0, contentLimit) : '';
|
||||
|
||||
return {
|
||||
id: tab.id,
|
||||
isActive: tab.id === activeTabId,
|
||||
title: tab.title,
|
||||
type: tab.type,
|
||||
connectionId: tab.connectionId,
|
||||
connectionName: activeConnection?.name || '',
|
||||
connectionType: activeConnection?.config?.type || '',
|
||||
dbName: tab.dbName || '',
|
||||
tableName: tab.tableName || '',
|
||||
filePath: tab.filePath || '',
|
||||
readOnly: tab.readOnly === true,
|
||||
queryMode: tab.queryMode || '',
|
||||
providerMode: tab.providerMode || '',
|
||||
resourcePath: tab.resourcePath || '',
|
||||
resourceKind: tab.resourceKind || '',
|
||||
redisDB: typeof tab.redisDB === 'number' ? tab.redisDB : null,
|
||||
schemaName: tab.schemaName || '',
|
||||
viewName: tab.viewName || '',
|
||||
viewKind: tab.viewKind || '',
|
||||
triggerName: tab.triggerName || '',
|
||||
eventName: tab.eventName || '',
|
||||
routineName: tab.routineName || '',
|
||||
routineType: tab.routineType || '',
|
||||
contentKind: resolveContentKind(tab, includeContent, trimmedContent),
|
||||
content: visibleContent,
|
||||
contentCharCount: trimmedContent.length,
|
||||
contentTruncated: includeContent && trimmedContent.length > visibleContent.length,
|
||||
};
|
||||
};
|
||||
|
||||
export const buildActiveTabSnapshot = (params: {
|
||||
tabs?: TabData[];
|
||||
activeTabId?: string | null;
|
||||
connections: SavedConnection[];
|
||||
includeContent?: boolean;
|
||||
}) => {
|
||||
const {
|
||||
tabs = [],
|
||||
activeTabId = null,
|
||||
connections,
|
||||
includeContent = true,
|
||||
} = params;
|
||||
const activeTab = tabs.find((tab) => tab.id === activeTabId);
|
||||
if (!activeTab) {
|
||||
return {
|
||||
hasActiveTab: false,
|
||||
message: '当前没有活动页签',
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
hasActiveTab: true,
|
||||
tabId: activeTab.id,
|
||||
...buildTabSnapshot({
|
||||
tab: activeTab,
|
||||
activeTabId,
|
||||
connections,
|
||||
includeContent,
|
||||
contentLimit: ACTIVE_TAB_CONTENT_LIMIT,
|
||||
}),
|
||||
};
|
||||
};
|
||||
|
||||
export const buildWorkspaceTabsSnapshot = (params: {
|
||||
tabs?: TabData[];
|
||||
activeTabId?: string | null;
|
||||
connections: SavedConnection[];
|
||||
includeContent?: boolean;
|
||||
limit?: unknown;
|
||||
}) => {
|
||||
const {
|
||||
tabs = [],
|
||||
activeTabId = null,
|
||||
connections,
|
||||
includeContent = false,
|
||||
limit,
|
||||
} = params;
|
||||
const safeLimit = normalizeWorkspaceTabLimit(limit);
|
||||
const orderedTabs = [...tabs].sort((left, right) => {
|
||||
if (left.id === activeTabId && right.id !== activeTabId) {
|
||||
return -1;
|
||||
}
|
||||
if (right.id === activeTabId && left.id !== activeTabId) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
const visibleTabs = orderedTabs.slice(0, safeLimit);
|
||||
|
||||
return {
|
||||
activeTabId,
|
||||
totalTabs: orderedTabs.length,
|
||||
returnedTabs: visibleTabs.length,
|
||||
truncated: orderedTabs.length > visibleTabs.length,
|
||||
tabs: visibleTabs.map((tab) =>
|
||||
buildTabSnapshot({
|
||||
tab,
|
||||
activeTabId,
|
||||
connections,
|
||||
includeContent,
|
||||
contentLimit: WORKSPACE_TAB_CONTENT_LIMIT,
|
||||
})),
|
||||
};
|
||||
};
|
||||
|
||||
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,
|
||||
};
|
||||
};
|
||||
@@ -34,6 +34,8 @@ const TOOL_ACTION_LABELS: Record<string, string> = {
|
||||
get_table_ddl: '提取建表语句',
|
||||
inspect_table_bundle: '抓取完整表结构快照',
|
||||
inspect_database_bundle: '抓取数据库结构总览',
|
||||
inspect_active_tab: '读取当前活动页签',
|
||||
inspect_workspace_tabs: '盘点当前工作区页签',
|
||||
inspect_recent_sql_logs: '回看最近 SQL 执行日志',
|
||||
preview_table_rows: '预览真实样例数据',
|
||||
execute_sql: '执行只读 SQL 验证',
|
||||
|
||||
@@ -309,6 +309,51 @@ export const BUILTIN_AI_TOOL_INFO: AIBuiltinToolInfo[] = [
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
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: "🧾",
|
||||
|
||||
Reference in New Issue
Block a user