diff --git a/frontend/src/components/AISettingsModal.edit-password.test.tsx b/frontend/src/components/AISettingsModal.edit-password.test.tsx index b3e3990..ad58361 100644 --- a/frontend/src/components/AISettingsModal.edit-password.test.tsx +++ b/frontend/src/components/AISettingsModal.edit-password.test.tsx @@ -22,7 +22,8 @@ describe('AISettingsModal edit password behavior', () => { expect(source).toContain('Service.AIGetMCPServers?.()'); expect(source).toContain('Service.AIListMCPTools?.()'); expect(source).toContain('Service.AIGetSkills?.()'); - expect(source).toContain('新增 Skill'); + expect(source).toContain("import AISettingsSkillsSection from './ai/AISettingsSkillsSection';"); + expect(source).toContain(' { diff --git a/frontend/src/components/AISettingsModal.tsx b/frontend/src/components/AISettingsModal.tsx index ef2c8bf..354eb9d 100644 --- a/frontend/src/components/AISettingsModal.tsx +++ b/frontend/src/components/AISettingsModal.tsx @@ -1,7 +1,7 @@ import React, { useState, useEffect, useCallback, useMemo, useRef } from 'react'; import { Modal, Button, Input, Select, Form, message as antdMessage, Tooltip, Tabs, Space, Popconfirm, Slider } from 'antd'; import { PlusOutlined, DeleteOutlined, EditOutlined, CheckOutlined, ApiOutlined, SafetyCertificateOutlined, RobotOutlined, ThunderboltOutlined, CloudOutlined, ExperimentOutlined, KeyOutlined, LinkOutlined, AppstoreOutlined, ToolOutlined } from '@ant-design/icons'; -import type { AIProviderConfig, AIProviderType, AISafetyLevel, AIContextLevel, AIUserPromptSettings, AIMCPServerConfig, AIMCPToolDescriptor, AIMCPClientInstallStatus, AISkillConfig, AISkillScope } from '../types'; +import type { AIProviderConfig, AIProviderType, AISafetyLevel, AIContextLevel, AIUserPromptSettings, AIMCPServerConfig, AIMCPToolDescriptor, AIMCPClientInstallStatus, AISkillConfig } from '../types'; import { QWEN_BAILIAN_ANTHROPIC_BASE_URL, QWEN_CODING_PLAN_ANTHROPIC_BASE_URL, @@ -24,6 +24,7 @@ import type { OverlayWorkbenchTheme } from '../utils/overlayWorkbenchTheme'; import { BUILTIN_AI_TOOL_INFO } from '../utils/aiToolRegistry'; import AIBuiltinToolsCatalog from './ai/AIBuiltinToolsCatalog'; import AISettingsMCPSection, { type MCPClientKey } from './ai/AISettingsMCPSection'; +import AISettingsSkillsSection from './ai/AISettingsSkillsSection'; interface AISettingsModalProps { open: boolean; onClose: () => void; @@ -97,16 +98,27 @@ const EMPTY_AI_USER_PROMPT_SETTINGS: AIUserPromptSettings = { jvmDiagnostic: '', }; -const EMPTY_MCP_SERVER = (): AIMCPServerConfig => ({ - id: `mcp-draft-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`, - name: '', - transport: 'stdio', - command: '', - args: [], - env: {}, - enabled: true, - timeoutSeconds: 20, -}); +const EMPTY_MCP_SERVER = (seed?: Partial): AIMCPServerConfig => { + const base: AIMCPServerConfig = { + id: `mcp-draft-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`, + name: '', + transport: 'stdio', + command: '', + args: [], + env: {}, + enabled: true, + timeoutSeconds: 20, + }; + return { + ...base, + ...seed, + transport: seed?.transport || base.transport, + args: Array.isArray(seed?.args) ? seed.args : base.args, + env: seed?.env || base.env, + enabled: seed?.enabled ?? base.enabled, + timeoutSeconds: seed?.timeoutSeconds || base.timeoutSeconds, + }; +}; const EMPTY_MCP_CLIENT_STATUSES: AIMCPClientInstallStatus[] = [ { @@ -212,13 +224,6 @@ const EMPTY_SKILL = (): AISkillConfig => ({ requiredTools: [], }); -const SKILL_SCOPE_OPTIONS: Array<{ value: AISkillScope; label: string; desc: string }> = [ - { value: 'global', label: '全局', desc: '所有 AI 会话都启用' }, - { value: 'database', label: '数据库', desc: '仅 SQL / 数据库场景启用' }, - { value: 'jvm', label: 'JVM 资源', desc: '仅 JVM 资源分析场景启用' }, - { value: 'jvmDiagnostic', label: 'JVM 诊断', desc: '仅 JVM 诊断工作台启用' }, -]; - const AISettingsModal: React.FC = ({ open, onClose, darkMode, overlayTheme, focusProviderId }) => { const [providers, setProviders] = useState([]); const [activeProviderId, setActiveProviderId] = useState(''); @@ -581,8 +586,8 @@ const AISettingsModal: React.FC = ({ open, onClose, darkMo setMCPServers((prev) => prev.map((item) => item.id === id ? { ...item, ...patch } : item)); }; - const handleAddMCPServer = () => { - setMCPServers((prev) => [...prev, EMPTY_MCP_SERVER()]); + const handleAddMCPServer = (seed?: Partial) => { + setMCPServers((prev) => [...prev, EMPTY_MCP_SERVER(seed)]); }; const handleSaveMCPServer = async (server: AIMCPServerConfig) => { @@ -1189,75 +1194,6 @@ const AISettingsModal: React.FC = ({ open, onClose, darkMo ); - const renderSkillSettings = () => ( -
-
- Skill 不是另一条大提示词,而是“命名的提示模块 + 作用域 + 工具依赖”。当前阶段仍建议保留在主仓库内,不需要单独新建 GitHub 仓库;只有未来要做共享 skill pack 分发时,再考虑拆仓。 -
-
-
启用后会按 scope 注入对应会话;如果依赖的工具不存在,该 Skill 会被自动跳过。
- -
- {skills.length === 0 && ( -
- 还没有 Skill。你可以给数据库、JVM、诊断场景分别定义专用的 system prompt。 -
- )} - {skills.map((skill) => ( -
-
- updateSkillDraft(skill.id, { name: event.target.value })} - placeholder="Skill 名称,例如:SQL 审查 / JVM 诊断计划" - style={{ borderRadius: 10, background: inputBg, border: `1px solid ${cardBorder}` }} - /> - updateSkillDraft(skill.id, { description: event.target.value })} - placeholder="给自己看的说明,例如:输出 SQL 前必须先确认字段名和风险" - style={{ borderRadius: 10, background: inputBg, border: `1px solid ${cardBorder}` }} - /> - updateSkillDraft(skill.id, { requiredTools: value })} - options={skillRequiredToolOptions} - placeholder="可选:声明这个 Skill 依赖哪些工具" - style={{ width: '100%' }} - /> - updateSkillDraft(skill.id, { systemPrompt: event.target.value })} - placeholder="输入这条 Skill 要追加的 system prompt。建议聚焦一个明确能力,不要和全局提示词重复。" - style={{ borderRadius: 10, background: inputBg, border: `1px solid ${cardBorder}`, fontFamily: 'var(--gn-font-mono)', resize: 'vertical' }} - /> -
- - handleDeleteSkill(skill.id)}> - - -
-
- ))} -
- ); - const modalShellStyle = { background: overlayTheme.shellBg, border: overlayTheme.shellBorder, boxShadow: overlayTheme.shellShadow, backdropFilter: overlayTheme.shellBackdropFilter, @@ -1368,7 +1304,21 @@ const AISettingsModal: React.FC = ({ open, onClose, darkMo onDeleteServer={handleDeleteMCPServer} /> )} - {activeSection === 'skills' && renderSkillSettings()} + {activeSection === 'skills' && ( + + )} {activeSection === 'tools' && ( { expect(markup).toContain('get_indexes'); expect(markup).toContain('get_foreign_keys'); expect(markup).toContain('get_triggers'); + expect(markup).toContain('理解样例数据'); + expect(markup).toContain('preview_table_rows'); }); }); diff --git a/frontend/src/components/ai/AIBuiltinToolsCatalog.tsx b/frontend/src/components/ai/AIBuiltinToolsCatalog.tsx index be6c4b5..9faf6c3 100644 --- a/frontend/src/components/ai/AIBuiltinToolsCatalog.tsx +++ b/frontend/src/components/ai/AIBuiltinToolsCatalog.tsx @@ -27,9 +27,14 @@ const BUILTIN_TOOL_FLOWS = [ steps: 'get_columns → get_indexes → get_foreign_keys → get_triggers → get_table_ddl', description: '适合做索引优化、关系梳理、隐式副作用排查和 DDL 审查。', }, + { + title: '理解样例数据', + steps: 'get_columns → preview_table_rows', + description: '适合先确认字段,再直接查看前几行真实样例数据和空值形态。', + }, { title: '只读验证', - steps: 'get_columns → execute_sql', + steps: 'get_columns → preview_table_rows → execute_sql', description: '适合生成 SQL 后做小范围结果核对,仍会受 AI 安全级别控制。', }, ]; diff --git a/frontend/src/components/ai/AIMCPClientInstallPanel.test.tsx b/frontend/src/components/ai/AIMCPClientInstallPanel.test.tsx index 4500ecf..a889754 100644 --- a/frontend/src/components/ai/AIMCPClientInstallPanel.test.tsx +++ b/frontend/src/components/ai/AIMCPClientInstallPanel.test.tsx @@ -55,9 +55,10 @@ describe('AIMCPClientInstallPanel', () => { ); expect(markup).toContain('不是给 GoNavi 自己安装 MCP'); - expect(markup).toContain('接入外部客户端'); - expect(markup).toContain('目标客户端'); - expect(markup).toContain('未接入'); + expect(markup).toContain('安装到外部 AI 客户端'); + expect(markup).toContain('第 1 步:选择安装目标'); + expect(markup).toContain('第 2 步:确认当前状态并安装'); + expect(markup).toContain('待安装'); expect(markup).toContain('需更新'); expect(markup).toContain('复制配置路径'); expect(markup).toContain('复制启动命令'); diff --git a/frontend/src/components/ai/AIMCPClientInstallPanel.tsx b/frontend/src/components/ai/AIMCPClientInstallPanel.tsx index 4789504..cb81efd 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)', }; @@ -49,7 +49,7 @@ const getStatusTone = (status: AIMCPClientInstallStatus | undefined, darkMode: b }; } 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)', }; @@ -58,36 +58,36 @@ const getStatusTone = (status: AIMCPClientInstallStatus | undefined, darkMode: b const getStatusSummary = (status: AIMCPClientInstallStatus | undefined) => { const messageText = String(status?.message || ''); if (status?.matchesCurrent) { - return '已经是当前 GoNavi 路径,无需重复接入。'; + return '这个客户端已经安装当前 GoNavi MCP,不需要再装一遍。'; } if (status?.installed) { - return '检测到已有 GoNavi 记录,但不是当前路径,建议更新。'; + return '这个客户端已经有旧配置,更新后会改成当前 GoNavi 安装路径。'; } if (messageText.includes('失败') || messageText.includes('异常')) { - return '状态读取异常,建议刷新后再检查一次。'; + return '状态读取异常,建议先刷新,再决定是否安装。'; } - return '当前还没有把 GoNavi MCP 写入这个客户端。'; + return '这个客户端还没有安装 GoNavi MCP。'; }; const getClientCardDescription = (status: AIMCPClientInstallStatus | undefined) => { if (status?.matchesCurrent) { - return '当前 GoNavi 路径已经写入,可直接在该客户端中调用。'; + return '当前 GoNavi 路径已经写入,可直接在这个客户端里调用。'; } if (status?.installed) { return '检测到旧的 GoNavi 记录,建议更新为当前安装路径。'; } - return '尚未写入 GoNavi MCP 配置。'; + return '还没有写入 GoNavi MCP 配置。'; }; const resolveActionLabel = (status: AIMCPClientInstallStatus | undefined) => { const label = status?.displayName || '客户端'; if (status?.matchesCurrent) { - return `${label} 已接入`; + return `已安装到 ${label}`; } if (status?.installed) { return `更新 ${label} 配置`; } - return `接入 ${label}`; + return `安装到 ${label}`; }; const AIMCPClientInstallPanel: React.FC = ({ @@ -109,7 +109,7 @@ const AIMCPClientInstallPanel: React.FC = ({ }) => (
- 这里不是给 GoNavi 自己安装 MCP,而是把 GoNavi 作为 MCP Server 接入 Claude Code 或 Codex 这类外部 AI 客户端。 + 这里不是给 GoNavi 自己安装 MCP,而是把 GoNavi 作为 MCP Server 安装到 Claude Code、Codex 这类外部 AI 客户端里使用。
= ({ }} >
-
接入外部客户端
+
安装到外部 AI 客户端
- 选择目标客户端后,GoNavi 会自动把当前安装路径写入它的用户级 MCP 配置文件,不需要你自己去找本机 exe 或手动改配置。 + 先选 1 个要安装到的目标客户端,GoNavi 会自动把当前安装路径写入它的用户级 MCP 配置文件,不需要你自己找本机 exe,也不用手改配置。
@@ -141,11 +141,16 @@ const AIMCPClientInstallPanel: React.FC = ({ lineHeight: 1.7, }} > - 只会修改你选中的客户端用户级 MCP 配置,不会安装新的 GoNavi,也不会替换 GoNavi 自己的程序文件。 + 只会修改你选中的外部客户端用户级 MCP 配置,不会下载新的 GoNavi,也不会替换 GoNavi 自己的程序文件。
-
目标客户端
+
+
第 1 步:选择安装目标
+
+ 每次只安装到一个外部客户端,避免重复写入。 +
+
{statuses.map((status) => { const client = status.client === 'codex' ? 'codex' : 'claude-code'; @@ -227,24 +232,29 @@ const AIMCPClientInstallPanel: React.FC = ({ gap: 6, }} > -
+
- {selectedStatus?.displayName || '客户端'} 状态 + 第 2 步:确认当前状态并安装
- {selectedStatus && ( -
- {getStatusTone(selectedStatus, darkMode).label} +
+
+ {selectedStatus?.displayName || '客户端'} 状态
- )} + {selectedStatus && ( +
+ {getStatusTone(selectedStatus, darkMode).label} +
+ )} +
{getStatusSummary(selectedStatus)} @@ -295,7 +305,7 @@ const AIMCPClientInstallPanel: React.FC = ({
- 写入后重启对应客户端即可生效;如果已经是当前路径,会直接显示“已接入”,避免重复接入。 + 写入后重启对应客户端即可生效;如果已经是当前路径,按钮会自动禁用,避免重复安装。
+ ))} +
+
支持命令、参数、环境变量和超时,保存后会自动进入 AI 工具列表。
- +
{mcpServers.length === 0 && (
diff --git a/frontend/src/components/ai/AISettingsSkillsSection.test.tsx b/frontend/src/components/ai/AISettingsSkillsSection.test.tsx new file mode 100644 index 0000000..5317d01 --- /dev/null +++ b/frontend/src/components/ai/AISettingsSkillsSection.test.tsx @@ -0,0 +1,30 @@ +import React from 'react'; +import { renderToStaticMarkup } from 'react-dom/server'; +import { describe, expect, it } from 'vitest'; + +import AISettingsSkillsSection from './AISettingsSkillsSection'; +import { buildOverlayWorkbenchTheme } from '../../utils/overlayWorkbenchTheme'; + +describe('AISettingsSkillsSection', () => { + it('renders the extracted skill configuration section', () => { + const markup = renderToStaticMarkup( + {}} + onUpdateSkillDraft={() => {}} + onSaveSkill={() => {}} + onDeleteSkill={() => {}} + />, + ); + + expect(markup).toContain('新增 Skill'); + expect(markup).toContain('还没有 Skill'); + expect(markup).toContain('命名的提示模块'); + }); +}); diff --git a/frontend/src/components/ai/AISettingsSkillsSection.tsx b/frontend/src/components/ai/AISettingsSkillsSection.tsx new file mode 100644 index 0000000..8bab4c8 --- /dev/null +++ b/frontend/src/components/ai/AISettingsSkillsSection.tsx @@ -0,0 +1,110 @@ +import React from 'react'; +import { Button, Input, Popconfirm, Select } from 'antd'; +import { DeleteOutlined, PlusOutlined } from '@ant-design/icons'; + +import type { AISkillConfig, AISkillScope } from '../../types'; +import type { OverlayWorkbenchTheme } from '../../utils/overlayWorkbenchTheme'; + +interface AISettingsSkillsSectionProps { + skills: AISkillConfig[]; + skillRequiredToolOptions: Array<{ label: string; value: string }>; + overlayTheme: OverlayWorkbenchTheme; + cardBg: string; + cardBorder: string; + inputBg: string; + loading: boolean; + onAddSkill: () => void; + onUpdateSkillDraft: (id: string, patch: Partial) => void; + onSaveSkill: (skill: AISkillConfig) => void; + onDeleteSkill: (id: string) => void; +} + +const SKILL_SCOPE_OPTIONS: Array<{ value: AISkillScope; label: string; desc: string }> = [ + { value: 'global', label: '全局', desc: '所有 AI 会话都启用' }, + { value: 'database', label: '数据库', desc: '仅 SQL / 数据库场景启用' }, + { value: 'jvm', label: 'JVM 资源', desc: '仅 JVM 资源分析场景启用' }, + { value: 'jvmDiagnostic', label: 'JVM 诊断', desc: '仅 JVM 诊断工作台启用' }, +]; + +const AISettingsSkillsSection: React.FC = ({ + skills, + skillRequiredToolOptions, + overlayTheme, + cardBg, + cardBorder, + inputBg, + loading, + onAddSkill, + onUpdateSkillDraft, + onSaveSkill, + onDeleteSkill, +}) => ( +
+
+ Skill 不是另一条大提示词,而是“命名的提示模块 + 作用域 + 工具依赖”。当前阶段仍建议保留在主仓库内,不需要单独新建 GitHub 仓库;只有未来要做共享 skill pack 分发时,再考虑拆仓。 +
+
+
启用后会按 scope 注入对应会话;如果依赖的工具不存在,该 Skill 会被自动跳过。
+ +
+ {skills.length === 0 && ( +
+ 还没有 Skill。你可以给数据库、JVM、诊断场景分别定义专用的 system prompt。 +
+ )} + {skills.map((skill) => ( +
+
+ onUpdateSkillDraft(skill.id, { name: event.target.value })} + placeholder="Skill 名称,例如:SQL 审查 / JVM 诊断计划" + style={{ borderRadius: 10, background: inputBg, border: `1px solid ${cardBorder}` }} + /> + onUpdateSkillDraft(skill.id, { description: event.target.value })} + placeholder="给自己看的说明,例如:输出 SQL 前必须先确认字段名和风险" + style={{ borderRadius: 10, background: inputBg, border: `1px solid ${cardBorder}` }} + /> + onUpdateSkillDraft(skill.id, { requiredTools: value })} + options={skillRequiredToolOptions} + placeholder="可选:声明这个 Skill 依赖哪些工具" + style={{ width: '100%' }} + /> + onUpdateSkillDraft(skill.id, { systemPrompt: event.target.value })} + placeholder="输入这条 Skill 要追加的 system prompt。建议聚焦一个明确能力,不要和全局提示词重复。" + style={{ borderRadius: 10, background: inputBg, border: `1px solid ${cardBorder}`, fontFamily: 'var(--gn-font-mono)', resize: 'vertical' }} + /> +
+ + onDeleteSkill(skill.id)}> + + +
+
+ ))} +
+); + +export default AISettingsSkillsSection; diff --git a/frontend/src/components/ai/aiLocalToolExecutor.test.ts b/frontend/src/components/ai/aiLocalToolExecutor.test.ts index 3d32493..2b96b39 100644 --- a/frontend/src/components/ai/aiLocalToolExecutor.test.ts +++ b/frontend/src/components/ai/aiLocalToolExecutor.test.ts @@ -156,4 +156,41 @@ describe('aiLocalToolExecutor', () => { expect(indexResult.content).toContain('idx_users_email'); expect(message.tool_name).toBe('自定义探针'); }); + + it('previews sample rows for a table without forcing the model to handwrite select limit sql', async () => { + const query = vi.fn().mockResolvedValue({ + success: true, + data: [ + { id: 1, status: 'paid', amount: 120.5 }, + { id: 2, status: 'pending', amount: null }, + ], + }); + const result = await executeLocalAIToolCall({ + toolCall: buildToolCall('preview_table_rows', { + connectionId: 'conn-1', + dbName: 'crm', + tableName: 'orders', + limit: 5, + }), + connections: [buildConnection()], + mcpTools: [], + toolContextMap: new Map(), + runtime: { + getDatabases: vi.fn(), + getTables: vi.fn(), + getColumns: vi.fn(), + getIndexes: vi.fn(), + getForeignKeys: vi.fn(), + getTriggers: vi.fn(), + showCreateTable: vi.fn(), + query, + }, + }); + + expect(result.success).toBe(true); + expect(query).toHaveBeenCalledWith(expect.anything(), 'crm', 'SELECT * FROM `orders` LIMIT 5 OFFSET 0'); + expect(result.content).toContain('"tableName":"orders"'); + expect(result.content).toContain('"status":"paid"'); + expect(result.content).toContain('"rowCount":2'); + }); }); diff --git a/frontend/src/components/ai/aiLocalToolExecutor.ts b/frontend/src/components/ai/aiLocalToolExecutor.ts index 245d349..67eb4ce 100644 --- a/frontend/src/components/ai/aiLocalToolExecutor.ts +++ b/frontend/src/components/ai/aiLocalToolExecutor.ts @@ -3,6 +3,7 @@ import { DBGetAllColumns, DBGetDatabases, DBGetTables } from '../../../wailsjs/g import type { AIChatMessage, AIMCPToolDescriptor, AIToolCall, SavedConnection } from '../../types'; import { buildRpcConnectionConfig } from '../../utils/connectionRpcConfig'; import { buildAIReadonlyPreviewSQL } from '../../utils/aiSqlLimit'; +import { buildPaginatedSelectSQL, quoteQualifiedIdent } from '../../utils/sql'; import { resolveAITableSchemaToolResult } from '../../utils/aiTableSchemaTool'; export interface AIToolContextEntry { @@ -115,6 +116,13 @@ const buildToolName = (toolCall: AIToolCall, descriptor?: AIMCPToolDescriptor) = const findConnection = (connections: SavedConnection[], connectionId: string) => connections.find((connection) => connection.id === connectionId); +const normalizePreviewLimit = (input: unknown): number => { + const value = Math.floor(Number(input) || 20); + if (value < 1) return 1; + if (value > 100) return 100; + return value; +}; + export async function executeLocalAIToolCall({ toolCall, connections, @@ -331,6 +339,47 @@ export async function executeLocalAIToolCall({ } break; } + case 'preview_table_rows': { + const connection = findConnection(connections, args.connectionId); + if (!connection) { + content = 'Connection not found'; + break; + } + try { + const safeDbName = args.dbName ? String(args.dbName).trim() : ''; + const safeTable = args.tableName ? String(args.tableName).trim() : ''; + if (!safeTable) { + content = 'tableName 不能为空'; + break; + } + const safeLimit = normalizePreviewLimit(args.limit); + const dbType = String(connection.config?.type || '').trim(); + const previewSQL = buildPaginatedSelectSQL( + dbType, + `SELECT * FROM ${quoteQualifiedIdent(dbType, safeTable)}`, + '', + safeLimit, + 0, + ); + const result = await mergedRuntime.query(buildRpcConnectionConfig(connection.config) as any, safeDbName, previewSQL); + if (result?.success) { + const rows = Array.isArray(result.data) ? result.data : []; + content = JSON.stringify({ + dbName: safeDbName, + tableName: safeTable, + limit: safeLimit, + rowCount: rows.length, + rows: rows.slice(0, safeLimit), + }); + success = true; + } else { + content = result?.message || 'Failed to preview table rows'; + } + } catch (error: any) { + content = `预览表样例数据失败: ${error?.message || error}`; + } + break; + } case 'execute_sql': { const connection = findConnection(connections, args.connectionId); if (!connection) { diff --git a/frontend/src/utils/aiToolRegistry.ts b/frontend/src/utils/aiToolRegistry.ts index 9bfb3fa..04a885e 100644 --- a/frontend/src/utils/aiToolRegistry.ts +++ b/frontend/src/utils/aiToolRegistry.ts @@ -229,6 +229,32 @@ export const BUILTIN_AI_TOOL_INFO: AIBuiltinToolInfo[] = [ }, }, }, + { + name: "preview_table_rows", + icon: "👀", + desc: "抽样预览指定表的前几行数据", + detail: + "传入 connectionId、dbName、tableName 和可选 limit,返回该表的前几行真实样例数据。适合先看数据形态、空值分布和枚举值,再决定怎么写 SQL。", + params: "connectionId, dbName, tableName, limit?", + tool: { + type: "function", + function: { + name: "preview_table_rows", + description: + "预览指定表的前几行样例数据。适用于快速理解字段取值形态、空值情况、时间格式和状态枚举,减少模型盲写 SQL。", + parameters: { + type: "object", + properties: { + connectionId: { type: "string", description: "连接ID" }, + dbName: { type: "string", description: "数据库名" }, + tableName: { type: "string", description: "表名" }, + limit: { type: "number", description: "可选,预览行数,默认 20,最大 100" }, + }, + required: ["connectionId", "dbName", "tableName"], + }, + }, + }, + }, { name: "execute_sql", icon: "▶️", diff --git a/frontend/src/utils/mcpServerTemplates.ts b/frontend/src/utils/mcpServerTemplates.ts new file mode 100644 index 0000000..ba95367 --- /dev/null +++ b/frontend/src/utils/mcpServerTemplates.ts @@ -0,0 +1,64 @@ +import type { AIMCPServerConfig } from '../types'; + +export interface MCPServerDraftTemplate { + key: string; + title: string; + description: string; + detail: string; + seed: Partial; +} + +export const MCP_SERVER_DRAFT_TEMPLATES: MCPServerDraftTemplate[] = [ + { + key: 'uvx', + title: 'uvx 工具', + description: '适合 Python/uv 生态里已经发布好的 MCP 包。', + detail: '示例会填成 `uvx some-mcp-server`,保存前把包名改成你自己的。', + seed: { + name: 'uvx 工具', + command: 'uvx', + args: ['some-mcp-server'], + env: {}, + timeoutSeconds: 20, + }, + }, + { + key: 'node', + title: 'Node 脚本', + description: '适合本地 js/ts 脚本或 npm 安装后的 node 启动器。', + detail: '示例会填成 `node server.js --stdio`,脚本名和参数可以继续改。', + seed: { + name: 'Node 脚本', + command: 'node', + args: ['server.js', '--stdio'], + env: {}, + timeoutSeconds: 20, + }, + }, + { + key: 'python', + title: 'Python 模块', + description: '适合 `python -m xxx` 这种按模块启动的服务。', + detail: '示例会填成 `python -m your_mcp_server`,模块名改成实际值即可。', + seed: { + name: 'Python 模块', + command: 'python', + args: ['-m', 'your_mcp_server'], + env: {}, + timeoutSeconds: 20, + }, + }, + { + key: 'exe', + title: '本机 EXE', + description: '适合已经编译好的本机二进制或公司内部工具。', + detail: '示例会填成 `your-mcp-server.exe stdio`,把 exe 路径换成真实值。', + seed: { + name: '本机 EXE', + command: 'your-mcp-server.exe', + args: ['stdio'], + env: {}, + timeoutSeconds: 20, + }, + }, +];