diff --git a/frontend/src/components/ai/AIBuiltinToolsCatalog.test.tsx b/frontend/src/components/ai/AIBuiltinToolsCatalog.test.tsx index 98b2bcd..cbdd806 100644 --- a/frontend/src/components/ai/AIBuiltinToolsCatalog.test.tsx +++ b/frontend/src/components/ai/AIBuiltinToolsCatalog.test.tsx @@ -40,6 +40,8 @@ describe('AIBuiltinToolsCatalog', () => { expect(markup).toContain('inspect_ai_context'); expect(markup).toContain('查看当前连接'); expect(markup).toContain('inspect_current_connection'); + expect(markup).toContain('盘点本地连接资产'); + expect(markup).toContain('inspect_saved_connections'); expect(markup).toContain('读取当前页签'); expect(markup).toContain('inspect_active_tab'); expect(markup).toContain('盘点当前工作区'); diff --git a/frontend/src/components/ai/AIBuiltinToolsCatalog.tsx b/frontend/src/components/ai/AIBuiltinToolsCatalog.tsx index 35847e2..36171c7 100644 --- a/frontend/src/components/ai/AIBuiltinToolsCatalog.tsx +++ b/frontend/src/components/ai/AIBuiltinToolsCatalog.tsx @@ -72,6 +72,11 @@ const BUILTIN_TOOL_FLOWS = [ steps: 'inspect_current_connection → get_databases / get_tables', description: '适合先确认当前活动数据源的类型、地址、当前库和 SSH/代理状态,再继续做库表探索或连接问题排查。', }, + { + title: '盘点本地连接资产', + steps: 'inspect_saved_connections → inspect_current_connection / get_databases', + description: '适合先按关键词或类型筛出本地保存的数据源,再挑目标连接继续看当前状态或库表结构。', + }, { title: '读取当前页签', steps: 'inspect_active_tab → get_columns / get_indexes / execute_sql', diff --git a/frontend/src/components/ai/AIMCPClientInstallPanel.test.tsx b/frontend/src/components/ai/AIMCPClientInstallPanel.test.tsx index 6d254a9..ac4e30f 100644 --- a/frontend/src/components/ai/AIMCPClientInstallPanel.test.tsx +++ b/frontend/src/components/ai/AIMCPClientInstallPanel.test.tsx @@ -62,18 +62,20 @@ describe('AIMCPClientInstallPanel', () => { />, ); + expect(markup).toContain('安装到外部客户端'); expect(markup).toContain('不是给 GoNavi 自己再装一个 MCP'); expect(markup).toContain('把 GoNavi MCP 接入外部 AI 客户端'); - expect(markup).toContain('第 1 步:选择目标客户端'); - expect(markup).toContain('第 2 步:确认状态后写入'); - expect(markup).toContain('未接入'); + expect(markup).toContain('第 1 步:选择安装目标'); + expect(markup).toContain('第 2 步:确认并安装'); + expect(markup).toContain('未安装'); expect(markup).toContain('需更新'); - expect(markup).toContain('命令已检测'); + expect(markup).toContain('CLI 已检测'); expect(markup).toContain('复制配置路径'); expect(markup).toContain('复制启动命令'); - expect(markup).toContain('更新 Codex 配置'); + expect(markup).toContain('更新到 Codex'); + expect(markup).toContain('当前目标:Codex'); expect(markup).toContain('本机命令状态:已检测到 codex'); - expect(markup).toContain('不会下载 Claude Code / Codex'); + expect(markup).toContain('不会下载安装 Claude Code / Codex'); }); it('shows an already-connected label and supports prewriting config when the client command is not detected locally', () => { @@ -124,8 +126,9 @@ describe('AIMCPClientInstallPanel', () => { />, ); - expect(markup).toContain('预写入 Claude Code 配置'); - expect(markup).toContain('未检测命令'); + expect(markup).toContain('安装到 Claude Code'); + expect(markup).toContain('CLI 未检测'); expect(markup).toContain('未检测到本机 claude 命令'); + expect(markup).toContain('已安装当前'); }); }); diff --git a/frontend/src/components/ai/AIMCPClientInstallPanel.tsx b/frontend/src/components/ai/AIMCPClientInstallPanel.tsx index 92374d4..7bedd9a 100644 --- a/frontend/src/components/ai/AIMCPClientInstallPanel.tsx +++ b/frontend/src/components/ai/AIMCPClientInstallPanel.tsx @@ -29,7 +29,7 @@ const getStatusTone = (status: AIMCPClientInstallStatus | undefined, darkMode: b const messageText = String(status?.message || ''); if (status?.matchesCurrent) { return { - label: '已接入', + label: '已安装当前', color: '#16a34a', bg: darkMode ? 'rgba(34,197,94,0.18)' : 'rgba(34,197,94,0.12)', }; @@ -43,13 +43,13 @@ const getStatusTone = (status: AIMCPClientInstallStatus | undefined, darkMode: b } if (messageText.includes('失败') || messageText.includes('异常')) { return { - label: '需检查', + label: '读取异常', color: '#dc2626', bg: darkMode ? 'rgba(239,68,68,0.18)' : 'rgba(239,68,68,0.1)', }; } return { - label: '未接入', + label: '未安装', color: darkMode ? 'rgba(255,255,255,0.72)' : '#64748b', bg: darkMode ? 'rgba(255,255,255,0.08)' : 'rgba(100,116,139,0.08)', }; @@ -66,13 +66,13 @@ const resolveClientCommandName = (status: AIMCPClientInstallStatus | undefined) const getClientDetectionTone = (status: AIMCPClientInstallStatus | undefined, darkMode: boolean) => { if (status?.clientDetected) { return { - label: '命令已检测', + label: 'CLI 已检测', color: '#16a34a', bg: darkMode ? 'rgba(34,197,94,0.18)' : 'rgba(34,197,94,0.12)', }; } return { - label: '未检测命令', + label: 'CLI 未检测', color: '#d97706', bg: darkMode ? 'rgba(245,158,11,0.18)' : 'rgba(245,158,11,0.12)', }; @@ -82,48 +82,48 @@ const getStatusSummary = (status: AIMCPClientInstallStatus | undefined) => { const label = status?.displayName || '这个客户端'; const messageText = String(status?.message || ''); if (status?.matchesCurrent) { - return `${label} 已经写入当前 GoNavi 路径,可直接把 GoNavi 当作 MCP Server 使用。`; + return `${label} 已安装当前 GoNavi MCP,可直接在这个客户端里调用。`; } if (status?.installed) { - return `${label} 已检测到旧的 GoNavi 路径,更新后会改成当前这份 GoNavi。`; + return `${label} 已检测到旧的 GoNavi 安装路径,更新后会切到当前这份 GoNavi。`; } if (messageText.includes('失败') || messageText.includes('异常')) { - return '状态读取异常,建议先刷新,再决定是否写入。'; + return `${label} 的安装状态读取失败,建议先刷新检测。`; } - return `${label} 还没有写入 GoNavi MCP 配置。`; + return `${label} 还没有安装 GoNavi MCP。`; }; -const getClientCardDescription = (status: AIMCPClientInstallStatus | undefined) => { +const getClientOptionSummary = (status: AIMCPClientInstallStatus | undefined) => { if (status?.matchesCurrent) { - return '当前 GoNavi 路径已经写入,可直接在这个客户端里调用。'; + return '当前 GoNavi 已安装到这个客户端。'; } if (status?.installed) { - return '检测到旧的 GoNavi 记录,建议更新为当前安装路径。'; + return '已发现旧配置,建议更新到当前安装路径。'; } - return '还没有写入 GoNavi MCP 配置。'; + if (String(status?.message || '').includes('失败') || String(status?.message || '').includes('异常')) { + return '安装状态读取异常,建议先刷新再处理。'; + } + return '尚未安装到这个客户端。'; }; const getClientDetectionSummary = (status: AIMCPClientInstallStatus | undefined) => { const label = status?.displayName || '这个客户端'; const commandName = resolveClientCommandName(status); if (status?.clientDetected) { - return `已检测到本机 ${commandName} 命令,写入后可直接重启 ${label} 验证。`; + return `已检测到本机 ${commandName} 命令,安装后重启 ${label} 即可验证。`; } - return `未检测到本机 ${commandName} 命令;如果你只装了桌面端或 CLI 还没加入 PATH,也可以先写配置,稍后再重启 ${label}。`; + return `未检测到本机 ${commandName} 命令;如果 CLI 还没加入 PATH,也可以先安装到 ${label},稍后再重启验证。`; }; const resolveActionLabel = (status: AIMCPClientInstallStatus | undefined) => { const label = status?.displayName || '客户端'; if (status?.matchesCurrent) { - return `已接入 ${label}`; + return `已安装到 ${label}`; } if (status?.installed) { - return `更新 ${label} 配置`; + return `更新到 ${label}`; } - if (status?.clientDetected === false) { - return `预写入 ${label} 配置`; - } - return `写入 ${label} 配置`; + return `安装到 ${label}`; }; const AIMCPClientInstallPanel: React.FC = ({ @@ -144,8 +144,42 @@ const AIMCPClientInstallPanel: React.FC = ({ onInstall, }) => (
-
- 这里的“安装”不是给 GoNavi 自己再装一个 MCP,而是把 GoNavi 暴露成 MCP Server,写入 Claude Code、Codex 这类外部 AI 客户端里使用。 +
+
安装到外部客户端
+
+ 这里的“安装”不是给 GoNavi 自己再装一个 MCP,而是把 GoNavi 的 MCP Server 配置写入 Claude Code、Codex 这类外部 AI 客户端。 +
+
+ {[ + '只写入用户级 MCP 配置', + '不会下载安装 Claude Code / Codex', + '不会重装 GoNavi 程序', + ].map((item) => ( +
+ {item} +
+ ))} +
= ({ flexDirection: 'column', gap: 14, }} - > -
-
把 GoNavi MCP 接入外部 AI 客户端
-
- 先选 1 个要接入的目标客户端,GoNavi 会自动把当前安装路径写入它的用户级 MCP 配置文件,不需要你自己找本机 exe,也不用手改配置。 -
-
- -
- 只会修改你选中的外部客户端用户级 MCP 配置,不会下载 Claude Code / Codex,也不会重新安装 GoNavi 自己的程序文件。 -
+
+
把 GoNavi MCP 接入外部 AI 客户端
+
+ 先选 1 个安装目标,再执行安装或更新。GoNavi 会自动写入当前安装路径,不需要你自己找本机 exe,也不用手改配置。 +
+
-
第 1 步:选择目标客户端
+
第 1 步:选择安装目标
- 每次只处理一个外部客户端,先看 GoNavi MCP 配置状态,再看本机有没有检测到对应命令。 + 这里只会处理你当前选中的 1 个外部客户端,并显示它是否已经安装过、是否需要更新。
-
+
{statuses.map((status) => { const client = status.client === 'codex' ? 'codex' : 'claude-code'; const active = selectedClient === client; @@ -199,56 +219,62 @@ const AIMCPClientInstallPanel: React.FC = ({ type="button" onClick={() => onSelectClient(client)} style={{ - padding: '14px 14px 12px', + padding: '14px 16px', borderRadius: 12, border: `1.5px solid ${active ? overlayTheme.selectedText : cardBorder}`, background: active ? overlayTheme.selectedBg : (darkMode ? 'rgba(255,255,255,0.02)' : 'rgba(255,255,255,0.7)'), cursor: 'pointer', display: 'flex', - flexDirection: 'column', - gap: 10, + alignItems: 'center', + justifyContent: 'space-between', + gap: 14, textAlign: 'left', - minHeight: 138, + minHeight: 92, transition: 'all 0.2s ease', opacity: statusLoading ? 0.72 : 1, }} > -
-
-
- {active ? : null} -
-
- {status.displayName} -
+
+
+ {active ? : null}
-
+
- {tone.label} +
+ {status.displayName} +
+
+ {tone.label} +
+
+
+ {getClientOptionSummary(status)}
= ({ color: detectionTone.color, background: detectionTone.bg, whiteSpace: 'nowrap', - flexShrink: 0, + alignSelf: 'flex-start', + minWidth: 76, + textAlign: 'center', }} > {detectionTone.label}
-
-
- {getClientCardDescription(status)} -
-
- {getClientDetectionSummary(status)} -
+
+ {active ? '当前已选中这个客户端。' : '点击切换到这个客户端。'} + {' '} + {getClientDetectionSummary(status)}
); @@ -293,11 +318,11 @@ const AIMCPClientInstallPanel: React.FC = ({ >
- 第 2 步:确认状态后写入 + 第 2 步:确认并安装
- {selectedStatus?.displayName || '客户端'} 状态 + 当前目标:{selectedStatus?.displayName || '客户端'}
{selectedStatus && (
= ({
- 命令未检测到时也可以先写配置;后续装好 CLI 或把命令加入 PATH 后,重启对应客户端即可生效。当前路径已经一致时按钮会自动禁用,避免重复写入。 + 命令未检测到时也可以先安装到目标客户端;后续装好 CLI 或把命令加入 PATH 后,重启对应客户端即可生效。已经是当前配置时按钮会自动禁用,避免重复安装。
)} +
+
操作说明
+
+ 测试工具发现 + {' '}只会按当前字段试启动一次,检查能发现哪些工具,不会保存配置。 + {' '}保存 + {' '}才会把这条 MCP 长期写入本地配置。 + {serverTools.length > 0 + ? ' 当前上方列出的工具,就是最近一次测试成功后发现到的别名。' + : ' 建议先测试成功,再保存;测试通过后,上方会显示这条服务实际发现到的工具。'} +
+
diff --git a/frontend/src/components/ai/AISettingsMCPSection.test.tsx b/frontend/src/components/ai/AISettingsMCPSection.test.tsx index a5ed520..e831626 100644 --- a/frontend/src/components/ai/AISettingsMCPSection.test.tsx +++ b/frontend/src/components/ai/AISettingsMCPSection.test.tsx @@ -96,7 +96,7 @@ describe('AISettingsMCPSection', () => { ); expect(markup).toContain('把 GoNavi MCP 接入外部 AI 客户端'); - expect(markup).toContain('未检测命令'); + expect(markup).toContain('CLI 未检测'); expect(markup).toContain('常见启动方式模板'); expect(markup).toContain('Node 脚本'); expect(markup).toContain('新增 MCP 服务'); diff --git a/frontend/src/components/ai/aiLocalToolExecutor.test.ts b/frontend/src/components/ai/aiLocalToolExecutor.test.ts index ca4673e..3673ada 100644 --- a/frontend/src/components/ai/aiLocalToolExecutor.test.ts +++ b/frontend/src/components/ai/aiLocalToolExecutor.test.ts @@ -467,6 +467,58 @@ describe('aiLocalToolExecutor', () => { expect(result.content).toContain('"activeTabType":"query"'); }); + it('returns the local saved connections snapshot so the model can find matching data sources by type or keyword', async () => { + const result = await executeLocalAIToolCall({ + toolCall: buildToolCall('inspect_saved_connections', { + type: 'mysql', + keyword: '订单', + }), + connections: [ + { + id: 'conn-1', + name: '订单主库', + config: { + type: 'mysql', + host: '10.10.1.18', + port: 3306, + user: 'root', + database: 'crm', + useSSH: true, + ssh: { + host: '192.168.1.8', + port: 22, + user: 'ops', + }, + }, + }, + { + id: 'conn-2', + name: '分析仓库', + config: { + type: 'postgres', + host: '10.10.1.20', + port: 5432, + user: 'analyst', + database: 'dw', + }, + }, + ], + mcpTools: [], + toolContextMap: new Map(), + runtime: { + getDatabases: vi.fn(), + getTables: vi.fn(), + }, + }); + + expect(result.success).toBe(true); + expect(result.content).toContain('"totalMatched":1'); + expect(result.content).toContain('"typeBreakdown":{"mysql":1}'); + expect(result.content).toContain('"name":"订单主库"'); + expect(result.content).toContain('"useSSH":true'); + expect(result.content).not.toContain('分析仓库'); + }); + 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/aiSavedConnectionInsights.ts b/frontend/src/components/ai/aiSavedConnectionInsights.ts new file mode 100644 index 0000000..c52e7cc --- /dev/null +++ b/frontend/src/components/ai/aiSavedConnectionInsights.ts @@ -0,0 +1,118 @@ +import type { SavedConnection } from '../../types'; + +const normalizeLimit = (input: unknown, fallback: number, max: number): number => { + const value = Math.floor(Number(input) || fallback); + if (value < 1) return 1; + if (value > max) return max; + return value; +}; + +const normalizeKeyword = (input: unknown): string => String(input || '').trim().toLowerCase(); + +const matchesKeyword = (keyword: string, fields: Array): boolean => { + if (!keyword) { + return true; + } + return fields.some((field) => String(field || '').toLowerCase().includes(keyword)); +}; + +const normalizeTypeFilter = (input: unknown): string => + String(input || '').trim().toLowerCase(); + +export const buildSavedConnectionsSnapshot = (params: { + connections: SavedConnection[]; + keyword?: unknown; + type?: unknown; + limit?: unknown; +}) => { + const { + connections, + keyword, + type, + limit, + } = params; + const safeKeyword = normalizeKeyword(keyword); + const safeType = normalizeTypeFilter(type); + const safeLimit = normalizeLimit(limit, 20, 100); + + const filteredConnections = connections.filter((connection) => { + const config = connection.config || {}; + const connectionType = String(config.type || '').trim().toLowerCase(); + if (safeType && connectionType !== safeType) { + return false; + } + return matchesKeyword(safeKeyword, [ + connection.id, + connection.name, + config.type, + config.host, + config.database, + config.user, + config.driver, + config.topology, + config.ssh?.host, + config.proxy?.host, + config.httpTunnel?.host, + ]); + }); + + const visibleConnections = filteredConnections + .slice(0, safeLimit) + .map((connection) => { + const config = connection.config || {}; + const includeDatabases = Array.isArray(connection.includeDatabases) + ? connection.includeDatabases.filter(Boolean) + : []; + const includeRedisDatabases = Array.isArray(connection.includeRedisDatabases) + ? connection.includeRedisDatabases.filter((item) => typeof item === 'number') + : []; + + return { + id: connection.id, + name: connection.name, + type: config.type || '', + host: config.host || '', + port: typeof config.port === 'number' ? config.port : null, + user: config.user || '', + configuredDatabase: config.database || '', + driver: config.driver || '', + topology: config.topology || 'single', + useSSL: config.useSSL === true, + useSSH: config.useSSH === true, + sshHost: config.useSSH ? (config.ssh?.host || '') : '', + sshPort: config.useSSH && typeof config.ssh?.port === 'number' ? config.ssh.port : null, + useProxy: config.useProxy === true, + proxyType: config.useProxy ? (config.proxy?.type || '') : '', + proxyHost: config.useProxy ? (config.proxy?.host || '') : '', + proxyPort: config.useProxy && typeof config.proxy?.port === 'number' ? config.proxy.port : null, + useHttpTunnel: config.useHttpTunnel === true, + httpTunnelHost: config.useHttpTunnel ? (config.httpTunnel?.host || '') : '', + httpTunnelPort: config.useHttpTunnel && typeof config.httpTunnel?.port === 'number' ? config.httpTunnel.port : null, + hasOpaqueURI: connection.hasOpaqueURI === true, + hasOpaqueDSN: connection.hasOpaqueDSN === true, + hasConnectionParams: Boolean(String(config.connectionParams || '').trim()), + includeDatabaseCount: includeDatabases.length, + includeRedisDatabaseCount: includeRedisDatabases.length, + }; + }); + + const typeBreakdown = filteredConnections.reduce>((acc, connection) => { + const key = String(connection.config?.type || 'unknown').trim() || 'unknown'; + acc[key] = (acc[key] || 0) + 1; + return acc; + }, {}); + + return { + keyword: safeKeyword, + type: safeType, + limit: safeLimit, + totalMatched: filteredConnections.length, + returnedConnections: visibleConnections.length, + truncated: filteredConnections.length > visibleConnections.length, + sshEnabledCount: filteredConnections.filter((item) => item.config?.useSSH === true).length, + proxyEnabledCount: filteredConnections.filter((item) => item.config?.useProxy === true).length, + httpTunnelEnabledCount: filteredConnections.filter((item) => item.config?.useHttpTunnel === true).length, + typeBreakdown, + connections: visibleConnections, + }; +}; diff --git a/frontend/src/components/ai/aiSnapshotInspectionToolExecutor.ts b/frontend/src/components/ai/aiSnapshotInspectionToolExecutor.ts index 4f1e0f9..cb600a5 100644 --- a/frontend/src/components/ai/aiSnapshotInspectionToolExecutor.ts +++ b/frontend/src/components/ai/aiSnapshotInspectionToolExecutor.ts @@ -25,6 +25,7 @@ import { buildSavedQueriesSnapshot, buildSqlSnippetsSnapshot, } from './aiSavedSqlInsights'; +import { buildSavedConnectionsSnapshot } from './aiSavedConnectionInsights'; import { buildActiveTabSnapshot, buildRecentSqlLogsSnapshot, @@ -173,6 +174,16 @@ export async function executeSnapshotInspectionToolCall( })), success: true, }; + case 'inspect_saved_connections': + return { + content: JSON.stringify(buildSavedConnectionsSnapshot({ + connections, + keyword: args.keyword, + type: args.type, + limit: args.limit, + })), + success: true, + }; case 'inspect_active_tab': return { content: JSON.stringify(buildActiveTabSnapshot({ @@ -248,6 +259,7 @@ export async function executeSnapshotInspectionToolCall( inspect_mcp_setup: '读取 MCP 配置状态失败', inspect_ai_guidance: '读取当前 AI 提示与技能配置失败', inspect_current_connection: '读取当前连接失败', + inspect_saved_connections: '读取本地连接清单失败', inspect_active_tab: '读取当前活动页签失败', inspect_workspace_tabs: '读取当前工作区页签失败', inspect_ai_context: '读取当前 AI 上下文失败', diff --git a/frontend/src/components/ai/aiSystemContextMessages.test.ts b/frontend/src/components/ai/aiSystemContextMessages.test.ts index 32cc846..2b46d90 100644 --- a/frontend/src/components/ai/aiSystemContextMessages.test.ts +++ b/frontend/src/components/ai/aiSystemContextMessages.test.ts @@ -68,7 +68,7 @@ describe('buildAISystemContextMessages', () => { connections: [connections[0]], tabs: [], activeTabId: null, - availableToolNames: ['inspect_workspace_tabs', 'inspect_ai_runtime', 'inspect_ai_providers', 'inspect_ai_chat_readiness', 'inspect_mcp_setup', 'inspect_ai_guidance', 'inspect_ai_context', 'inspect_current_connection', 'inspect_saved_queries', 'inspect_sql_snippets', 'get_columns'], + availableToolNames: ['inspect_workspace_tabs', 'inspect_ai_runtime', 'inspect_ai_providers', 'inspect_ai_chat_readiness', 'inspect_mcp_setup', 'inspect_ai_guidance', 'inspect_ai_context', 'inspect_current_connection', 'inspect_saved_connections', 'inspect_saved_queries', 'inspect_sql_snippets', 'get_columns'], skills, userPromptSettings, }); @@ -82,6 +82,7 @@ describe('buildAISystemContextMessages', () => { expect(joined).toContain('inspect_ai_guidance 读取真实提示与技能配置'); expect(joined).toContain('inspect_ai_context 读取当前挂载的表结构上下文'); expect(joined).toContain('inspect_current_connection'); + expect(joined).toContain('inspect_saved_connections'); expect(joined).toContain('inspect_saved_queries'); expect(joined).toContain('inspect_sql_snippets'); expect(joined).toContain('当前连接'); diff --git a/frontend/src/components/ai/aiSystemContextMessages.ts b/frontend/src/components/ai/aiSystemContextMessages.ts index dfb5c18..c3586e2 100644 --- a/frontend/src/components/ai/aiSystemContextMessages.ts +++ b/frontend/src/components/ai/aiSystemContextMessages.ts @@ -387,6 +387,12 @@ SELECT * FROM users WHERE status = 1; content: '如果用户提到“当前连接”“当前数据源”“我现在连的是哪个库/地址”“这个连接走没走 SSH/代理”,优先调用 inspect_current_connection 读取当前活动连接摘要,不要凭界面或记忆猜测。', }); } + if (availableToolNames.includes('inspect_saved_connections')) { + systemMessages.push({ + role: 'system', + content: '如果用户提到“本地存了哪些连接”“帮我找 mysql / postgres / redis 连接”“哪条连接配了 SSH/代理”,优先调用 inspect_saved_connections 读取真实本地连接清单,再决定继续查看哪条连接。', + }); + } if (availableToolNames.includes('inspect_saved_queries')) { systemMessages.push({ role: 'system', diff --git a/frontend/src/components/ai/messageBubble/AIMessageStatusBlocks.tsx b/frontend/src/components/ai/messageBubble/AIMessageStatusBlocks.tsx index ba65263..d61640d 100644 --- a/frontend/src/components/ai/messageBubble/AIMessageStatusBlocks.tsx +++ b/frontend/src/components/ai/messageBubble/AIMessageStatusBlocks.tsx @@ -40,6 +40,7 @@ const TOOL_ACTION_LABELS: Record = { inspect_table_bundle: '抓取完整表结构快照', inspect_database_bundle: '抓取数据库结构总览', inspect_current_connection: '读取当前连接摘要', + inspect_saved_connections: '盘点本地已保存连接', inspect_active_tab: '读取当前活动页签', inspect_workspace_tabs: '盘点当前工作区页签', inspect_recent_sql_logs: '回看最近 SQL 执行日志', diff --git a/frontend/src/utils/aiToolRegistry.test.ts b/frontend/src/utils/aiToolRegistry.test.ts index db1cab8..30e71df 100644 --- a/frontend/src/utils/aiToolRegistry.test.ts +++ b/frontend/src/utils/aiToolRegistry.test.ts @@ -45,6 +45,13 @@ describe('aiToolRegistry', () => { expect(info?.tool.function.description).toContain('SSH/代理/HTTP 隧道状态'); }); + it('registers the saved-connections inspector as a builtin tool', () => { + const info = BUILTIN_AI_TOOL_INFO.find((item) => item.name === 'inspect_saved_connections'); + expect(info).toBeTruthy(); + expect(info?.desc).toContain('已保存连接'); + expect(info?.tool.function.description).toContain('本地已保存连接清单'); + }); + it('registers the saved-query and sql-snippet inspectors as builtin tools', () => { const savedQueryTool = BUILTIN_AI_TOOL_INFO.find((item) => item.name === 'inspect_saved_queries'); const snippetTool = BUILTIN_AI_TOOL_INFO.find((item) => item.name === 'inspect_sql_snippets'); @@ -77,6 +84,7 @@ describe('aiToolRegistry', () => { expect(tools.some((item) => item.function.name === 'inspect_mcp_setup')).toBe(true); expect(tools.some((item) => item.function.name === 'inspect_ai_guidance')).toBe(true); expect(tools.some((item) => item.function.name === 'inspect_current_connection')).toBe(true); + expect(tools.some((item) => item.function.name === 'inspect_saved_connections')).toBe(true); expect(tools.some((item) => item.function.name === 'inspect_saved_queries')).toBe(true); expect(tools.some((item) => item.function.name === 'inspect_sql_snippets')).toBe(true); expect(tools.some((item) => item.function.name === 'custom_probe')).toBe(true); diff --git a/frontend/src/utils/aiToolRegistry.ts b/frontend/src/utils/aiToolRegistry.ts index aacbcdd..d42f721 100644 --- a/frontend/src/utils/aiToolRegistry.ts +++ b/frontend/src/utils/aiToolRegistry.ts @@ -434,6 +434,30 @@ export const BUILTIN_AI_TOOL_INFO: AIBuiltinToolInfo[] = [ }, }, }, + { + name: "inspect_saved_connections", + icon: "🧭", + desc: "查看本地已保存连接清单", + detail: + "可按关键词或数据库类型过滤,返回本地保存的数据源列表、连接类型分布,以及每条连接的地址、当前库、SSH/代理/HTTP 隧道状态。适合用户问“我本地存了哪些连接”“帮我找 mysql / postgres 连接”“哪条连接配置了 SSH”时先读真实本地连接资产。", + params: "keyword?, type?, limit?", + tool: { + type: "function", + function: { + name: "inspect_saved_connections", + description: + "读取本地已保存连接清单,可按关键词和数据库类型过滤,并返回每条连接的类型、地址、当前库、SSH/代理/HTTP 隧道等摘要。适用于用户提到本地保存了哪些连接、要找哪条 mysql/postgres 连接、哪条连接启用了 SSH 或代理时,先读取真实本地连接资产再回答。", + parameters: { + type: "object", + properties: { + keyword: { type: "string", description: "可选,按连接名、ID、类型、主机、数据库名或 SSH/代理地址做关键词筛选" }, + type: { type: "string", description: "可选,只看某种数据库类型,例如 mysql、postgres、redis、mongodb" }, + limit: { type: "number", description: "可选,最多返回多少条连接,默认 20,最大 100" }, + }, + }, + }, + }, + }, { name: "inspect_active_tab", icon: "📍",