import { useState, useEffect } from 'react'; import { Card, Button, Space, Typography, Modal, Form, Input, Switch, Select, message, Tag, Spin, Empty, Alert, Row, Col, } from 'antd'; import { PlusOutlined, EditOutlined, DeleteOutlined, CheckCircleOutlined, CloseCircleOutlined, ThunderboltOutlined, InfoCircleOutlined, ToolOutlined, ApiOutlined, QuestionCircleOutlined, WarningOutlined, } from '@ant-design/icons'; import { mcpPluginApi, settingsApi } from '../services/api'; import type { MCPPlugin, MCPTool } from '../types'; const { Paragraph, Text, Title } = Typography; const { TextArea } = Input; export default function MCPPluginsPage() { const [isMobile, setIsMobile] = useState(window.innerWidth <= 768); const [form] = Form.useForm(); // 响应式监听窗口大小变化 useEffect(() => { const handleResize = () => { setIsMobile(window.innerWidth <= 768); }; window.addEventListener('resize', handleResize); return () => window.removeEventListener('resize', handleResize); }, []); const [modal, contextHolder] = Modal.useModal(); const [loading, setLoading] = useState(false); const [plugins, setPlugins] = useState([]); const [modalVisible, setModalVisible] = useState(false); const [editingPlugin, setEditingPlugin] = useState(null); const [testingPluginId, setTestingPluginId] = useState(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(() => { 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 () => { try { const data = await mcpPluginApi.getPlugins(); setPlugins(data); } catch (error) { console.error('Load plugins failed:', error); message.error('加载插件列表失败'); } }; const handleCreate = () => { if (modelSupportStatus !== 'supported') { modal.confirm({ title: '模型能力检查', centered: true, icon: , content: '为了确保 MCP 插件正常工作,您当前使用的 AI 模型必须支持 Function Calling(工具调用)能力。请先进行模型支持检测。', okText: '去检测', cancelText: '取消', onOk: handleCheckFunctionCalling, }); return; } setEditingPlugin(null); form.resetFields(); form.setFieldsValue({ enabled: true, category: 'search', config_json: `{ "mcpServers": { "exa": { "type": "http", "url": "https://mcp.exa.ai/mcp?exaApiKey=YOUR_API_KEY", "headers": {} } } }` }); setModalVisible(true); }; const handleEdit = (plugin: MCPPlugin) => { setEditingPlugin(plugin); // 重构为标准MCP配置格式 const mcpConfig: Record>> = { mcpServers: { [plugin.plugin_name]: { type: 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 { mcpConfig.mcpServers[plugin.plugin_name].command = plugin.command; mcpConfig.mcpServers[plugin.plugin_name].args = plugin.args || []; mcpConfig.mcpServers[plugin.plugin_name].env = plugin.env || {}; } form.setFieldsValue({ config_json: JSON.stringify(mcpConfig, null, 2), enabled: plugin.enabled, category: plugin.category || 'general', }); setModalVisible(true); }; const handleDelete = (plugin: MCPPlugin) => { modal.confirm({ title: '删除插件', content: `确定要删除插件 "${plugin.display_name || plugin.plugin_name}" 吗?`, centered: true, okText: '确定', cancelText: '取消', okType: 'danger', onOk: async () => { try { await mcpPluginApi.deletePlugin(plugin.id); message.success('插件已删除'); loadPlugins(); } catch (error) { console.error('Delete plugin failed:', error); message.error('删除插件失败'); } }, }); }; const handleToggle = async (plugin: MCPPlugin, enabled: boolean) => { try { await mcpPluginApi.togglePlugin(plugin.id, enabled); message.success(enabled ? '插件已启用' : '插件已禁用'); loadPlugins(); } catch (error) { console.error('Toggle plugin failed:', error); message.error('切换插件状态失败'); } }; const handleTest = async (pluginId: string) => { setTestingPluginId(pluginId); try { const result = await mcpPluginApi.testPlugin(pluginId); // 测试完成后,无论成功失败都刷新插件列表以更新状态 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: '🎉 测试成功', centered: true, width: isMobile ? '95%' : 700, content: (
✓ {result.message}
可用工具数
{result.tools_count || 0}
总响应时间
{result.response_time_ms?.toFixed(0) || 0}ms
{aiChoice && (
🤖 AI选择的工具 {aiChoice} {callTime && {callTime}}
)} {paramsStr && (
📝 调用参数
                    {(() => { try { return JSON.stringify(JSON.parse(paramsStr), null, 2); } catch { return paramsStr; } })()}
                  
)} {resultStr && (
📊 返回结果预览
                    {resultStr}
                  
)}
), }); } else { modal.error({ title: '测试失败', centered: true, width: isMobile ? '90%' : 600, content: (
{result.error && (
错误信息: {result.error}
)} {result.suggestions && result.suggestions.length > 0 && (
💡 建议:
    {result.suggestions.map((s: string, i: number) => (
  • {s}
  • ))}
)}
), }); } } catch { message.error('测试插件失败'); } finally { setTestingPluginId(null); } }; const handleViewTools = async (pluginId: string) => { try { const result = await mcpPluginApi.getPluginTools(pluginId); setViewingTools({ pluginId, tools: result.tools }); } catch (error) { console.error('Get tools failed:', error); message.error('获取工具列表失败'); } }; 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: (
✓ {result.message}
API 提供商
{result.provider}
响应时间
{result.response_time_ms?.toFixed(0) || 0}ms
🔧 模型信息 {result.model} {result.details?.finish_reason && ( finish_reason: {result.details.finish_reason} )}
{result.details && (
📊 检测详情
✓ 工具调用数量: {result.details.tool_call_count || 0}
✓ 测试工具: {result.details.test_tool || 'N/A'}
✓ 响应类型: {result.details.response_type || 'N/A'}
)} {result.tool_calls && result.tool_calls.length > 0 && (
🔨 工具调用示例
                    {JSON.stringify(result.tool_calls[0], null, 2)}
                  
)} {result.suggestions && result.suggestions.length > 0 && (
💡 建议
    {result.suggestions.map((s: string, i: number) => (
  • {s}
  • ))}
)}
), }); } else { setModelSupportStatus('unsupported'); modal.warning({ title: '❌ Function Calling 支持检测', centered: true, width: isMobile ? '95%' : 700, content: (
{result.error && (
错误信息: {result.error}
)} {result.response_preview && (
📝 模型返回内容(前200字符)
                    {result.response_preview}
                  
)} {result.suggestions && result.suggestions.length > 0 && (
💡 建议:
    {result.suggestions.map((s: string, i: number) => (
  • {s}
  • ))}
)}
), }); } } 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 { message.error('配置JSON格式错误,请检查'); setLoading(false); return; } const data = { config_json: values.config_json, enabled: values.enabled, category: values.category || 'general', }; // 统一使用简化API,后端会自动判断是创建还是更新 await mcpPluginApi.createPluginSimple(data); message.success(editingPlugin ? '插件已更新' : '插件已创建'); setModalVisible(false); form.resetFields(); loadPlugins(); } catch (error: unknown) { const err = error as { response?: { data?: { detail?: string } } }; const errorMsg = err?.response?.data?.detail || '操作失败'; message.error(errorMsg); } finally { setLoading(false); } }; const getStatusTag = (plugin: MCPPlugin) => { if (!plugin.enabled) { return 已禁用; } switch (plugin.status) { case 'active': return }>运行中; case 'error': return ( } title={plugin.last_error}>错误 ); default: return 未激活; } }; return ( <> {contextHolder}
{/* 顶部导航卡片 */} {/* 装饰性背景元素 */}
<ToolOutlined style={{ color: 'rgba(255,255,255,0.9)', marginRight: 8 }} /> MCP插件管理 扩展AI能力,连接外部工具与服务
{modelSupportStatus === 'supported' ? ( ) : modelSupportStatus === 'unsupported' ? ( ) : ( )}
模型能力检查 {modelSupportStatus === 'supported' ? '当前模型支持 Function Calling,可正常使用 MCP 插件' : modelSupportStatus === 'unsupported' ? '当前模型不支持 Function Calling,无法使用 MCP 插件' : '请先检测模型是否支持 Function Calling 能力'}
什么是 MCP 插件? MCP (Model Context Protocol) 协议允许 AI 调用外部工具获取数据。通过添加插件,AI 可以访问搜索引擎、数据库、API 等服务,大幅增强创作能力。
{/* 主内容区 */}
{/* 模型能力未验证时的警告提示 */} {modelSupportStatus !== 'supported' && plugins.length > 0 && ( : } style={{ marginBottom: 16, borderRadius: 8 }} action={ } /> )} {/* 插件列表 */} {plugins.length === 0 ? ( ) : ( {plugins.map((plugin) => (
{/* 插件信息区域 */}
{/* 标题和状态标签 */}
{plugin.display_name || plugin.plugin_name} {getStatusTag(plugin)}
{/* 移动端:开关放在标题行右侧 */} {isMobile && ( handleToggle(plugin, checked)} disabled={modelSupportStatus !== 'supported'} size="small" checkedChildren="开" unCheckedChildren="关" style={{ flexShrink: 0, height: 16, minHeight: 16, lineHeight: '16px' }} /> )}
{/* 类型和分类标签 */}
{plugin.plugin_type?.toUpperCase() || 'UNKNOWN'} {plugin.category && plugin.category !== 'general' && ( {plugin.category} )}
{plugin.description && ( {plugin.description} )} {/* 只显示有值的URL或命令,脱敏处理敏感信息 */} {(plugin.plugin_type === 'http' || plugin.plugin_type === 'streamable_http' || plugin.plugin_type === 'sse') && plugin.server_url && (
{(() => { // 脱敏处理:隐藏URL中的API Key const url = plugin.server_url; try { const urlObj = new URL(url); // 替换查询参数中的敏感信息 const params = new URLSearchParams(urlObj.search); let maskedUrl = `${urlObj.protocol}//${urlObj.host}${urlObj.pathname}`; const sensitiveKeys = ['apiKey', 'api_key', 'key', 'token', 'secret', 'password', 'auth']; let hasParams = false; params.forEach((value, key) => { const isSensitive = sensitiveKeys.some(k => key.toLowerCase().includes(k.toLowerCase())); const maskedValue = isSensitive ? '***' : value; maskedUrl += (hasParams ? '&' : '?') + `${key}=${maskedValue}`; hasParams = true; }); return maskedUrl; } catch { // 如果URL解析失败,尝试简单替换 return url.replace(/([?&])(apiKey|api_key|key|token|secret|password|auth)=([^&]+)/gi, '$1$2=***'); } })()}
)} {plugin.plugin_type === 'stdio' && plugin.command && (
{plugin.command} {plugin.args?.join(' ')}
)} {/* 显示最后错误信息 */} {plugin.last_error && ( 错误: {plugin.last_error} )}
{/* 操作按钮区域 */}
{/* 桌面端显示开关 */} {!isMobile && ( handleToggle(plugin, checked)} disabled={modelSupportStatus !== 'supported'} checkedChildren="开" unCheckedChildren="关" /> )}
))}
)}
{/* 创建/编辑插件模态框 */} { setModalVisible(false); form.resetFields(); }} onOk={() => form.submit()} width={isMobile ? '100%' : 600} confirmLoading={loading} okText="保存" cancelText="取消" >