feat: 重构MCP功能和AI服务提供者架构
This commit is contained in:
@@ -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": {}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user