diff --git a/frontend/src/components/AISettingsModal.edit-password.test.tsx b/frontend/src/components/AISettingsModal.edit-password.test.tsx index f2b00d8..b3e3990 100644 --- a/frontend/src/components/AISettingsModal.edit-password.test.tsx +++ b/frontend/src/components/AISettingsModal.edit-password.test.tsx @@ -22,26 +22,23 @@ describe('AISettingsModal edit password behavior', () => { expect(source).toContain('Service.AIGetMCPServers?.()'); expect(source).toContain('Service.AIListMCPTools?.()'); expect(source).toContain('Service.AIGetSkills?.()'); - expect(source).toContain('新增 MCP 服务'); expect(source).toContain('新增 Skill'); }); it('delegates bulky MCP and built-in tool sections to dedicated ai components', () => { expect(source).toContain("import AIBuiltinToolsCatalog from './ai/AIBuiltinToolsCatalog';"); - expect(source).toContain("import AIMCPClientInstallPanel from './ai/AIMCPClientInstallPanel';"); - expect(source).toContain("import AIMCPServerCard from './ai/AIMCPServerCard';"); - expect(source).toContain(' { - expect(source).toContain('statuses={mcpClientStatuses}'); - expect(source).toContain('selectedClient={selectedMCPClient}'); + expect(source).toContain('mcpClientStatuses={mcpClientStatuses}'); + expect(source).toContain('selectedMCPClient={selectedMCPClient}'); expect(source).toContain('onRefreshStatus={() => void loadMCPClientStatuses()}'); expect(source).toContain('onCopyConfigPath={() => void handleCopySelectedMCPConfigPath()}'); expect(source).toContain('onCopyLaunchCommand={() => void handleCopySelectedMCPLaunchCommand()}'); - expect(source).toContain('handleInstallSelectedMCPClient'); + expect(source).toContain('onInstallSelectedClient={handleInstallSelectedMCPClient}'); }); it('waits briefly for the AI service bridge before warning and removes noisy provider debug logs', () => { diff --git a/frontend/src/components/AISettingsModal.tsx b/frontend/src/components/AISettingsModal.tsx index b2fc6c7..ef2c8bf 100644 --- a/frontend/src/components/AISettingsModal.tsx +++ b/frontend/src/components/AISettingsModal.tsx @@ -23,8 +23,7 @@ import { buildAddProviderEditorSession, buildClosedProviderEditorSession, buildE import type { OverlayWorkbenchTheme } from '../utils/overlayWorkbenchTheme'; import { BUILTIN_AI_TOOL_INFO } from '../utils/aiToolRegistry'; import AIBuiltinToolsCatalog from './ai/AIBuiltinToolsCatalog'; -import AIMCPClientInstallPanel from './ai/AIMCPClientInstallPanel'; -import AIMCPServerCard from './ai/AIMCPServerCard'; +import AISettingsMCPSection, { type MCPClientKey } from './ai/AISettingsMCPSection'; interface AISettingsModalProps { open: boolean; onClose: () => void; @@ -42,8 +41,6 @@ interface MCPClientInstallResult { args?: string[]; } -type MCPClientKey = 'claude-code' | 'codex'; - // 预设配置:每个预设映射到后端 type(openai/anthropic/gemini/custom)并附带默认 URL 和 Model interface ProviderPreset { key: string; @@ -1192,54 +1189,6 @@ const AISettingsModal: React.FC = ({ open, onClose, darkMo ); - const renderMCPSettings = () => ( -
- void loadMCPClientStatuses()} - onCopyConfigPath={() => void handleCopySelectedMCPConfigPath()} - onCopyLaunchCommand={() => void handleCopySelectedMCPLaunchCommand()} - onInstall={handleInstallSelectedMCPClient} - /> -
-
支持命令、参数、环境变量和超时,保存后会自动进入 AI 工具列表。
- -
- {mcpServers.length === 0 && ( -
- 还没有 MCP 服务。常见形式是 `node server.js`、`uvx some-mcp-server`、`python -m server`。 -
- )} - {mcpServers.map((server) => ( - tool.serverId === server.id)} - cardBg={cardBg} - cardBorder={cardBorder} - inputBg={inputBg} - darkMode={darkMode} - overlayTheme={overlayTheme} - loading={loading} - onChange={(patch) => updateMCPServerDraft(server.id, patch)} - onTest={() => handleTestMCPServer(server)} - onSave={() => handleSaveMCPServer(server)} - onDelete={() => handleDeleteMCPServer(server.id)} - /> - ))} -
- ); - const renderSkillSettings = () => (
@@ -1392,7 +1341,33 @@ const AISettingsModal: React.FC = ({ open, onClose, darkMo {activeSection === 'providers' && (isEditing ? renderProviderForm() : renderProviderList())} {activeSection === 'safety' && renderSafetySettings()} {activeSection === 'context' && renderContextSettings()} - {activeSection === 'mcp' && renderMCPSettings()} + {activeSection === 'mcp' && ( + void loadMCPClientStatuses()} + onCopyConfigPath={() => void handleCopySelectedMCPConfigPath()} + onCopyLaunchCommand={() => void handleCopySelectedMCPLaunchCommand()} + onInstallSelectedClient={handleInstallSelectedMCPClient} + onAddServer={handleAddMCPServer} + onUpdateServerDraft={updateMCPServerDraft} + onTestServer={handleTestMCPServer} + onSaveServer={handleSaveMCPServer} + onDeleteServer={handleDeleteMCPServer} + /> + )} {activeSection === 'skills' && renderSkillSettings()} {activeSection === 'tools' && ( { - it('renders the deep structure analysis flow and the newly added built-in tools', () => { + it('renders the field-to-table flow and the deeper structure analysis tools', () => { const markup = renderToStaticMarkup( { />, ); + expect(markup).toContain('字段反查表'); + expect(markup).toContain('get_all_columns'); expect(markup).toContain('结构深挖'); expect(markup).toContain('get_indexes'); expect(markup).toContain('get_foreign_keys'); diff --git a/frontend/src/components/ai/AIBuiltinToolsCatalog.tsx b/frontend/src/components/ai/AIBuiltinToolsCatalog.tsx index da33dd7..be6c4b5 100644 --- a/frontend/src/components/ai/AIBuiltinToolsCatalog.tsx +++ b/frontend/src/components/ai/AIBuiltinToolsCatalog.tsx @@ -17,6 +17,11 @@ const BUILTIN_TOOL_FLOWS = [ steps: 'get_connections → get_databases → get_tables → get_columns', description: '适合先找连接、找库、找表,再确认真实字段名后生成 SQL。', }, + { + title: '字段反查表', + steps: 'get_databases → get_all_columns', + description: '适合只知道字段名、业务含义或注释关键词,但还不确定具体落在哪张表。', + }, { title: '结构深挖', steps: 'get_columns → get_indexes → get_foreign_keys → get_triggers → get_table_ddl', diff --git a/frontend/src/components/ai/AIMCPServerCard.test.tsx b/frontend/src/components/ai/AIMCPServerCard.test.tsx index bb891a8..e255354 100644 --- a/frontend/src/components/ai/AIMCPServerCard.test.tsx +++ b/frontend/src/components/ai/AIMCPServerCard.test.tsx @@ -6,15 +6,15 @@ import AIMCPServerCard from './AIMCPServerCard'; import { buildOverlayWorkbenchTheme } from '../../utils/overlayWorkbenchTheme'; describe('AIMCPServerCard', () => { - it('renders explicit MCP parameter hints for command, args, and env', () => { + it('renders explicit MCP parameter hints and the actual launch preview for command, args, and env', () => { const markup = renderToStaticMarkup( { expect(markup).toContain('每个参数单独录入一个标签'); expect(markup).toContain('每行一个 KEY=VALUE'); expect(markup).toContain('当前阶段只支持 stdio'); + expect(markup).toContain('实际启动命令预览'); + expect(markup).toContain('node server.js --stdio'); }); }); diff --git a/frontend/src/components/ai/AIMCPServerCard.tsx b/frontend/src/components/ai/AIMCPServerCard.tsx index de57844..ba3098d 100644 --- a/frontend/src/components/ai/AIMCPServerCard.tsx +++ b/frontend/src/components/ai/AIMCPServerCard.tsx @@ -37,6 +37,20 @@ const MCP_COMMAND_EXAMPLES = [ 'python -m your_mcp_server', ]; +const quoteCommandPart = (value: string): string => { + const text = String(value || '').trim(); + if (!text) { + return ''; + } + return /[\s"]/u.test(text) ? `"${text.replace(/"/g, '\\"')}"` : text; +}; + +const formatLaunchPreview = (command: string, args?: string[]): string => + [command, ...(Array.isArray(args) ? args : [])] + .map((item) => quoteCommandPart(item)) + .filter(Boolean) + .join(' '); + const MCPHelpBlock: React.FC<{ title: string; description: string; @@ -71,121 +85,137 @@ export const AIMCPServerCard: React.FC = ({ onTest, onSave, onDelete, -}) => ( -
-
-
填写示例
-
- 启动命令只填可执行程序本身,不要把参数混在一起。常见形式: - {' '} - {MCP_COMMAND_EXAMPLES.join(' / ')} -
-
+}) => { + const launchPreview = formatLaunchPreview(server.command, server.args); -
- - onChange({ name: event.target.value })} - placeholder="服务名称,例如:Filesystem / Browser / GitHub" - style={{ borderRadius: 10, background: inputBg, border: `1px solid ${cardBorder}` }} - /> - - - onChange({ transport: value as AIMCPServerConfig['transport'] })} - options={[{ label: 'stdio', value: 'stdio' }]} - /> - - - onChange({ command: event.target.value })} - placeholder="启动命令,例如:node / uvx / python" - style={{ borderRadius: 10, background: inputBg, border: `1px solid ${cardBorder}` }} - /> - - - onChange({ timeoutSeconds: Number(event.target.value) || 20 })} - placeholder="超时(秒)" - style={{ borderRadius: 10, background: inputBg, border: `1px solid ${cardBorder}` }} - /> - -
- - - onChange({ name: event.target.value })} + placeholder="服务名称,例如:Filesystem / Browser / GitHub" + style={{ borderRadius: 10, background: inputBg, border: `1px solid ${cardBorder}` }} + /> + + + onChange({ transport: value as AIMCPServerConfig['transport'] })} + options={[{ label: 'stdio', value: 'stdio' }]} + /> + + + onChange({ command: event.target.value })} + placeholder="启动命令,例如:node / uvx / python" + style={{ borderRadius: 10, background: inputBg, border: `1px solid ${cardBorder}` }} + /> + + + onChange({ timeoutSeconds: Number(event.target.value) || 20 })} + placeholder="超时(秒)" + style={{ borderRadius: 10, background: inputBg, border: `1px solid ${cardBorder}` }} + /> + +
+ + +