mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-06-15 19:19:35 +08:00
✨ feat(ai-settings): 优化MCP安装引导并补充表样例预览工具
- 拆分 Skills 区块并补充 MCP 启动模板\n- 改造 Claude/Codex 安装目标选择与状态展示\n- 新增 preview_table_rows 内置工具与目录说明
This commit is contained in:
@@ -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('<AISettingsSkillsSection');
|
||||
});
|
||||
|
||||
it('delegates bulky MCP and built-in tool sections to dedicated ai components', () => {
|
||||
|
||||
@@ -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>): 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<AISettingsModalProps> = ({ open, onClose, darkMode, overlayTheme, focusProviderId }) => {
|
||||
const [providers, setProviders] = useState<AIProviderConfig[]>([]);
|
||||
const [activeProviderId, setActiveProviderId] = useState<string>('');
|
||||
@@ -581,8 +586,8 @@ const AISettingsModal: React.FC<AISettingsModalProps> = ({ 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<AIMCPServerConfig>) => {
|
||||
setMCPServers((prev) => [...prev, EMPTY_MCP_SERVER(seed)]);
|
||||
};
|
||||
|
||||
const handleSaveMCPServer = async (server: AIMCPServerConfig) => {
|
||||
@@ -1189,75 +1194,6 @@ const AISettingsModal: React.FC<AISettingsModalProps> = ({ open, onClose, darkMo
|
||||
</div>
|
||||
);
|
||||
|
||||
const renderSkillSettings = () => (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
|
||||
<div style={{ fontSize: 13, color: overlayTheme.mutedText, marginBottom: 4 }}>
|
||||
Skill 不是另一条大提示词,而是“命名的提示模块 + 作用域 + 工具依赖”。当前阶段仍建议保留在主仓库内,不需要单独新建 GitHub 仓库;只有未来要做共享 skill pack 分发时,再考虑拆仓。
|
||||
</div>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', gap: 12 }}>
|
||||
<div style={{ fontSize: 12, color: overlayTheme.mutedText }}>启用后会按 scope 注入对应会话;如果依赖的工具不存在,该 Skill 会被自动跳过。</div>
|
||||
<Button icon={<PlusOutlined />} onClick={handleAddSkill} style={{ borderRadius: 10 }}>新增 Skill</Button>
|
||||
</div>
|
||||
{skills.length === 0 && (
|
||||
<div style={{ padding: '18px 16px', borderRadius: 14, border: `1px dashed ${cardBorder}`, background: cardBg, color: overlayTheme.mutedText }}>
|
||||
还没有 Skill。你可以给数据库、JVM、诊断场景分别定义专用的 system prompt。
|
||||
</div>
|
||||
)}
|
||||
{skills.map((skill) => (
|
||||
<div key={skill.id} style={{ padding: '14px 16px', borderRadius: 14, border: `1px solid ${cardBorder}`, background: cardBg, display: 'flex', flexDirection: 'column', gap: 12 }}>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'minmax(0,1fr) 132px', gap: 12 }}>
|
||||
<Input
|
||||
value={skill.name}
|
||||
onChange={(event) => updateSkillDraft(skill.id, { name: event.target.value })}
|
||||
placeholder="Skill 名称,例如:SQL 审查 / JVM 诊断计划"
|
||||
style={{ borderRadius: 10, background: inputBg, border: `1px solid ${cardBorder}` }}
|
||||
/>
|
||||
<Select
|
||||
value={skill.enabled ? 'enabled' : 'disabled'}
|
||||
onChange={(value) => updateSkillDraft(skill.id, { enabled: value === 'enabled' })}
|
||||
options={[{ label: '已启用', value: 'enabled' }, { label: '已禁用', value: 'disabled' }]}
|
||||
/>
|
||||
</div>
|
||||
<Input
|
||||
value={skill.description || ''}
|
||||
onChange={(event) => updateSkillDraft(skill.id, { description: event.target.value })}
|
||||
placeholder="给自己看的说明,例如:输出 SQL 前必须先确认字段名和风险"
|
||||
style={{ borderRadius: 10, background: inputBg, border: `1px solid ${cardBorder}` }}
|
||||
/>
|
||||
<Select
|
||||
mode="multiple"
|
||||
value={skill.scopes || []}
|
||||
onChange={(value) => updateSkillDraft(skill.id, { scopes: value as AISkillScope[] })}
|
||||
options={SKILL_SCOPE_OPTIONS.map((option) => ({ label: `${option.label} · ${option.desc}`, value: option.value }))}
|
||||
placeholder="选择这个 Skill 要作用到哪些场景"
|
||||
style={{ width: '100%' }}
|
||||
/>
|
||||
<Select
|
||||
mode="multiple"
|
||||
value={skill.requiredTools || []}
|
||||
onChange={(value) => updateSkillDraft(skill.id, { requiredTools: value })}
|
||||
options={skillRequiredToolOptions}
|
||||
placeholder="可选:声明这个 Skill 依赖哪些工具"
|
||||
style={{ width: '100%' }}
|
||||
/>
|
||||
<Input.TextArea
|
||||
rows={6}
|
||||
value={skill.systemPrompt}
|
||||
onChange={(event) => 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' }}
|
||||
/>
|
||||
<div style={{ display: 'flex', justifyContent: 'flex-end', gap: 8 }}>
|
||||
<Button type="primary" onClick={() => handleSaveSkill(skill)} loading={loading} style={{ borderRadius: 10, fontWeight: 600 }}>保存</Button>
|
||||
<Popconfirm title="删除这个 Skill?" okText="删除" cancelText="取消" onConfirm={() => handleDeleteSkill(skill.id)}>
|
||||
<Button danger icon={<DeleteOutlined />} style={{ borderRadius: 10 }}>删除</Button>
|
||||
</Popconfirm>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
|
||||
const modalShellStyle = {
|
||||
background: overlayTheme.shellBg, border: overlayTheme.shellBorder,
|
||||
boxShadow: overlayTheme.shellShadow, backdropFilter: overlayTheme.shellBackdropFilter,
|
||||
@@ -1368,7 +1304,21 @@ const AISettingsModal: React.FC<AISettingsModalProps> = ({ open, onClose, darkMo
|
||||
onDeleteServer={handleDeleteMCPServer}
|
||||
/>
|
||||
)}
|
||||
{activeSection === 'skills' && renderSkillSettings()}
|
||||
{activeSection === 'skills' && (
|
||||
<AISettingsSkillsSection
|
||||
skills={skills}
|
||||
skillRequiredToolOptions={skillRequiredToolOptions}
|
||||
overlayTheme={overlayTheme}
|
||||
cardBg={cardBg}
|
||||
cardBorder={cardBorder}
|
||||
inputBg={inputBg}
|
||||
loading={loading}
|
||||
onAddSkill={handleAddSkill}
|
||||
onUpdateSkillDraft={updateSkillDraft}
|
||||
onSaveSkill={handleSaveSkill}
|
||||
onDeleteSkill={handleDeleteSkill}
|
||||
/>
|
||||
)}
|
||||
{activeSection === 'tools' && (
|
||||
<AIBuiltinToolsCatalog
|
||||
darkMode={darkMode}
|
||||
|
||||
@@ -22,5 +22,7 @@ describe('AIBuiltinToolsCatalog', () => {
|
||||
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');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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 安全级别控制。',
|
||||
},
|
||||
];
|
||||
|
||||
@@ -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('复制启动命令');
|
||||
|
||||
@@ -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<AIMCPClientInstallPanelProps> = ({
|
||||
@@ -109,7 +109,7 @@ const AIMCPClientInstallPanel: React.FC<AIMCPClientInstallPanelProps> = ({
|
||||
}) => (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
|
||||
<div style={{ fontSize: 13, color: overlayTheme.mutedText, marginBottom: 4, lineHeight: 1.7 }}>
|
||||
这里不是给 GoNavi 自己安装 MCP,而是把 GoNavi 作为 MCP Server 接入 Claude Code 或 Codex 这类外部 AI 客户端。
|
||||
这里不是给 GoNavi 自己安装 MCP,而是把 GoNavi 作为 MCP Server 安装到 Claude Code、Codex 这类外部 AI 客户端里使用。
|
||||
</div>
|
||||
|
||||
<div
|
||||
@@ -124,9 +124,9 @@ const AIMCPClientInstallPanel: React.FC<AIMCPClientInstallPanelProps> = ({
|
||||
}}
|
||||
>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
|
||||
<div style={{ fontWeight: 700, fontSize: 14, color: overlayTheme.titleText }}>接入外部客户端</div>
|
||||
<div style={{ fontWeight: 700, fontSize: 14, color: overlayTheme.titleText }}>安装到外部 AI 客户端</div>
|
||||
<div style={{ fontSize: 12, color: overlayTheme.mutedText, lineHeight: 1.7 }}>
|
||||
选择目标客户端后,GoNavi 会自动把当前安装路径写入它的用户级 MCP 配置文件,不需要你自己去找本机 exe 或手动改配置。
|
||||
先选 1 个要安装到的目标客户端,GoNavi 会自动把当前安装路径写入它的用户级 MCP 配置文件,不需要你自己找本机 exe,也不用手改配置。
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -141,11 +141,16 @@ const AIMCPClientInstallPanel: React.FC<AIMCPClientInstallPanelProps> = ({
|
||||
lineHeight: 1.7,
|
||||
}}
|
||||
>
|
||||
只会修改你选中的客户端用户级 MCP 配置,不会安装新的 GoNavi,也不会替换 GoNavi 自己的程序文件。
|
||||
只会修改你选中的外部客户端用户级 MCP 配置,不会下载新的 GoNavi,也不会替换 GoNavi 自己的程序文件。
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
|
||||
<div style={{ fontWeight: 700, fontSize: 13, color: overlayTheme.titleText }}>目标客户端</div>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
|
||||
<div style={{ fontWeight: 700, fontSize: 13, color: overlayTheme.titleText }}>第 1 步:选择安装目标</div>
|
||||
<div style={{ fontSize: 12, color: overlayTheme.mutedText, lineHeight: 1.6 }}>
|
||||
每次只安装到一个外部客户端,避免重复写入。
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(220px, 1fr))', gap: 10 }}>
|
||||
{statuses.map((status) => {
|
||||
const client = status.client === 'codex' ? 'codex' : 'claude-code';
|
||||
@@ -227,24 +232,29 @@ const AIMCPClientInstallPanel: React.FC<AIMCPClientInstallPanelProps> = ({
|
||||
gap: 6,
|
||||
}}
|
||||
>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap' }}>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
|
||||
<div style={{ fontWeight: 700, fontSize: 13, color: overlayTheme.titleText }}>
|
||||
{selectedStatus?.displayName || '客户端'} 状态
|
||||
第 2 步:确认当前状态并安装
|
||||
</div>
|
||||
{selectedStatus && (
|
||||
<div
|
||||
style={{
|
||||
padding: '3px 9px',
|
||||
borderRadius: 999,
|
||||
fontSize: 11,
|
||||
fontWeight: 700,
|
||||
color: getStatusTone(selectedStatus, darkMode).color,
|
||||
background: getStatusTone(selectedStatus, darkMode).bg,
|
||||
}}
|
||||
>
|
||||
{getStatusTone(selectedStatus, darkMode).label}
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap' }}>
|
||||
<div style={{ fontWeight: 700, fontSize: 13, color: overlayTheme.titleText }}>
|
||||
{selectedStatus?.displayName || '客户端'} 状态
|
||||
</div>
|
||||
)}
|
||||
{selectedStatus && (
|
||||
<div
|
||||
style={{
|
||||
padding: '3px 9px',
|
||||
borderRadius: 999,
|
||||
fontSize: 11,
|
||||
fontWeight: 700,
|
||||
color: getStatusTone(selectedStatus, darkMode).color,
|
||||
background: getStatusTone(selectedStatus, darkMode).bg,
|
||||
}}
|
||||
>
|
||||
{getStatusTone(selectedStatus, darkMode).label}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ fontSize: 12, color: overlayTheme.titleText, lineHeight: 1.7 }}>
|
||||
{getStatusSummary(selectedStatus)}
|
||||
@@ -295,7 +305,7 @@ const AIMCPClientInstallPanel: React.FC<AIMCPClientInstallPanelProps> = ({
|
||||
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', gap: 12, flexWrap: 'wrap' }}>
|
||||
<div style={{ fontSize: 12, color: overlayTheme.mutedText, lineHeight: 1.6 }}>
|
||||
写入后重启对应客户端即可生效;如果已经是当前路径,会直接显示“已接入”,避免重复接入。
|
||||
写入后重启对应客户端即可生效;如果已经是当前路径,按钮会自动禁用,避免重复安装。
|
||||
</div>
|
||||
<Button
|
||||
type={selectedStatus?.matchesCurrent ? 'default' : 'primary'}
|
||||
|
||||
@@ -1,10 +1,42 @@
|
||||
import React from 'react';
|
||||
import { renderToStaticMarkup } from 'react-dom/server';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import AISettingsMCPSection from './AISettingsMCPSection';
|
||||
import { buildOverlayWorkbenchTheme } from '../../utils/overlayWorkbenchTheme';
|
||||
|
||||
const flattenElementText = (node: any): string => {
|
||||
if (node == null || typeof node === 'boolean') {
|
||||
return '';
|
||||
}
|
||||
if (typeof node === 'string' || typeof node === 'number') {
|
||||
return String(node);
|
||||
}
|
||||
if (Array.isArray(node)) {
|
||||
return node.map((item) => flattenElementText(item)).join('');
|
||||
}
|
||||
return flattenElementText(node.props?.children);
|
||||
};
|
||||
|
||||
const findElement = (node: any, predicate: (element: any) => boolean): any => {
|
||||
if (node == null || typeof node === 'boolean' || typeof node === 'string' || typeof node === 'number') {
|
||||
return null;
|
||||
}
|
||||
if (Array.isArray(node)) {
|
||||
for (const item of node) {
|
||||
const match = findElement(item, predicate);
|
||||
if (match) {
|
||||
return match;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
if (predicate(node)) {
|
||||
return node;
|
||||
}
|
||||
return findElement(node.props?.children, predicate);
|
||||
};
|
||||
|
||||
describe('AISettingsMCPSection', () => {
|
||||
it('renders the extracted MCP client installer and server management entry point', () => {
|
||||
const markup = renderToStaticMarkup(
|
||||
@@ -56,8 +88,51 @@ describe('AISettingsMCPSection', () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(markup).toContain('接入外部客户端');
|
||||
expect(markup).toContain('安装到外部 AI 客户端');
|
||||
expect(markup).toContain('常见启动方式模板');
|
||||
expect(markup).toContain('Node 脚本');
|
||||
expect(markup).toContain('新增 MCP 服务');
|
||||
expect(markup).toContain('还没有 MCP 服务');
|
||||
});
|
||||
|
||||
it('seeds a new draft when a launch template is selected', () => {
|
||||
const onAddServer = vi.fn();
|
||||
const tree = AISettingsMCPSection({
|
||||
mcpClientStatuses: [],
|
||||
selectedMCPClient: 'claude-code',
|
||||
selectedMCPClientStatus: undefined,
|
||||
selectedMCPClientCommandText: '',
|
||||
mcpServers: [],
|
||||
mcpTools: [],
|
||||
darkMode: false,
|
||||
overlayTheme: buildOverlayWorkbenchTheme(false),
|
||||
cardBg: '#fff',
|
||||
cardBorder: 'rgba(0,0,0,0.08)',
|
||||
inputBg: '#fff',
|
||||
loading: false,
|
||||
mcpClientStatusLoading: false,
|
||||
onSelectClient: () => {},
|
||||
onRefreshStatus: () => {},
|
||||
onCopyConfigPath: () => {},
|
||||
onCopyLaunchCommand: () => {},
|
||||
onInstallSelectedClient: () => {},
|
||||
onAddServer,
|
||||
onUpdateServerDraft: () => {},
|
||||
onTestServer: () => {},
|
||||
onSaveServer: () => {},
|
||||
onDeleteServer: () => {},
|
||||
});
|
||||
|
||||
const nodeTemplateButton = findElement(
|
||||
tree,
|
||||
(node) => node.type === 'button' && flattenElementText(node.props?.children).includes('Node 脚本'),
|
||||
);
|
||||
expect(nodeTemplateButton).toBeTruthy();
|
||||
nodeTemplateButton.props.onClick();
|
||||
|
||||
expect(onAddServer).toHaveBeenCalledWith(expect.objectContaining({
|
||||
command: 'node',
|
||||
args: ['server.js', '--stdio'],
|
||||
}));
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,6 +3,7 @@ import { Button } from 'antd';
|
||||
import { PlusOutlined } from '@ant-design/icons';
|
||||
|
||||
import type { AIMCPClientInstallStatus, AIMCPServerConfig, AIMCPToolDescriptor } from '../../types';
|
||||
import { MCP_SERVER_DRAFT_TEMPLATES } from '../../utils/mcpServerTemplates';
|
||||
import type { OverlayWorkbenchTheme } from '../../utils/overlayWorkbenchTheme';
|
||||
import AIMCPClientInstallPanel from './AIMCPClientInstallPanel';
|
||||
import AIMCPServerCard from './AIMCPServerCard';
|
||||
@@ -28,7 +29,7 @@ interface AISettingsMCPSectionProps {
|
||||
onCopyConfigPath: () => void;
|
||||
onCopyLaunchCommand: () => void;
|
||||
onInstallSelectedClient: () => void;
|
||||
onAddServer: () => void;
|
||||
onAddServer: (seed?: Partial<AIMCPServerConfig>) => void;
|
||||
onUpdateServerDraft: (id: string, patch: Partial<AIMCPServerConfig>) => void;
|
||||
onTestServer: (server: AIMCPServerConfig) => void;
|
||||
onSaveServer: (server: AIMCPServerConfig) => void;
|
||||
@@ -78,9 +79,47 @@ const AISettingsMCPSection: React.FC<AISettingsMCPSectionProps> = ({
|
||||
onCopyLaunchCommand={onCopyLaunchCommand}
|
||||
onInstall={onInstallSelectedClient}
|
||||
/>
|
||||
<div
|
||||
style={{
|
||||
padding: '14px 16px',
|
||||
borderRadius: 14,
|
||||
border: `1px solid ${cardBorder}`,
|
||||
background: cardBg,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: 10,
|
||||
}}
|
||||
>
|
||||
<div style={{ fontWeight: 700, fontSize: 14, color: overlayTheme.titleText }}>常见启动方式模板</div>
|
||||
<div style={{ fontSize: 12, color: overlayTheme.mutedText, lineHeight: 1.7 }}>
|
||||
不确定命令和参数怎么拆时,先选一个最接近的启动方式。GoNavi 会自动带入示例值,你再改成自己的脚本名、模块名或 exe 路径即可。
|
||||
</div>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(180px, 1fr))', gap: 10 }}>
|
||||
{MCP_SERVER_DRAFT_TEMPLATES.map((template) => (
|
||||
<button
|
||||
key={template.key}
|
||||
type="button"
|
||||
onClick={() => onAddServer(template.seed)}
|
||||
style={{
|
||||
textAlign: 'left',
|
||||
padding: '12px 13px',
|
||||
borderRadius: 12,
|
||||
border: `1px solid ${cardBorder}`,
|
||||
background: darkMode ? 'rgba(255,255,255,0.03)' : 'rgba(255,255,255,0.72)',
|
||||
color: overlayTheme.titleText,
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
>
|
||||
<div style={{ fontSize: 13, fontWeight: 700 }}>{template.title}</div>
|
||||
<div style={{ marginTop: 4, fontSize: 12, color: overlayTheme.mutedText, lineHeight: 1.6 }}>{template.description}</div>
|
||||
<div style={{ marginTop: 6, fontSize: 12, color: overlayTheme.mutedText, lineHeight: 1.6 }}>{template.detail}</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', gap: 12 }}>
|
||||
<div style={{ fontSize: 12, color: overlayTheme.mutedText }}>支持命令、参数、环境变量和超时,保存后会自动进入 AI 工具列表。</div>
|
||||
<Button icon={<PlusOutlined />} onClick={onAddServer} style={{ borderRadius: 10 }}>新增 MCP 服务</Button>
|
||||
<Button icon={<PlusOutlined />} onClick={() => onAddServer()} style={{ borderRadius: 10 }}>新增 MCP 服务</Button>
|
||||
</div>
|
||||
{mcpServers.length === 0 && (
|
||||
<div style={{ padding: '18px 16px', borderRadius: 14, border: `1px dashed ${cardBorder}`, background: cardBg, color: overlayTheme.mutedText }}>
|
||||
|
||||
30
frontend/src/components/ai/AISettingsSkillsSection.test.tsx
Normal file
30
frontend/src/components/ai/AISettingsSkillsSection.test.tsx
Normal file
@@ -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(
|
||||
<AISettingsSkillsSection
|
||||
skills={[]}
|
||||
skillRequiredToolOptions={[]}
|
||||
overlayTheme={buildOverlayWorkbenchTheme(false)}
|
||||
cardBg="#fff"
|
||||
cardBorder="rgba(0,0,0,0.08)"
|
||||
inputBg="#fff"
|
||||
loading={false}
|
||||
onAddSkill={() => {}}
|
||||
onUpdateSkillDraft={() => {}}
|
||||
onSaveSkill={() => {}}
|
||||
onDeleteSkill={() => {}}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(markup).toContain('新增 Skill');
|
||||
expect(markup).toContain('还没有 Skill');
|
||||
expect(markup).toContain('命名的提示模块');
|
||||
});
|
||||
});
|
||||
110
frontend/src/components/ai/AISettingsSkillsSection.tsx
Normal file
110
frontend/src/components/ai/AISettingsSkillsSection.tsx
Normal file
@@ -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<AISkillConfig>) => 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<AISettingsSkillsSectionProps> = ({
|
||||
skills,
|
||||
skillRequiredToolOptions,
|
||||
overlayTheme,
|
||||
cardBg,
|
||||
cardBorder,
|
||||
inputBg,
|
||||
loading,
|
||||
onAddSkill,
|
||||
onUpdateSkillDraft,
|
||||
onSaveSkill,
|
||||
onDeleteSkill,
|
||||
}) => (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
|
||||
<div style={{ fontSize: 13, color: overlayTheme.mutedText, marginBottom: 4 }}>
|
||||
Skill 不是另一条大提示词,而是“命名的提示模块 + 作用域 + 工具依赖”。当前阶段仍建议保留在主仓库内,不需要单独新建 GitHub 仓库;只有未来要做共享 skill pack 分发时,再考虑拆仓。
|
||||
</div>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', gap: 12 }}>
|
||||
<div style={{ fontSize: 12, color: overlayTheme.mutedText }}>启用后会按 scope 注入对应会话;如果依赖的工具不存在,该 Skill 会被自动跳过。</div>
|
||||
<Button icon={<PlusOutlined />} onClick={onAddSkill} style={{ borderRadius: 10 }}>新增 Skill</Button>
|
||||
</div>
|
||||
{skills.length === 0 && (
|
||||
<div style={{ padding: '18px 16px', borderRadius: 14, border: `1px dashed ${cardBorder}`, background: cardBg, color: overlayTheme.mutedText }}>
|
||||
还没有 Skill。你可以给数据库、JVM、诊断场景分别定义专用的 system prompt。
|
||||
</div>
|
||||
)}
|
||||
{skills.map((skill) => (
|
||||
<div key={skill.id} style={{ padding: '14px 16px', borderRadius: 14, border: `1px solid ${cardBorder}`, background: cardBg, display: 'flex', flexDirection: 'column', gap: 12 }}>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'minmax(0,1fr) 132px', gap: 12 }}>
|
||||
<Input
|
||||
value={skill.name}
|
||||
onChange={(event) => onUpdateSkillDraft(skill.id, { name: event.target.value })}
|
||||
placeholder="Skill 名称,例如:SQL 审查 / JVM 诊断计划"
|
||||
style={{ borderRadius: 10, background: inputBg, border: `1px solid ${cardBorder}` }}
|
||||
/>
|
||||
<Select
|
||||
value={skill.enabled ? 'enabled' : 'disabled'}
|
||||
onChange={(value) => onUpdateSkillDraft(skill.id, { enabled: value === 'enabled' })}
|
||||
options={[{ label: '已启用', value: 'enabled' }, { label: '已禁用', value: 'disabled' }]}
|
||||
/>
|
||||
</div>
|
||||
<Input
|
||||
value={skill.description || ''}
|
||||
onChange={(event) => onUpdateSkillDraft(skill.id, { description: event.target.value })}
|
||||
placeholder="给自己看的说明,例如:输出 SQL 前必须先确认字段名和风险"
|
||||
style={{ borderRadius: 10, background: inputBg, border: `1px solid ${cardBorder}` }}
|
||||
/>
|
||||
<Select
|
||||
mode="multiple"
|
||||
value={skill.scopes || []}
|
||||
onChange={(value) => onUpdateSkillDraft(skill.id, { scopes: value as AISkillScope[] })}
|
||||
options={SKILL_SCOPE_OPTIONS.map((option) => ({ label: `${option.label} · ${option.desc}`, value: option.value }))}
|
||||
placeholder="选择这个 Skill 要作用到哪些场景"
|
||||
style={{ width: '100%' }}
|
||||
/>
|
||||
<Select
|
||||
mode="multiple"
|
||||
value={skill.requiredTools || []}
|
||||
onChange={(value) => onUpdateSkillDraft(skill.id, { requiredTools: value })}
|
||||
options={skillRequiredToolOptions}
|
||||
placeholder="可选:声明这个 Skill 依赖哪些工具"
|
||||
style={{ width: '100%' }}
|
||||
/>
|
||||
<Input.TextArea
|
||||
rows={6}
|
||||
value={skill.systemPrompt}
|
||||
onChange={(event) => 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' }}
|
||||
/>
|
||||
<div style={{ display: 'flex', justifyContent: 'flex-end', gap: 8 }}>
|
||||
<Button type="primary" onClick={() => onSaveSkill(skill)} loading={loading} style={{ borderRadius: 10, fontWeight: 600 }}>保存</Button>
|
||||
<Popconfirm title="删除这个 Skill?" okText="删除" cancelText="取消" onConfirm={() => onDeleteSkill(skill.id)}>
|
||||
<Button danger icon={<DeleteOutlined />} style={{ borderRadius: 10 }}>删除</Button>
|
||||
</Popconfirm>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
|
||||
export default AISettingsSkillsSection;
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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: "▶️",
|
||||
|
||||
64
frontend/src/utils/mcpServerTemplates.ts
Normal file
64
frontend/src/utils/mcpServerTemplates.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
import type { AIMCPServerConfig } from '../types';
|
||||
|
||||
export interface MCPServerDraftTemplate {
|
||||
key: string;
|
||||
title: string;
|
||||
description: string;
|
||||
detail: string;
|
||||
seed: Partial<AIMCPServerConfig>;
|
||||
}
|
||||
|
||||
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,
|
||||
},
|
||||
},
|
||||
];
|
||||
Reference in New Issue
Block a user