From 2c95009d1f52f7909dad3ced185f245bf4c08854 Mon Sep 17 00:00:00 2001 From: Syngnat Date: Mon, 8 Jun 2026 17:05:29 +0800 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20feat(ai-tools):=20=E6=96=B0?= =?UTF-8?q?=E5=A2=9E=E5=B7=A5=E4=BD=9C=E5=8C=BA=E9=A1=B5=E7=AD=BE=E6=8E=A2?= =?UTF-8?q?=E9=92=88=E5=B9=B6=E6=8B=86=E5=88=86=E7=95=8C=E9=9D=A2=E6=B4=9E?= =?UTF-8?q?=E5=AF=9F=E6=A8=A1=E5=9D=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../AIChatPanel.message-boundary.test.tsx | 4 + frontend/src/components/AIChatPanel.tsx | 16 +- .../ai/AIBuiltinToolsCatalog.test.tsx | 6 +- .../components/ai/AIBuiltinToolsCatalog.tsx | 10 + .../components/ai/aiLocalToolExecutor.test.ts | 84 ++++++++ .../src/components/ai/aiLocalToolExecutor.ts | 80 ++++---- .../src/components/ai/aiWorkspaceInsights.ts | 192 ++++++++++++++++++ .../messageBubble/AIMessageStatusBlocks.tsx | 2 + frontend/src/utils/aiToolRegistry.ts | 45 ++++ 9 files changed, 395 insertions(+), 44 deletions(-) create mode 100644 frontend/src/components/ai/aiWorkspaceInsights.ts diff --git a/frontend/src/components/AIChatPanel.message-boundary.test.tsx b/frontend/src/components/AIChatPanel.message-boundary.test.tsx index 13b66d6..48b26d1 100644 --- a/frontend/src/components/AIChatPanel.message-boundary.test.tsx +++ b/frontend/src/components/AIChatPanel.message-boundary.test.tsx @@ -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'); }); diff --git a/frontend/src/components/AIChatPanel.tsx b/frontend/src/components/AIChatPanel.tsx index 072305b..bf02630 100644 --- a/frontend/src/components/AIChatPanel.tsx +++ b/frontend/src/components/AIChatPanel.tsx @@ -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, diff --git a/frontend/src/components/ai/AIBuiltinToolsCatalog.test.tsx b/frontend/src/components/ai/AIBuiltinToolsCatalog.test.tsx index 1e91c3e..26a958c 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, 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( { 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('理解样例数据'); diff --git a/frontend/src/components/ai/AIBuiltinToolsCatalog.tsx b/frontend/src/components/ai/AIBuiltinToolsCatalog.tsx index f1e9ef4..0ee1005 100644 --- a/frontend/src/components/ai/AIBuiltinToolsCatalog.tsx +++ b/frontend/src/components/ai/AIBuiltinToolsCatalog.tsx @@ -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', diff --git a/frontend/src/components/ai/aiLocalToolExecutor.test.ts b/frontend/src/components/ai/aiLocalToolExecutor.test.ts index c0e3dc2..150d77d 100644 --- a/frontend/src/components/ai/aiLocalToolExecutor.test.ts +++ b/frontend/src/components/ai/aiLocalToolExecutor.test.ts @@ -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({ diff --git a/frontend/src/components/ai/aiLocalToolExecutor.ts b/frontend/src/components/ai/aiLocalToolExecutor.ts index 4dae668..ea8b3f5 100644 --- a/frontend/src/components/ai/aiLocalToolExecutor.ts +++ b/frontend/src/components/ai/aiLocalToolExecutor.ts @@ -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; 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}`; diff --git a/frontend/src/components/ai/aiWorkspaceInsights.ts b/frontend/src/components/ai/aiWorkspaceInsights.ts new file mode 100644 index 0000000..e50485b --- /dev/null +++ b/frontend/src/components/ai/aiWorkspaceInsights.ts @@ -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, + }; +}; diff --git a/frontend/src/components/ai/messageBubble/AIMessageStatusBlocks.tsx b/frontend/src/components/ai/messageBubble/AIMessageStatusBlocks.tsx index 81cffa4..94bffe3 100644 --- a/frontend/src/components/ai/messageBubble/AIMessageStatusBlocks.tsx +++ b/frontend/src/components/ai/messageBubble/AIMessageStatusBlocks.tsx @@ -34,6 +34,8 @@ const TOOL_ACTION_LABELS: Record = { 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 验证', diff --git a/frontend/src/utils/aiToolRegistry.ts b/frontend/src/utils/aiToolRegistry.ts index 73e233a..013cbdd 100644 --- a/frontend/src/utils/aiToolRegistry.ts +++ b/frontend/src/utils/aiToolRegistry.ts @@ -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: "🧾",