update:1.更新mcp插件功能,目前只支持remote调用

This commit is contained in:
xiamuceer
2025-11-07 22:14:20 +08:00
parent 1e998920e3
commit 88115a45c5
26 changed files with 4088 additions and 138 deletions
+2
View File
@@ -14,6 +14,7 @@ import ChapterReader from './pages/ChapterReader';
import ChapterAnalysis from './pages/ChapterAnalysis';
import WritingStyles from './pages/WritingStyles';
import Settings from './pages/Settings';
import MCPPlugins from './pages/MCPPlugins';
// import Polish from './pages/Polish';
import Login from './pages/Login';
import AuthCallback from './pages/AuthCallback';
@@ -36,6 +37,7 @@ function App() {
<Route path="/" element={<ProtectedRoute><ProjectList /></ProtectedRoute>} />
<Route path="/wizard" element={<ProtectedRoute><ProjectWizardNew /></ProtectedRoute>} />
<Route path="/settings" element={<ProtectedRoute><Settings /></ProtectedRoute>} />
<Route path="/mcp-plugins" element={<ProtectedRoute><MCPPlugins /></ProtectedRoute>} />
<Route path="/chapters/:chapterId/reader" element={<ProtectedRoute><ChapterReader /></ProtectedRoute>} />
<Route path="/project/:projectId" element={<ProtectedRoute><ProjectDetail /></ProtectedRoute>}>
<Route index element={<Navigate to="world-setting" replace />} />
+708
View File
@@ -0,0 +1,708 @@
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<MCPPlugin[]>([]);
const [modalVisible, setModalVisible] = useState(false);
const [editingPlugin, setEditingPlugin] = useState<MCPPlugin | null>(null);
const [testingPluginId, setTestingPluginId] = useState<string | null>(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: (
<div>
<p style={{ marginBottom: 16, fontSize: '15px', fontWeight: 500 }}>{result.message}</p>
{/* 显示详细的测试结果 */}
{result.suggestions && result.suggestions.length > 0 && (
<div style={{ marginTop: 16 }}>
<Text strong style={{ fontSize: '14px' }}>:</Text>
<div style={{
marginTop: 8,
padding: 12,
background: '#f5f5f5',
borderRadius: 4,
fontSize: '13px',
fontFamily: 'monospace',
maxHeight: '400px',
overflowY: 'auto'
}}>
{result.suggestions.map((suggestion: string, index: number) => (
<div key={index} style={{
marginBottom: index < (result.suggestions?.length || 0) - 1 ? 8 : 0,
whiteSpace: 'pre-wrap',
wordBreak: 'break-word',
lineHeight: 1.6
}}>
{suggestion}
</div>
))}
</div>
</div>
)}
{/* 显示工具数量 */}
{result.tools_count !== undefined && (
<div style={{ marginTop: 12 }}>
<Text type="secondary">🔧 : <strong>{result.tools_count}</strong></Text>
</div>
)}
{/* 显示响应时间 */}
{result.response_time_ms !== undefined && (
<div style={{ marginTop: 4 }}>
<Text type="secondary"> : <strong>{result.response_time_ms}ms</strong></Text>
</div>
)}
<div style={{
marginTop: 16,
padding: 12,
background: '#f6ffed',
border: '1px solid #b7eb8f',
borderRadius: 4
}}>
<Text type="success" style={{ fontSize: '13px' }}>
"运行中"
</Text>
</div>
</div>
),
});
} else {
Modal.error({
title: '❌ 测试失败',
width: 700,
content: (
<div>
<p style={{ marginBottom: 16, fontSize: '15px', fontWeight: 500 }}>{result.message}</p>
{/* 显示错误信息 */}
{result.error && (
<div style={{ marginTop: 16 }}>
<Text strong style={{ fontSize: '14px' }}>:</Text>
<div style={{
marginTop: 8,
padding: 12,
background: '#fff2f0',
borderRadius: 4,
color: '#cf1322',
fontSize: '13px',
fontFamily: 'monospace',
whiteSpace: 'pre-wrap',
wordBreak: 'break-word',
maxHeight: '300px',
overflowY: 'auto'
}}>
{result.error}
</div>
</div>
)}
{/* 显示建议 */}
{result.suggestions && result.suggestions.length > 0 && (
<div style={{ marginTop: 16 }}>
<Text strong style={{ fontSize: '14px' }}>💡 :</Text>
<ul style={{ marginTop: 8, marginBottom: 0, paddingLeft: 20 }}>
{result.suggestions.map((suggestion: string, index: number) => (
<li key={index} style={{ marginBottom: 6, fontSize: '13px' }}>
{suggestion}
</li>
))}
</ul>
</div>
)}
<div style={{
marginTop: 16,
padding: 12,
background: '#fff7e6',
border: '1px solid #ffd591',
borderRadius: 4
}}>
<Text style={{ fontSize: '13px', color: '#ad6800' }}>
</Text>
</div>
</div>
),
});
}
} 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 <Tag color="default"></Tag>;
}
switch (plugin.status) {
case 'active':
return <Tag color="success" icon={<CheckCircleOutlined />}></Tag>;
case 'error':
return (
<Tooltip title={plugin.last_error}>
<Tag color="error" icon={<CloseCircleOutlined />}></Tag>
</Tooltip>
);
default:
return <Tag color="default"></Tag>;
}
};
return (
<Layout style={{ minHeight: '100vh', background: '#f0f2f5' }}>
{/* 顶部导航栏 */}
<Header style={{
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
padding: isMobile ? '0 16px' : '0 24px',
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
boxShadow: '0 2px 8px rgba(0,0,0,0.15)',
position: 'fixed',
top: 0,
left: 0,
right: 0,
zIndex: 1000,
height: isMobile ? 56 : 64
}}>
<Space size={isMobile ? 12 : 16}>
<Button
type="text"
icon={<ArrowLeftOutlined />}
onClick={() => navigate('/')}
style={{
color: '#fff',
fontSize: isMobile ? 16 : 18,
display: 'flex',
alignItems: 'center'
}}
>
{!isMobile && '返回'}
</Button>
<Title level={isMobile ? 4 : 3} style={{
margin: 0,
color: '#fff',
fontSize: isMobile ? 18 : 24
}}>
MCP插件管理
</Title>
</Space>
</Header>
{/* 主内容区 */}
<Content style={{
marginTop: isMobile ? 56 : 64,
padding: isMobile ? '16px' : '24px',
maxWidth: 1400,
width: '100%',
margin: `${isMobile ? 56 : 64}px auto 0`,
}}>
<Card
variant="borderless"
style={{
borderRadius: isMobile ? 8 : 12,
boxShadow: '0 2px 8px rgba(0,0,0,0.06)',
marginBottom: isMobile ? 16 : 24
}}
>
<div style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: isMobile ? 16 : 20
}}>
<Title level={isMobile ? 5 : 4} style={{ margin: 0 }}>
</Title>
<Button
type="primary"
icon={<PlusOutlined />}
onClick={handleCreate}
size={isMobile ? 'middle' : 'large'}
style={{
borderRadius: 8,
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
border: 'none'
}}
>
</Button>
</div>
<Alert
message="什么是 MCP 插件?"
description={
<div style={{ fontSize: isMobile ? '12px' : '14px' }}>
<p style={{ margin: '8px 0' }}>
MCP (Model Context Protocol) AI
</p>
<p style={{ margin: '8px 0 0 0' }}>
MCP AI 访API
</p>
</div>
}
type="info"
showIcon
icon={<InfoCircleOutlined />}
style={{ marginBottom: isMobile ? 16 : 20 }}
/>
</Card>
{/* 插件列表 */}
<Spin spinning={loading}>
{plugins.length === 0 ? (
<Empty
description="还没有添加任何插件"
image={Empty.PRESENTED_IMAGE_SIMPLE}
style={{ padding: isMobile ? '40px 0' : '60px 0' }}
>
<Button type="primary" icon={<PlusOutlined />} onClick={handleCreate}>
</Button>
</Empty>
) : (
<Space direction="vertical" size="middle" style={{ width: '100%' }}>
{plugins.map((plugin) => (
<Card
key={plugin.id}
size="small"
style={{
borderRadius: 8,
border: '1px solid #f0f0f0',
}}
>
<div
style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'flex-start',
gap: '16px',
flexWrap: isMobile ? 'wrap' : 'nowrap',
}}
>
<div style={{ flex: 1, minWidth: 0 }}>
<Space direction="vertical" size="small" style={{ width: '100%' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', flexWrap: 'wrap' }}>
<Text strong style={{ fontSize: isMobile ? '14px' : '16px' }}>
{plugin.display_name || plugin.plugin_name}
</Text>
{getStatusTag(plugin)}
<Tag color={plugin.plugin_type === 'http' ? 'blue' : 'cyan'}>
{plugin.plugin_type?.toUpperCase() || 'UNKNOWN'}
</Tag>
{plugin.category && plugin.category !== 'general' && (
<Tag color="purple">{plugin.category}</Tag>
)}
</div>
{plugin.description && (
<Paragraph
type="secondary"
style={{
margin: 0,
fontSize: isMobile ? '12px' : '13px',
}}
ellipsis={{ rows: 2 }}
>
{plugin.description}
</Paragraph>
)}
{/* 只显示有值的URL或命令,脱敏处理敏感信息 */}
{plugin.plugin_type === 'http' && plugin.server_url && (
<div style={{ fontSize: isMobile ? '11px' : '12px' }}>
<Text type="secondary" code>
{(() => {
// 脱敏处理:隐藏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=***');
}
})()}
</Text>
</div>
)}
{plugin.plugin_type === 'stdio' && plugin.command && (
<div style={{ fontSize: isMobile ? '11px' : '12px' }}>
<Text type="secondary" code>
{plugin.command} {plugin.args?.join(' ')}
</Text>
</div>
)}
{/* 显示最后错误信息 */}
{plugin.last_error && (
<Text type="danger" style={{ fontSize: isMobile ? '11px' : '12px' }}>
: {plugin.last_error}
</Text>
)}
</Space>
</div>
<Space size="small" wrap>
<Tooltip title={plugin.enabled ? '禁用插件' : '启用插件'}>
<Switch
checked={plugin.enabled}
onChange={(checked) => handleToggle(plugin, checked)}
size={isMobile ? 'small' : 'default'}
/>
</Tooltip>
<Tooltip title="测试连接">
<Button
icon={<ThunderboltOutlined />}
onClick={() => handleTest(plugin.id)}
loading={testingPluginId === plugin.id}
size={isMobile ? 'small' : 'middle'}
/>
</Tooltip>
<Tooltip title="查看工具">
<Button
icon={<ToolOutlined />}
onClick={() => handleViewTools(plugin.id)}
disabled={!plugin.enabled || plugin.status !== 'active'}
size={isMobile ? 'small' : 'middle'}
/>
</Tooltip>
<Tooltip title="编辑">
<Button
icon={<EditOutlined />}
onClick={() => handleEdit(plugin)}
size={isMobile ? 'small' : 'middle'}
/>
</Tooltip>
<Tooltip title="删除">
<Button
danger
icon={<DeleteOutlined />}
onClick={() => handleDelete(plugin)}
size={isMobile ? 'small' : 'middle'}
/>
</Tooltip>
</Space>
</div>
</Card>
))}
</Space>
)}
</Spin>
</Content>
{/* 创建/编辑插件模态框 */}
<Modal
title={editingPlugin ? '编辑插件' : '添加插件'}
open={modalVisible}
onCancel={() => {
setModalVisible(false);
form.resetFields();
}}
onOk={() => form.submit()}
width={isMobile ? '100%' : 600}
confirmLoading={loading}
okText="保存"
cancelText="取消"
>
<Form form={form} layout="vertical" onFinish={handleSubmit}>
<Form.Item
label="MCP配置JSON"
name="config_json"
rules={[{ required: true, message: '请输入配置JSON' }]}
extra="粘贴标准MCP配置,系统自动提取插件名称。支持HTTP和Stdio类型"
>
<TextArea
rows={16}
placeholder={`示例:
{
"mcpServers": {
"exa": {
"type": "http",
"url": "https://mcp.exa.ai/mcp?exaApiKey=YOUR_API_KEY",
"headers": {}
}
}
}`}
style={{ fontFamily: 'monospace', fontSize: '13px' }}
/>
</Form.Item>
<Form.Item
label="插件分类"
name="category"
rules={[{ required: true, message: '请选择插件分类' }]}
extra="选择插件的功能类别,用于AI智能匹配使用场景"
>
<Select placeholder="请选择分类">
<Select.Option value="search"> (Search) - </Select.Option>
<Select.Option value="analysis"> (Analysis) - </Select.Option>
<Select.Option value="filesystem"> (FileSystem) - </Select.Option>
<Select.Option value="database"> (Database) - </Select.Option>
<Select.Option value="api">API调用 (API) - </Select.Option>
<Select.Option value="generation"> (Generation) - </Select.Option>
<Select.Option value="general"> (General) - </Select.Option>
</Select>
</Form.Item>
</Form>
</Modal>
{/* 查看工具列表模态框 */}
<Modal
title="可用工具列表"
open={!!viewingTools}
onCancel={() => setViewingTools(null)}
footer={[
<Button key="close" onClick={() => setViewingTools(null)}>
</Button>,
]}
width={isMobile ? '100%' : 700}
>
{viewingTools && (
<Space direction="vertical" size="middle" style={{ width: '100%' }}>
{viewingTools.tools.length === 0 ? (
<Empty description="该插件没有提供任何工具" />
) : (
viewingTools.tools.map((tool, index) => (
<Card key={index} size="small" style={{ borderRadius: 8 }}>
<Descriptions column={1} size="small">
<Descriptions.Item label="工具名称">
<Text code strong>
{tool.name}
</Text>
</Descriptions.Item>
{tool.description && (
<Descriptions.Item label="描述">{tool.description}</Descriptions.Item>
)}
{tool.inputSchema && (
<Descriptions.Item label="输入参数">
<pre
style={{
margin: 0,
padding: 8,
background: '#f5f5f5',
borderRadius: 4,
fontSize: isMobile ? '11px' : '12px',
overflow: 'auto',
}}
>
{JSON.stringify(tool.inputSchema, null, 2)}
</pre>
</Descriptions.Item>
)}
</Descriptions>
</Card>
))
)}
</Space>
)}
</Modal>
</Layout>
);
}
+70 -48
View File
@@ -1,7 +1,7 @@
import { useEffect, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { Card, Button, Empty, Modal, message, Spin, Row, Col, Statistic, Space, Tag, Progress, Typography, Tooltip, Badge, Alert, Upload, Checkbox, Divider, Switch } from 'antd';
import { EditOutlined, DeleteOutlined, BookOutlined, RocketOutlined, CalendarOutlined, FileTextOutlined, TrophyOutlined, FireOutlined, SettingOutlined, InfoCircleOutlined, CloseOutlined, UploadOutlined, DownloadOutlined } from '@ant-design/icons';
import { Card, Button, Empty, Modal, message, Spin, Row, Col, Statistic, Space, Tag, Progress, Typography, Tooltip, Badge, Alert, Upload, Checkbox, Divider, Switch, Dropdown } from 'antd';
import { EditOutlined, DeleteOutlined, BookOutlined, RocketOutlined, CalendarOutlined, FileTextOutlined, TrophyOutlined, FireOutlined, SettingOutlined, InfoCircleOutlined, CloseOutlined, UploadOutlined, DownloadOutlined, ApiOutlined, MoreOutlined } from '@ant-design/icons';
import { projectApi } from '../services/api';
import { useStore } from '../store';
import { useProjectSync } from '../store/hooks';
@@ -388,10 +388,25 @@ export default function ProjectList() {
>
</Button>
<Button
type="default"
size="middle"
icon={<ApiOutlined />}
onClick={() => navigate('/mcp-plugins')}
style={{
flex: 1,
borderRadius: 8,
borderColor: '#722ed1',
color: '#722ed1',
boxShadow: '0 2px 8px rgba(114, 46, 209, 0.2)'
}}
>
MCP
</Button>
</Space>
</Space>
) : (
// PC端:原有布局
// PC端:优化后的布局 - 主要按钮 + 下拉菜单
<Space size={12} style={{ display: 'flex', justifyContent: 'flex-end' }}>
<Button
type="primary"
@@ -407,51 +422,6 @@ export default function ProjectList() {
>
</Button>
<Button
type="default"
size="large"
icon={<DownloadOutlined />}
onClick={handleOpenExportModal}
disabled={exportableProjects.length === 0}
style={{
borderRadius: 8,
borderColor: '#1890ff',
color: '#1890ff',
boxShadow: '0 2px 8px rgba(24, 144, 255, 0.2)'
}}
onMouseEnter={(e) => {
e.currentTarget.style.borderColor = '#1890ff';
e.currentTarget.style.background = '#e6f7ff';
}}
onMouseLeave={(e) => {
e.currentTarget.style.borderColor = '#1890ff';
e.currentTarget.style.background = 'transparent';
}}
>
</Button>
<Button
type="default"
size="large"
icon={<UploadOutlined />}
onClick={() => setImportModalVisible(true)}
style={{
borderRadius: 8,
borderColor: '#52c41a',
color: '#52c41a',
boxShadow: '0 2px 8px rgba(82, 196, 26, 0.2)'
}}
onMouseEnter={(e) => {
e.currentTarget.style.borderColor = '#52c41a';
e.currentTarget.style.background = '#f6ffed';
}}
onMouseLeave={(e) => {
e.currentTarget.style.borderColor = '#52c41a';
e.currentTarget.style.background = 'transparent';
}}
>
</Button>
<Button
type="default"
size="large"
@@ -476,6 +446,58 @@ export default function ProjectList() {
>
API设置
</Button>
<Dropdown
menu={{
items: [
{
key: 'export',
label: '导出项目',
icon: <DownloadOutlined />,
onClick: handleOpenExportModal,
disabled: exportableProjects.length === 0
},
{
key: 'import',
label: '导入项目',
icon: <UploadOutlined />,
onClick: () => setImportModalVisible(true)
},
{
type: 'divider'
},
{
key: 'mcp',
label: 'MCP插件',
icon: <ApiOutlined />,
onClick: () => navigate('/mcp-plugins')
}
]
}}
placement="bottomRight"
>
<Button
size="large"
icon={<MoreOutlined />}
style={{
borderRadius: 8,
borderColor: '#d9d9d9',
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.08)',
transition: 'all 0.3s ease'
}}
onMouseEnter={(e) => {
e.currentTarget.style.borderColor = '#1890ff';
e.currentTarget.style.color = '#1890ff';
e.currentTarget.style.boxShadow = '0 2px 12px rgba(24, 144, 255, 0.3)';
}}
onMouseLeave={(e) => {
e.currentTarget.style.borderColor = '#d9d9d9';
e.currentTarget.style.color = 'rgba(0, 0, 0, 0.88)';
e.currentTarget.style.boxShadow = '0 2px 8px rgba(0, 0, 0, 0.08)';
}}
>
</Button>
</Dropdown>
<UserMenu />
</Space>
)}
+54
View File
@@ -1,4 +1,9 @@
import axios from 'axios';
interface MCPPluginSimpleCreate {
config_json: string;
enabled: boolean;
}
import { message } from 'antd';
import { ssePost } from '../utils/sseClient';
import type { SSEClientOptions } from '../utils/sseClient';
@@ -30,6 +35,13 @@ import type {
WritingStyleUpdate,
PresetStyle,
WritingStyleListResponse,
MCPPlugin,
MCPPluginCreate,
MCPPluginUpdate,
MCPTestResult,
MCPTool,
MCPToolCallRequest,
MCPToolCallResponse,
} from '../types';
const api = axios.create({
@@ -428,4 +440,46 @@ export const wizardStreamApi = {
{},
options
),
};
export const mcpPluginApi = {
// 获取所有插件
getPlugins: () =>
api.get<unknown, MCPPlugin[]>('/mcp/plugins'),
// 获取单个插件
getPlugin: (id: string) =>
api.get<unknown, MCPPlugin>(`/mcp/plugins/${id}`),
// 创建插件
createPlugin: (data: MCPPluginCreate) =>
api.post<unknown, MCPPlugin>('/mcp/plugins', data),
// 简化创建插件(通过标准MCP配置JSON)
createPluginSimple: (data: MCPPluginSimpleCreate) =>
api.post<unknown, MCPPlugin>('/mcp/plugins/simple', data),
// 更新插件
updatePlugin: (id: string, data: MCPPluginUpdate) =>
api.put<unknown, MCPPlugin>(`/mcp/plugins/${id}`, data),
// 删除插件
deletePlugin: (id: string) =>
api.delete<unknown, { message: string }>(`/mcp/plugins/${id}`),
// 启用/禁用插件
togglePlugin: (id: string, enabled: boolean) =>
api.post<unknown, MCPPlugin>(`/mcp/plugins/${id}/toggle`, null, { params: { enabled } }),
// 测试插件连接
testPlugin: (id: string) =>
api.post<unknown, MCPTestResult>(`/mcp/plugins/${id}/test`),
// 获取插件工具列表
getPluginTools: (id: string) =>
api.get<unknown, { tools: MCPTool[] }>(`/mcp/plugins/${id}/tools`),
// 调用工具
callTool: (data: MCPToolCallRequest) =>
api.post<unknown, MCPToolCallResponse>('/mcp/call', data),
};
+81
View File
@@ -522,4 +522,85 @@ export interface TriggerAnalysisResponse {
chapter_id: string;
status: string;
message: string;
}
// MCP 插件类型定义 - 优化后只包含必要字段
export interface MCPPlugin {
id: string;
plugin_name: string;
display_name: string;
description?: string;
plugin_type: 'http' | 'stdio';
category: string;
// HTTP类型字段
server_url?: string;
headers?: Record<string, string>;
// Stdio类型字段
command?: string;
args?: string[];
env?: Record<string, string>;
// 状态字段
enabled: boolean;
status: 'active' | 'inactive' | 'error';
last_error?: string;
last_test_at?: string;
// 时间戳
created_at: string;
}
export interface MCPPluginCreate {
plugin_name: string;
display_name?: string;
description?: string;
server_type: 'http' | 'stdio';
server_url?: string;
command?: string;
args?: string[];
env?: Record<string, string>;
headers?: Record<string, string>;
enabled?: boolean;
}
export interface MCPPluginUpdate {
display_name?: string;
description?: string;
server_url?: string;
command?: string;
args?: string[];
env?: Record<string, string>;
headers?: Record<string, string>;
enabled?: boolean;
}
export interface MCPTool {
name: string;
description?: string;
inputSchema?: Record<string, unknown>;
}
export interface MCPTestResult {
success: boolean;
message: string;
tools?: MCPTool[];
tools_count?: number;
response_time_ms?: number;
error?: string;
error_type?: string;
suggestions?: string[];
}
export interface MCPToolCallRequest {
plugin_id: string;
tool_name: string;
arguments: Record<string, unknown>;
}
export interface MCPToolCallResponse {
success: boolean;
result?: unknown;
error?: string;
}