feat: 重构MCP功能和AI服务提供者架构

This commit is contained in:
xiamuceer-j
2026-01-09 17:13:19 +08:00
parent f3c224261d
commit 77c5489ff8
49 changed files with 4763 additions and 4307 deletions
+422 -77
View File
@@ -28,8 +28,11 @@ import {
InfoCircleOutlined,
ToolOutlined,
ArrowLeftOutlined,
ApiOutlined,
QuestionCircleOutlined,
WarningOutlined,
} from '@ant-design/icons';
import { mcpPluginApi } from '../services/api';
import { mcpPluginApi, settingsApi } from '../services/api';
import type { MCPPlugin, MCPTool } from '../types';
const { Paragraph, Text, Title } = Typography;
@@ -46,24 +49,112 @@ export default function MCPPluginsPage() {
const [editingPlugin, setEditingPlugin] = useState<MCPPlugin | null>(null);
const [testingPluginId, setTestingPluginId] = useState<string | null>(null);
const [viewingTools, setViewingTools] = useState<{ pluginId: string; tools: MCPTool[] } | null>(null);
const [checkingFunctionCalling, setCheckingFunctionCalling] = useState(false);
const [modelSupportStatus, setModelSupportStatus] = useState<'unknown' | 'supported' | 'unsupported'>('unknown');
useEffect(() => {
loadPlugins();
}, []);
const initPage = async () => {
setLoading(true);
try {
// 1. 并行获取插件列表和当前设置
const [pluginsData, settings] = await Promise.all([
mcpPluginApi.getPlugins(),
settingsApi.getSettings()
]);
setPlugins(pluginsData);
// 2. 检查配置一致性
const verifiedConfigStr = localStorage.getItem('mcp_verified_config');
if (verifiedConfigStr) {
try {
const verifiedConfig = JSON.parse(verifiedConfigStr);
const currentConfig = {
provider: settings.api_provider,
baseUrl: settings.api_base_url,
model: settings.llm_model
};
// 比较关键配置是否发生变更
const isConfigChanged =
verifiedConfig.provider !== currentConfig.provider ||
verifiedConfig.baseUrl !== currentConfig.baseUrl ||
verifiedConfig.model !== currentConfig.model;
if (isConfigChanged) {
// 配置已变更
setModelSupportStatus('unknown');
// 检查是否有正在运行的插件
const activePlugins = pluginsData.filter(p => p.enabled);
if (activePlugins.length > 0) {
// 自动禁用所有插件
message.loading({ content: '检测到模型配置变更,正在为了安全自动禁用插件...', key: 'auto_disable' });
await Promise.all(activePlugins.map(p => mcpPluginApi.togglePlugin(p.id, false)));
// 重新加载插件列表状态
const updatedPlugins = await mcpPluginApi.getPlugins();
setPlugins(updatedPlugins);
message.success({ content: '已自动禁用所有插件,请重新检测模型能力', key: 'auto_disable' });
modal.warning({
title: '配置变更提醒',
centered: true,
content: '检测到您更换了 AI 模型或接口地址。为了防止错误调用,系统已自动暂停所有 MCP 插件。请重新进行"模型能力检查",确认新模型支持 Function Calling 后再启用插件。',
okText: '知道了',
});
} else {
// 没有运行中的插件,仅提示
message.info('检测到模型配置已变更,请重新检测模型能力');
}
// 清除旧的验证状态
localStorage.removeItem('mcp_verified_config');
} else {
// 配置未变更,恢复验证状态(根据缓存的状态恢复)
const cachedStatus = verifiedConfig.status || 'supported';
setModelSupportStatus(cachedStatus as 'unknown' | 'supported' | 'unsupported');
}
} catch (e) {
console.error('Failed to parse verified config:', e);
localStorage.removeItem('mcp_verified_config');
}
}
} catch (error) {
console.error('Init page failed:', error);
message.error('页面初始化失败');
} finally {
setLoading(false);
}
};
initPage();
}, [modal]);
const loadPlugins = async () => {
setLoading(true);
try {
const data = await mcpPluginApi.getPlugins();
setPlugins(data);
} catch (error) {
console.error('Load plugins failed:', error);
message.error('加载插件列表失败');
} finally {
setLoading(false);
}
};
const handleCreate = () => {
if (modelSupportStatus !== 'supported') {
modal.confirm({
title: '模型能力检查',
centered: true,
icon: <WarningOutlined />,
content: '为了确保 MCP 插件正常工作,您当前使用的 AI 模型必须支持 Function Calling(工具调用)能力。请先进行模型支持检测。',
okText: '去检测',
cancelText: '取消',
onOk: handleCheckFunctionCalling,
});
return;
}
setEditingPlugin(null);
form.resetFields();
form.setFieldsValue({
@@ -86,7 +177,7 @@ export default function MCPPluginsPage() {
setEditingPlugin(plugin);
// 重构为标准MCP配置格式
const mcpConfig: any = {
const mcpConfig: Record<string, Record<string, Record<string, unknown>>> = {
mcpServers: {
[plugin.plugin_name]: {
type: plugin.plugin_type || 'http'
@@ -94,7 +185,7 @@ export default function MCPPluginsPage() {
}
};
if (plugin.plugin_type === 'http') {
if (plugin.plugin_type === 'http' || plugin.plugin_type === 'streamable_http' || plugin.plugin_type === 'sse') {
mcpConfig.mcpServers[plugin.plugin_name].url = plugin.server_url;
mcpConfig.mcpServers[plugin.plugin_name].headers = plugin.headers || {};
} else {
@@ -125,6 +216,7 @@ export default function MCPPluginsPage() {
message.success('插件已删除');
loadPlugins();
} catch (error) {
console.error('Delete plugin failed:', error);
message.error('删除插件失败');
}
},
@@ -137,6 +229,7 @@ export default function MCPPluginsPage() {
message.success(enabled ? '插件已启用' : '插件已禁用');
loadPlugins();
} catch (error) {
console.error('Toggle plugin failed:', error);
message.error('切换插件状态失败');
}
};
@@ -150,45 +243,62 @@ export default function MCPPluginsPage() {
await loadPlugins();
if (result.success) {
const suggestions = result.suggestions || [];
const aiChoice = suggestions.find((s: string) => s.startsWith('🤖'))?.replace('🤖 AI选择: ', '') || '';
const paramsStr = suggestions.find((s: string) => s.startsWith('📝'))?.replace('📝 参数: ', '') || '';
const callTime = suggestions.find((s: string) => s.startsWith('⏱️'))?.replace('⏱️ 耗时: ', '') || '';
const resultStr = suggestions.find((s: string) => s.startsWith('📊'))?.replace('📊 结果:\n', '') || '';
modal.success({
title: '测试成功',
title: '🎉 测试成功',
centered: true,
width: isMobile ? '90%' : 600,
width: isMobile ? '95%' : 700,
content: (
<div style={{ padding: '8px 0' }}>
<div style={{ marginBottom: 24, padding: 16, background: 'var(--color-success-bg)', border: '1px solid var(--color-success-border)', borderRadius: 8 }}>
<Typography.Text strong style={{ color: 'var(--color-success)' }}>
<div style={{ marginBottom: 16, padding: 12, background: 'var(--color-success-bg)', border: '1px solid var(--color-success-border)', borderRadius: 8 }}>
<Typography.Text strong style={{ color: 'var(--color-success)', fontSize: 14 }}>
{result.message}
</Typography.Text>
</div>
{(result.tools_count !== undefined || result.response_time_ms !== undefined) && (
<div style={{
padding: 16,
background: 'var(--color-bg-layout)',
borderRadius: 8,
marginBottom: 16
}}>
{result.tools_count !== undefined && (
<div style={{ marginBottom: 8, fontSize: 14 }}>
<Text type="secondary"></Text>
<Text strong>{result.tools_count}</Text>
</div>
)}
{result.response_time_ms !== undefined && (
<div style={{ fontSize: 14 }}>
<Text type="secondary"></Text>
<Text strong>{result.response_time_ms}ms</Text>
</div>
)}
<div style={{ display: 'grid', gridTemplateColumns: isMobile ? '1fr' : '1fr 1fr', gap: 12, marginBottom: 16 }}>
<div style={{ padding: 12, background: 'var(--color-bg-layout)', borderRadius: 8 }}>
<Text type="secondary" style={{ fontSize: 12 }}></Text>
<div><Text strong style={{ fontSize: 20 }}>{result.tools_count || 0}</Text></div>
</div>
<div style={{ padding: 12, background: 'var(--color-bg-layout)', borderRadius: 8 }}>
<Text type="secondary" style={{ fontSize: 12 }}></Text>
<div><Text strong style={{ fontSize: 20 }}>{result.response_time_ms?.toFixed(0) || 0}ms</Text></div>
</div>
</div>
{aiChoice && (
<div style={{ marginBottom: 12, padding: 12, background: 'var(--color-info-bg)', borderRadius: 8, border: '1px solid var(--color-info-border)' }}>
<Text type="secondary" style={{ fontSize: 12, display: 'block', marginBottom: 4 }}>🤖 AI选择的工具</Text>
<Text code strong>{aiChoice}</Text>
{callTime && <Tag color="blue" style={{ marginLeft: 8 }}>{callTime}</Tag>}
</div>
)}
<Alert
message='插件状态已自动更新为"运行中"'
type="success"
showIcon
/>
{paramsStr && (
<div style={{ marginBottom: 12 }}>
<Text type="secondary" style={{ fontSize: 12, display: 'block', marginBottom: 4 }}>📝 </Text>
<pre style={{ margin: 0, padding: 8, background: 'var(--color-bg-layout)', borderRadius: 4, fontSize: 12, overflow: 'auto', maxHeight: 100 }}>
{(() => { try { return JSON.stringify(JSON.parse(paramsStr), null, 2); } catch { return paramsStr; } })()}
</pre>
</div>
)}
{resultStr && (
<div style={{ marginBottom: 12 }}>
<Text type="secondary" style={{ fontSize: 12, display: 'block', marginBottom: 4 }}>📊 </Text>
<pre style={{ margin: 0, padding: 8, background: 'var(--color-bg-layout)', borderRadius: 4, fontSize: 11, overflow: 'auto', maxHeight: 150, whiteSpace: 'pre-wrap', wordBreak: 'break-word' }}>
{resultStr}
</pre>
</div>
)}
<Alert message='插件状态已自动更新为"运行中"' type="success" showIcon />
</div>
),
});
@@ -248,7 +358,7 @@ export default function MCPPluginsPage() {
),
});
}
} catch (error: any) {
} catch {
message.error('测试插件失败');
} finally {
setTestingPluginId(null);
@@ -260,17 +370,181 @@ export default function MCPPluginsPage() {
const result = await mcpPluginApi.getPluginTools(pluginId);
setViewingTools({ pluginId, tools: result.tools });
} catch (error) {
console.error('Get tools failed:', error);
message.error('获取工具列表失败');
}
};
const handleSubmit = async (values: any) => {
const handleCheckFunctionCalling = async () => {
// 从设置中获取当前配置
setCheckingFunctionCalling(true);
try {
const settings = await settingsApi.getSettings();
if (!settings.api_key || !settings.llm_model) {
message.warning('请先在设置页面配置 API Key 和模型');
return;
}
const result = await settingsApi.checkFunctionCalling({
api_key: settings.api_key,
api_base_url: settings.api_base_url || '',
provider: settings.api_provider || 'openai',
llm_model: settings.llm_model,
});
// 无论成功失败,都缓存当前测试的配置和状态
const configToCache = {
provider: settings.api_provider,
baseUrl: settings.api_base_url,
model: settings.llm_model,
status: result.success && result.supported ? 'supported' : 'unsupported',
testedAt: new Date().toISOString()
};
localStorage.setItem('mcp_verified_config', JSON.stringify(configToCache));
if (result.success && result.supported) {
setModelSupportStatus('supported');
modal.success({
title: '✅ Function Calling 支持检测',
centered: true,
width: isMobile ? '95%' : 700,
content: (
<div style={{ padding: '8px 0' }}>
<div style={{ marginBottom: 16, padding: 12, background: 'var(--color-success-bg)', border: '1px solid var(--color-success-border)', borderRadius: 8 }}>
<Typography.Text strong style={{ color: 'var(--color-success)', fontSize: 14 }}>
{result.message}
</Typography.Text>
</div>
<div style={{ display: 'grid', gridTemplateColumns: isMobile ? '1fr' : '1fr 1fr', gap: 12, marginBottom: 16 }}>
<div style={{ padding: 12, background: 'var(--color-bg-layout)', borderRadius: 8 }}>
<Text type="secondary" style={{ fontSize: 12 }}>API </Text>
<div><Text strong style={{ fontSize: 16 }}>{result.provider}</Text></div>
</div>
<div style={{ padding: 12, background: 'var(--color-bg-layout)', borderRadius: 8 }}>
<Text type="secondary" style={{ fontSize: 12 }}></Text>
<div><Text strong style={{ fontSize: 16 }}>{result.response_time_ms?.toFixed(0) || 0}ms</Text></div>
</div>
</div>
<div style={{ marginBottom: 12, padding: 12, background: 'var(--color-info-bg)', borderRadius: 8, border: '1px solid var(--color-info-border)' }}>
<Text type="secondary" style={{ fontSize: 12, display: 'block', marginBottom: 4 }}>🔧 </Text>
<Text code strong>{result.model}</Text>
{result.details?.finish_reason && (
<Tag color="green" style={{ marginLeft: 8 }}>finish_reason: {result.details.finish_reason}</Tag>
)}
</div>
{result.details && (
<div style={{ marginBottom: 12 }}>
<Text type="secondary" style={{ fontSize: 12, display: 'block', marginBottom: 4 }}>📊 </Text>
<div style={{ padding: 8, background: 'var(--color-bg-layout)', borderRadius: 4, fontSize: 12 }}>
<div> : {result.details.tool_call_count || 0}</div>
<div> : {result.details.test_tool || 'N/A'}</div>
<div> : {result.details.response_type || 'N/A'}</div>
</div>
</div>
)}
{result.tool_calls && result.tool_calls.length > 0 && (
<div style={{ marginBottom: 12 }}>
<Text type="secondary" style={{ fontSize: 12, display: 'block', marginBottom: 4 }}>🔨 </Text>
<pre style={{ margin: 0, padding: 8, background: 'var(--color-bg-layout)', borderRadius: 4, fontSize: 11, overflow: 'auto', maxHeight: 150 }}>
{JSON.stringify(result.tool_calls[0], null, 2)}
</pre>
</div>
)}
{result.suggestions && result.suggestions.length > 0 && (
<div style={{ padding: 12, background: 'var(--color-success-bg)', border: '1px solid var(--color-success-border)', borderRadius: 8 }}>
<Text strong style={{ fontSize: 13, display: 'block', marginBottom: 8 }}>💡 </Text>
<ul style={{ margin: 0, paddingLeft: 20, fontSize: 12 }}>
{result.suggestions.map((s: string, i: number) => (
<li key={i} style={{ marginBottom: 4 }}>{s}</li>
))}
</ul>
</div>
)}
</div>
),
});
} else {
setModelSupportStatus('unsupported');
modal.warning({
title: '❌ Function Calling 支持检测',
centered: true,
width: isMobile ? '95%' : 700,
content: (
<div style={{ padding: '8px 0' }}>
<div style={{ marginBottom: 16 }}>
<Alert
message={result.message || '模型不支持 Function Calling'}
type="warning"
showIcon
/>
</div>
{result.error && (
<div style={{
padding: 16,
background: 'var(--color-warning-bg)',
border: '1px solid var(--color-warning-border)',
borderRadius: 8,
marginBottom: 16
}}>
<Text strong style={{ fontSize: 14, display: 'block', marginBottom: 8 }}>:</Text>
<Text style={{ fontSize: 13, fontFamily: 'monospace' }}>
{result.error}
</Text>
</div>
)}
{result.response_preview && (
<div style={{ marginBottom: 12 }}>
<Text type="secondary" style={{ fontSize: 12, display: 'block', marginBottom: 4 }}>📝 200</Text>
<pre style={{ margin: 0, padding: 8, background: 'var(--color-bg-layout)', borderRadius: 4, fontSize: 11, overflow: 'auto', maxHeight: 100, whiteSpace: 'pre-wrap' }}>
{result.response_preview}
</pre>
</div>
)}
{result.suggestions && result.suggestions.length > 0 && (
<div style={{
padding: 16,
background: 'var(--color-info-bg)',
border: '1px solid var(--color-info-border)',
borderRadius: 8
}}>
<Text strong style={{ fontSize: 14, display: 'block', marginBottom: 8 }}>💡 :</Text>
<ul style={{ margin: 0, paddingLeft: 20, fontSize: 13 }}>
{result.suggestions.map((s: string, i: number) => (
<li key={i} style={{ marginBottom: 4 }}>{s}</li>
))}
</ul>
</div>
)}
</div>
),
});
}
} catch (error) {
console.error('Check function calling failed:', error);
message.error('检测失败,请稍后重试');
setModelSupportStatus('unsupported');
} finally {
setCheckingFunctionCalling(false);
}
};
const handleSubmit = async (values: { config_json: string; enabled: boolean; category?: string }) => {
setLoading(true);
try {
// 验证JSON格式
try {
JSON.parse(values.config_json);
} catch (e) {
} catch {
message.error('配置JSON格式错误,请检查');
setLoading(false);
return;
@@ -289,8 +563,9 @@ export default function MCPPluginsPage() {
setModalVisible(false);
form.resetFields();
loadPlugins();
} catch (error: any) {
const errorMsg = error?.response?.data?.detail || '操作失败';
} catch (error: unknown) {
const err = error as { response?: { data?: { detail?: string } } };
const errorMsg = err?.response?.data?.detail || '操作失败';
message.error(errorMsg);
} finally {
setLoading(false);
@@ -407,38 +682,104 @@ export default function MCPPluginsPage() {
</Col>
</Row>
{/* 使用提示 */}
<Alert
message={
<Space align="center">
<InfoCircleOutlined style={{ fontSize: 16, color: 'var(--color-primary)' }} />
<Text strong style={{ fontSize: isMobile ? 13 : 14, color: 'var(--color-text-primary)' }}> MCP </Text>
</Space>
}
description={
<div>
<Text style={{ fontSize: isMobile ? 12 : 13, display: 'block', marginBottom: 8 }}>
<strong>MCP (Model Context Protocol)</strong> AI
</Text>
<Text style={{ fontSize: isMobile ? 12 : 13, display: 'block' }}>
MCP AI 访API
</Text>
<div style={{ marginTop: isMobile ? 16 : 24, display: 'flex', gap: 16, flexDirection: isMobile ? 'column' : 'row' }}>
<Card
variant="borderless"
style={{
flex: 1,
borderRadius: 12,
background: 'rgba(255, 255, 255, 0.9)',
border: '1px solid rgba(255, 255, 255, 0.6)',
backdropFilter: 'blur(10px)',
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.03)'
}}
bodyStyle={{ padding: 20 }}
>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<Space align="start">
<div style={{
width: 40, height: 40, borderRadius: '50%',
background: modelSupportStatus === 'supported' ? 'var(--color-success-bg)' : modelSupportStatus === 'unsupported' ? 'var(--color-error-bg)' : 'var(--color-info-bg)',
display: 'flex', alignItems: 'center', justifyContent: 'center',
border: `1px solid ${modelSupportStatus === 'supported' ? 'var(--color-success-border)' : modelSupportStatus === 'unsupported' ? 'var(--color-error-border)' : 'var(--color-info-border)'}`
}}>
{modelSupportStatus === 'supported' ? (
<CheckCircleOutlined style={{ fontSize: 20, color: 'var(--color-success)' }} />
) : modelSupportStatus === 'unsupported' ? (
<CloseCircleOutlined style={{ fontSize: 20, color: 'var(--color-error)' }} />
) : (
<QuestionCircleOutlined style={{ fontSize: 20, color: 'var(--color-info)' }} />
)}
</div>
<div>
<Text strong style={{ fontSize: 16, display: 'block', color: 'var(--color-text-primary)' }}></Text>
<Text type="secondary" style={{ fontSize: 13 }}>
{modelSupportStatus === 'supported'
? '当前模型支持 Function Calling,可正常使用 MCP 插件'
: modelSupportStatus === 'unsupported'
? '当前模型不支持 Function Calling,无法使用 MCP 插件'
: '请先检测模型是否支持 Function Calling 能力'}
</Text>
</div>
</Space>
<Button
type={modelSupportStatus === 'supported' ? 'default' : 'primary'}
icon={<ApiOutlined />}
onClick={handleCheckFunctionCalling}
loading={checkingFunctionCalling}
style={{ borderRadius: 8 }}
>
{modelSupportStatus === 'unknown' ? '开始检测' : '重新检测'}
</Button>
</div>
}
type="info"
showIcon={false}
style={{
marginTop: isMobile ? 16 : 24,
borderRadius: 12,
background: 'rgba(230, 247, 255, 0.6)',
border: '1px solid rgba(145, 213, 255, 0.6)',
backdropFilter: 'blur(5px)'
}}
/>
</Card>
<Card
variant="borderless"
style={{
flex: 1,
borderRadius: 12,
background: 'rgba(230, 247, 255, 0.6)',
border: '1px solid rgba(145, 213, 255, 0.6)',
backdropFilter: 'blur(10px)',
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.03)'
}}
bodyStyle={{ padding: 20 }}
>
<Space align="start">
<InfoCircleOutlined style={{ fontSize: 20, color: 'var(--color-primary)', marginTop: 4 }} />
<div>
<Text strong style={{ fontSize: 16, display: 'block', color: 'var(--color-text-primary)', marginBottom: 4 }}> MCP </Text>
<Text style={{ fontSize: 13, display: 'block', color: 'var(--color-text-secondary)', lineHeight: 1.6 }}>
MCP (Model Context Protocol) AI AI 访API
</Text>
</div>
</Space>
</Card>
</div>
</Card>
{/* 主内容区 */}
<div style={{ flex: 1 }}>
{/* 模型能力未验证时的警告提示 */}
{modelSupportStatus !== 'supported' && plugins.length > 0 && (
<Alert
message={
modelSupportStatus === 'unsupported'
? '当前模型不支持 Function Calling,所有插件操作已禁用'
: '请先完成模型能力检查,才能操作插件'
}
type={modelSupportStatus === 'unsupported' ? 'error' : 'warning'}
showIcon
icon={modelSupportStatus === 'unsupported' ? <CloseCircleOutlined /> : <WarningOutlined />}
style={{ marginBottom: 16, borderRadius: 8 }}
action={
<Button size="small" type="primary" onClick={handleCheckFunctionCalling} loading={checkingFunctionCalling}>
{modelSupportStatus === 'unknown' ? '开始检测' : '重新检测'}
</Button>
}
/>
)}
{/* 插件列表 */}
<Spin spinning={loading}>
@@ -479,7 +820,7 @@ export default function MCPPluginsPage() {
{plugin.display_name || plugin.plugin_name}
</Text>
{getStatusTag(plugin)}
<Tag color={plugin.plugin_type === 'http' ? 'blue' : 'cyan'}>
<Tag color={plugin.plugin_type === 'http' || plugin.plugin_type === 'streamable_http' || plugin.plugin_type === 'sse' ? 'blue' : 'cyan'}>
{plugin.plugin_type?.toUpperCase() || 'UNKNOWN'}
</Tag>
{plugin.category && plugin.category !== 'general' && (
@@ -500,7 +841,7 @@ export default function MCPPluginsPage() {
)}
{/* 只显示有值的URL或命令,脱敏处理敏感信息 */}
{plugin.plugin_type === 'http' && plugin.server_url && (
{(plugin.plugin_type === 'http' || plugin.plugin_type === 'streamable_http' || plugin.plugin_type === 'sse') && plugin.server_url && (
<div style={{ fontSize: isMobile ? '11px' : '12px' }}>
<Text type="secondary" code>
{(() => {
@@ -551,9 +892,10 @@ export default function MCPPluginsPage() {
<Space size="small" wrap>
<Switch
title={plugin.enabled ? '禁用插件' : '启用插件'}
title={modelSupportStatus !== 'supported' ? '请先完成模型能力检查' : (plugin.enabled ? '禁用插件' : '启用插件')}
checked={plugin.enabled}
onChange={(checked) => handleToggle(plugin, checked)}
disabled={modelSupportStatus !== 'supported'}
size={isMobile ? 'small' : 'default'}
style={{
flexShrink: 0,
@@ -563,30 +905,33 @@ export default function MCPPluginsPage() {
}}
/>
<Button
title="测试连接"
title={modelSupportStatus !== 'supported' ? '请先完成模型能力检查' : '测试连接'}
icon={<ThunderboltOutlined />}
onClick={() => handleTest(plugin.id)}
loading={testingPluginId === plugin.id}
disabled={modelSupportStatus !== 'supported'}
size={isMobile ? 'small' : 'middle'}
/>
<Button
title="查看工具"
title={modelSupportStatus !== 'supported' ? '请先完成模型能力检查' : '查看工具'}
icon={<ToolOutlined />}
onClick={() => handleViewTools(plugin.id)}
disabled={!plugin.enabled || plugin.status !== 'active'}
disabled={modelSupportStatus !== 'supported' || !plugin.enabled || plugin.status !== 'active'}
size={isMobile ? 'small' : 'middle'}
/>
<Button
title="编辑"
title={modelSupportStatus !== 'supported' ? '请先完成模型能力检查' : '编辑'}
icon={<EditOutlined />}
onClick={() => handleEdit(plugin)}
disabled={modelSupportStatus !== 'supported'}
size={isMobile ? 'small' : 'middle'}
/>
<Button
title="删除"
title={modelSupportStatus !== 'supported' ? '请先完成模型能力检查' : '删除'}
danger
icon={<DeleteOutlined />}
onClick={() => handleDelete(plugin)}
disabled={modelSupportStatus !== 'supported'}
size={isMobile ? 'small' : 'middle'}
/>
</Space>
@@ -627,7 +972,7 @@ export default function MCPPluginsPage() {
{
"mcpServers": {
"exa": {
"type": "http",
"type": "streamable_http",
"url": "https://mcp.exa.ai/mcp?exaApiKey=YOUR_API_KEY",
"headers": {}
}
+162 -2
View File
@@ -1,8 +1,8 @@
import { useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { Card, Form, Input, Button, Select, Slider, InputNumber, message, Space, Typography, Spin, Modal, Alert, Grid, Tabs, List, Tag, Popconfirm, Empty, Row, Col } from 'antd';
import { SettingOutlined, SaveOutlined, DeleteOutlined, ReloadOutlined, ArrowLeftOutlined, InfoCircleOutlined, CheckCircleOutlined, CloseCircleOutlined, ThunderboltOutlined, PlusOutlined, EditOutlined, CopyOutlined } from '@ant-design/icons';
import { settingsApi } from '../services/api';
import { SettingOutlined, SaveOutlined, DeleteOutlined, ReloadOutlined, ArrowLeftOutlined, InfoCircleOutlined, CheckCircleOutlined, CloseCircleOutlined, ThunderboltOutlined, PlusOutlined, EditOutlined, CopyOutlined, WarningOutlined } from '@ant-design/icons';
import { settingsApi, mcpPluginApi } from '../services/api';
import type { SettingsUpdate, APIKeyPreset, PresetCreateRequest, APIKeyPresetConfig } from '../types';
const { Title, Text } = Typography;
@@ -95,10 +95,86 @@ export default function SettingsPage() {
const handleSave = async (values: SettingsUpdate) => {
setLoading(true);
try {
// 检查是否与 MCP 缓存的配置不一致
const verifiedConfigStr = localStorage.getItem('mcp_verified_config');
let configChanged = false;
if (verifiedConfigStr) {
try {
const verifiedConfig = JSON.parse(verifiedConfigStr);
configChanged =
verifiedConfig.provider !== values.api_provider ||
verifiedConfig.baseUrl !== values.api_base_url ||
verifiedConfig.model !== values.llm_model;
} catch (e) {
console.error('Failed to parse verified config:', e);
}
}
await settingsApi.saveSettings(values);
message.success('设置已保存');
setHasSettings(true);
setIsDefaultSettings(false);
// 如果配置发生变化,需要处理 MCP 插件
if (configChanged) {
// 清除 MCP 验证缓存
localStorage.removeItem('mcp_verified_config');
// 检查并禁用所有 MCP 插件
try {
const plugins = await mcpPluginApi.getPlugins();
const activePlugins = plugins.filter(p => p.enabled);
if (activePlugins.length > 0) {
// 禁用所有插件
message.loading({ content: '正在禁用 MCP 插件...', key: 'disable_mcp' });
await Promise.all(activePlugins.map(p => mcpPluginApi.togglePlugin(p.id, false)));
message.success({ content: '已禁用所有 MCP 插件', key: 'disable_mcp' });
// 显示提示弹窗
modal.warning({
title: (
<Space>
<WarningOutlined style={{ color: '#faad14' }} />
<span>API </span>
</Space>
),
centered: true,
content: (
<div style={{ padding: '8px 0' }}>
<Alert
message="检测到您修改了 API 配置(提供商、地址或模型),为确保 MCP 插件正常工作,系统已自动禁用所有插件。"
type="warning"
showIcon
style={{ marginBottom: 16 }}
/>
<div style={{
padding: 12,
background: 'var(--color-info-bg)',
border: '1px solid var(--color-info-border)',
borderRadius: 8
}}>
<Text strong style={{ display: 'block', marginBottom: 8 }}></Text>
<ol style={{ margin: 0, paddingLeft: 20, fontSize: 13 }}>
<li> MCP </li>
<li>"模型能力检查"</li>
<li> Function Calling </li>
</ol>
</div>
</div>
),
okText: '前往 MCP 页面',
cancelText: '稍后处理',
onOk: () => {
navigate('/mcp-plugins');
},
});
}
} catch (err) {
console.error('Failed to disable MCP plugins:', err);
}
}
} catch (error) {
message.error('保存设置失败');
} finally {
@@ -348,10 +424,94 @@ export default function SettingsPage() {
const handlePresetActivate = async (presetId: string, presetName: string) => {
try {
// 获取预设配置用于比较
const preset = presets.find(p => p.id === presetId);
await settingsApi.activatePreset(presetId);
message.success(`已激活预设: ${presetName}`);
loadPresets();
loadSettings(); // 重新加载当前配置
// 检查是否与 MCP 缓存的配置不一致
if (preset) {
const verifiedConfigStr = localStorage.getItem('mcp_verified_config');
let configChanged = false;
if (verifiedConfigStr) {
try {
const verifiedConfig = JSON.parse(verifiedConfigStr);
configChanged =
verifiedConfig.provider !== preset.config.api_provider ||
verifiedConfig.baseUrl !== preset.config.api_base_url ||
verifiedConfig.model !== preset.config.llm_model;
} catch (e) {
console.error('Failed to parse verified config:', e);
configChanged = true; // 解析失败也视为配置变化
}
} else {
// 没有缓存的配置,如果有启用的插件也需要处理
configChanged = true;
}
if (configChanged) {
// 清除 MCP 验证缓存
localStorage.removeItem('mcp_verified_config');
// 检查并禁用所有 MCP 插件
try {
const plugins = await mcpPluginApi.getPlugins();
const activePlugins = plugins.filter(p => p.enabled);
if (activePlugins.length > 0) {
// 禁用所有插件
message.loading({ content: '正在禁用 MCP 插件...', key: 'disable_mcp' });
await Promise.all(activePlugins.map(p => mcpPluginApi.togglePlugin(p.id, false)));
message.success({ content: '已禁用所有 MCP 插件', key: 'disable_mcp' });
// 显示提示弹窗
modal.warning({
title: (
<Space>
<WarningOutlined style={{ color: '#faad14' }} />
<span>API </span>
</Space>
),
centered: true,
content: (
<div style={{ padding: '8px 0' }}>
<Alert
message={`切换到预设「${presetName}」后,API 配置发生了变化。为确保 MCP 插件正常工作,系统已自动禁用所有插件。`}
type="warning"
showIcon
style={{ marginBottom: 16 }}
/>
<div style={{
padding: 12,
background: 'var(--color-info-bg)',
border: '1px solid var(--color-info-border)',
borderRadius: 8
}}>
<Text strong style={{ display: 'block', marginBottom: 8 }}></Text>
<ol style={{ margin: 0, paddingLeft: 20, fontSize: 13 }}>
<li> MCP </li>
<li>"模型能力检查"</li>
<li> Function Calling </li>
</ol>
</div>
</div>
),
okText: '前往 MCP 页面',
cancelText: '稍后处理',
onOk: () => {
navigate('/mcp-plugins');
},
});
}
} catch (err) {
console.error('Failed to disable MCP plugins:', err);
}
}
}
} catch (error) {
message.error('激活失败');
console.error(error);