import { useState, useEffect } from 'react'; import { useNavigate } from 'react-router-dom'; import { Card, Button, Space, Typography, Modal, Form, Input, Switch, Select, message, Tag, Tooltip, Spin, Empty, Alert, Descriptions, Layout, } from 'antd'; import { PlusOutlined, EditOutlined, DeleteOutlined, CheckCircleOutlined, CloseCircleOutlined, ThunderboltOutlined, InfoCircleOutlined, ToolOutlined, ArrowLeftOutlined, } from '@ant-design/icons'; import { mcpPluginApi } from '../services/api'; import type { MCPPlugin, MCPTool } from '../types'; const { Paragraph, Text, Title } = Typography; const { TextArea } = Input; const { Header, Content } = Layout; export default function MCPPluginsPage() { const navigate = useNavigate(); const isMobile = window.innerWidth <= 768; const [form] = Form.useForm(); 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); useEffect(() => { loadPlugins(); }, []); const loadPlugins = async () => { setLoading(true); try { const data = await mcpPluginApi.getPlugins(); setPlugins(data); } catch (error) { message.error('加载插件列表失败'); } finally { setLoading(false); } }; const handleCreate = () => { 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: any = { mcpServers: { [plugin.plugin_name]: { type: plugin.plugin_type || 'http' } } }; if (plugin.plugin_type === 'http') { 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}" 吗?`, okText: '确定', cancelText: '取消', okType: 'danger', onOk: async () => { try { await mcpPluginApi.deletePlugin(plugin.id); message.success('插件已删除'); loadPlugins(); } catch (error) { message.error('删除插件失败'); } }, }); }; const handleToggle = async (plugin: MCPPlugin, enabled: boolean) => { try { await mcpPluginApi.togglePlugin(plugin.id, enabled); message.success(enabled ? '插件已启用' : '插件已禁用'); loadPlugins(); } catch (error) { message.error('切换插件状态失败'); } }; const handleTest = async (pluginId: string) => { setTestingPluginId(pluginId); try { const result = await mcpPluginApi.testPlugin(pluginId); // 测试完成后,无论成功失败都刷新插件列表以更新状态 await loadPlugins(); if (result.success) { Modal.success({ title: '✅ 测试成功', width: 700, content: (

{result.message}

{/* 显示详细的测试结果 */} {result.suggestions && result.suggestions.length > 0 && (
测试详情:
{result.suggestions.map((suggestion: string, index: number) => (
{suggestion}
))}
)} {/* 显示工具数量 */} {result.tools_count !== undefined && (
🔧 可用工具数: {result.tools_count}
)} {/* 显示响应时间 */} {result.response_time_ms !== undefined && (
⏱️ 响应时间: {result.response_time_ms}ms
)}
✓ 插件状态已自动更新为"运行中"
), }); } else { Modal.error({ title: '❌ 测试失败', width: 700, content: (

{result.message}

{/* 显示错误信息 */} {result.error && (
错误信息:
{result.error}
)} {/* 显示建议 */} {result.suggestions && result.suggestions.length > 0 && (
💡 建议:
    {result.suggestions.map((suggestion: string, index: number) => (
  • {suggestion}
  • ))}
)}
⚠️ 插件状态已更新,请检查配置后重试
), }); } } catch (error: any) { message.error('测试插件失败'); } finally { setTestingPluginId(null); } }; const handleViewTools = async (pluginId: string) => { try { const result = await mcpPluginApi.getPluginTools(pluginId); setViewingTools({ pluginId, tools: result.tools }); } catch (error) { message.error('获取工具列表失败'); } }; const handleSubmit = async (values: any) => { setLoading(true); try { // 验证JSON格式 try { JSON.parse(values.config_json); } catch (e) { 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: any) { const errorMsg = error?.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 ( }>错误 ); default: return 未激活; } }; return ( {/* 顶部导航栏 */}
MCP插件管理
{/* 主内容区 */}
我的插件

MCP (Model Context Protocol) 是一个标准化的协议,允许 AI 调用外部工具获取数据。

通过添加 MCP 插件,AI 可以访问搜索引擎、数据库、API 等外部服务,增强创作能力。

} type="info" showIcon icon={} style={{ marginBottom: isMobile ? 16 : 20 }} />
{/* 插件列表 */} {plugins.length === 0 ? ( ) : ( {plugins.map((plugin) => (
{plugin.display_name || plugin.plugin_name} {getStatusTag(plugin)} {plugin.plugin_type?.toUpperCase() || 'UNKNOWN'} {plugin.category && plugin.category !== 'general' && ( {plugin.category} )}
{plugin.description && ( {plugin.description} )} {/* 只显示有值的URL或命令,脱敏处理敏感信息 */} {plugin.plugin_type === 'http' && 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} )}
handleToggle(plugin, checked)} size={isMobile ? 'small' : 'default'} />
))}
)}
{/* 创建/编辑插件模态框 */} { setModalVisible(false); form.resetFields(); }} onOk={() => form.submit()} width={isMobile ? '100%' : 600} confirmLoading={loading} okText="保存" cancelText="取消" >