diff --git a/src/components/McpPanel.tsx b/src/components/McpPanel.tsx index 900a563..86af5f6 100644 --- a/src/components/McpPanel.tsx +++ b/src/components/McpPanel.tsx @@ -1,385 +1,28 @@ -import { useEffect, useState } from 'react'; -import { Button, Card, List, Modal, Form, Input, Select, Tag, Space, Popconfirm, App as AntApp, Tooltip, Alert } from 'antd'; -import { McpAPI, McpServer, McpStatus } from '../api'; - -interface Props { - agentId: string; -} - -const PRESETS: { label: string; description: string; config: any }[] = [ - { - label: 'filesystem (官方 NPM)', - description: '本地文件系统操作(读、列目录)。请把最后一个参数改成你要暴露给智能体的目录绝对路径。', - config: { - mcpServers: { - filesystem: { - command: 'npx', - args: ['-y', '@modelcontextprotocol/server-filesystem', '/path/to/your/dir'] - } - } - } - }, - { - label: 'fetch (官方 NPM)', - description: '抓取网页/URL 内容', - config: { - mcpServers: { - fetch: { command: 'npx', args: ['-y', '@modelcontextprotocol/server-fetch'] } - } - } - }, - { - label: 'sequential-thinking (官方 NPM)', - description: '辅助思维链推理', - config: { - mcpServers: { - 'sequential-thinking': { - command: 'npx', - args: ['-y', '@modelcontextprotocol/server-sequential-thinking'] - } - } - } - } -]; - -export default function McpPanel({ agentId }: Props) { - const { message } = AntApp.useApp(); - const [servers, setServers] = useState([]); - const [statusList, setStatusList] = useState([]); - const [statusLoading, setStatusLoading] = useState(false); - const [editing, setEditing] = useState(null); - const [createOpen, setCreateOpen] = useState(false); - const [importOpen, setImportOpen] = useState(false); - const [importJson, setImportJson] = useState(''); - - const refresh = async () => { - const list = await McpAPI.list(agentId); - setServers(list); - }; - - const refreshStatus = async () => { - setStatusLoading(true); - try { - const s = await McpAPI.status(agentId); - setStatusList(s); - } catch (e: any) { - message.error('状态获取失败:' + (e?.message ?? e)); - } finally { - setStatusLoading(false); - } - }; - - useEffect(() => { - refresh(); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [agentId]); - - const handleSave = async (values: any) => { - try { - // args 字符串 → 数组 - let args: string[] = []; - if (typeof values.argsText === 'string') { - args = values.argsText - .split(/\r?\n/) - .map((s: string) => s.trim()) - .filter(Boolean); - } - let env: Record = {}; - if (values.envText) { - try { - env = JSON.parse(values.envText); - } catch { - message.error('env 必须是合法 JSON 对象'); - return; - } - } - const payload = { - name: values.name, - transport: values.transport, - command: values.command || '', - args, - env, - url: values.url || '' - }; - if (editing) { - await McpAPI.update(agentId, editing.id, payload); - message.success('已更新'); - } else { - await McpAPI.create(agentId, payload); - message.success('已创建'); - } - setEditing(null); - setCreateOpen(false); - refresh(); - } catch (e: any) { - message.error('保存失败:' + (e?.message ?? e)); - } - }; - - const handleImport = async () => { - let parsed: any; - try { - parsed = JSON.parse(importJson); - } catch { - message.error('JSON 解析失败'); - return; - } - try { - const r = await McpAPI.importJSON(agentId, parsed, false); - message.success(`已导入 ${r.imported} 个 MCP Server`); - setImportOpen(false); - setImportJson(''); - refresh(); - } catch (e: any) { - message.error('导入失败:' + (e?.message ?? e)); - } - }; - - return ( - - 🔌 MCP Servers - {servers.length} 个 - - } - extra={ - - - - - - } - > - - - { - const status = statusList.find((x) => x.id === s.id); - return ( - { - setEditing(s); - setCreateOpen(true); - }} - > - 编辑 - , - { - await McpAPI.remove(agentId, s.id); - message.success('已删除'); - refresh(); - }} - > - - - ]} - > - - {s.transport} - {s.name} - {!s.enabled && 已停用} - {status?.error && ( - - 连接失败 - - )} - {status && !status.error && ( - {status.toolCount} 工具就绪 - )} - - } - description={ - - - {s.transport === 'stdio' - ? `${s.command} ${s.args.join(' ')}` - : s.url} - - {status?.tools && status.tools.length > 0 && ( - - {status.tools.slice(0, 8).map((t) => ( - - - {t.name} - - - ))} - {status.tools.length > 8 && +{status.tools.length - 8}} - - )} - - } - /> - - ); - }} - /> - - { - setCreateOpen(false); - setEditing(null); - }} - footer={null} - destroyOnHidden - > - { - setCreateOpen(false); - setEditing(null); - }} - /> - - - setImportOpen(false)} - onOk={handleImport} - okText="导入" - cancelText="取消" - > - - - {PRESETS.map((p) => ( - - - - ))} - - setImportJson(e.target.value)} - placeholder={`{ - "mcpServers": { - "filesystem": { - "command": "npx", - "args": ["-y", "@modelcontextprotocol/server-filesystem", "/path/to/dir"] - } - } -}`} - autoSize={{ minRows: 12, maxRows: 20 }} - style={{ fontFamily: 'Consolas, Menlo, monospace', fontSize: 13 }} - /> - - - ); -} - -function McpForm({ - initial, - onSubmit, - onCancel -}: { - initial: McpServer | null; - onSubmit: (values: any) => void; - onCancel: () => void; -}) { - const [form] = Form.useForm(); - const [transport, setTransport] = useState<'stdio' | 'sse' | 'http'>(initial?.transport ?? 'stdio'); - - useEffect(() => { - form.setFieldsValue({ - name: initial?.name ?? '', - transport: initial?.transport ?? 'stdio', - command: initial?.command ?? '', - argsText: (initial?.args ?? []).join('\n'), - envText: initial?.env ? JSON.stringify(initial.env, null, 2) : '', - url: initial?.url ?? '' - }); - setTransport(initial?.transport ?? 'stdio'); - }, [initial, form]); - - return ( -
- - - - - - - - - - - - - - ) : ( - - - - )} - - - - - -
- ); -} +import { useEffect, useState } from 'react'; +import { useMcpPanelLogic } from './McpPanel/McpPanelLogic'; +import McpPanelWeb from './McpPanel/components/McpPanelWeb'; +import McpPanelH5 from './McpPanel/components/McpPanelH5'; + +interface Props { + agentId: string; +} + +const isMobileDevice = () => { + if (typeof window === 'undefined') return false; + return window.innerWidth < 768; +}; + +export default function McpPanel({ agentId }: Props) { + const logic = useMcpPanelLogic({ agentId }); + const [isMobile, setIsMobile] = useState(isMobileDevice()); + + useEffect(() => { + const handleResize = () => { + setIsMobile(isMobileDevice()); + }; + window.addEventListener('resize', handleResize); + return () => window.removeEventListener('resize', handleResize); + }, []); + + return isMobile ? : ; +} diff --git a/src/components/McpPanel/McpPanelLogic.ts b/src/components/McpPanel/McpPanelLogic.ts new file mode 100644 index 0000000..06af663 --- /dev/null +++ b/src/components/McpPanel/McpPanelLogic.ts @@ -0,0 +1,170 @@ +import { useEffect, useState } from 'react'; +import { McpAPI, McpServer, McpStatus } from '../../api'; + +export const PRESETS: { label: string; description: string; config: any }[] = [ + { + label: 'filesystem (官方 NPM)', + description: '本地文件系统操作(读、列目录)。请把最后一个参数改成你要暴露给智能体的目录绝对路径。', + config: { + mcpServers: { + filesystem: { + command: 'npx', + args: ['-y', '@modelcontextprotocol/server-filesystem', '/path/to/your/dir'], + }, + }, + }, + }, + { + label: 'fetch (官方 NPM)', + description: '抓取网页/URL 内容', + config: { + mcpServers: { + fetch: { command: 'npx', args: ['-y', '@modelcontextprotocol/server-fetch'] }, + }, + }, + }, + { + label: 'sequential-thinking (官方 NPM)', + description: '辅助思维链推理', + config: { + mcpServers: { + 'sequential-thinking': { + command: 'npx', + args: ['-y', '@modelcontextprotocol/server-sequential-thinking'], + }, + }, + }, + }, +]; + +export function useMcpPanelLogic({ agentId }: { agentId: string }) { + const [servers, setServers] = useState([]); + const [statusList, setStatusList] = useState([]); + const [statusLoading, setStatusLoading] = useState(false); + const [editing, setEditing] = useState(null); + const [createOpen, setCreateOpen] = useState(false); + const [importOpen, setImportOpen] = useState(false); + const [importJson, setImportJson] = useState(''); + + const refresh = async () => { + const list = await McpAPI.list(agentId); + setServers(list); + }; + + const refreshStatus = async () => { + setStatusLoading(true); + try { + const s = await McpAPI.status(agentId); + setStatusList(s); + } catch (e: any) { + console.error('获取状态失败', e); + } finally { + setStatusLoading(false); + } + }; + + useEffect(() => { + refresh(); + }, [agentId]); + + const handleSave = async (values: any) => { + // args 字符串 → 数组 + let args: string[] = []; + if (typeof values.argsText === 'string') { + args = values.argsText + .split(/\r?\n/) + .map((s: string) => s.trim()) + .filter(Boolean); + } + let env: Record = {}; + if (values.envText) { + try { + env = JSON.parse(values.envText); + } catch { + return { success: false, error: 'env 必须是合法 JSON 对象' }; + } + } + const payload = { + name: values.name, + transport: values.transport, + command: values.command || '', + args, + env, + url: values.url || '', + }; + try { + if (editing) { + await McpAPI.update(agentId, editing.id, payload); + } else { + await McpAPI.create(agentId, payload); + } + setEditing(null); + setCreateOpen(false); + refresh(); + return { success: true }; + } catch (e: any) { + return { success: false, error: e?.message ?? '保存失败' }; + } + }; + + const handleImport = async () => { + let parsed: any; + try { + parsed = JSON.parse(importJson); + } catch { + return { success: false, error: 'JSON 解析失败' }; + } + try { + const r = await McpAPI.importJSON(agentId, parsed, false); + setImportOpen(false); + setImportJson(''); + refresh(); + return { success: true, imported: r.imported }; + } catch (e: any) { + return { success: false, error: e?.message ?? '导入失败' }; + } + }; + + const handleDelete = async (id: string) => { + await McpAPI.remove(agentId, id); + refresh(); + }; + + const openCreate = () => { + setEditing(null); + setCreateOpen(true); + }; + + const openEdit = (s: McpServer) => { + setEditing(s); + setCreateOpen(true); + }; + + const closeCreate = () => { + setCreateOpen(false); + setEditing(null); + }; + + return { + servers, + statusList, + statusLoading, + editing, + createOpen, + importOpen, + importJson, + PRESETS, + refresh, + refreshStatus, + handleSave, + handleImport, + handleDelete, + openCreate, + openEdit, + closeCreate, + setImportOpen, + setImportJson, + }; +} + +export type McpPanelLogicOutput = ReturnType; diff --git a/src/components/McpPanel/components/McpPanelH5.tsx b/src/components/McpPanel/components/McpPanelH5.tsx new file mode 100644 index 0000000..ff16172 --- /dev/null +++ b/src/components/McpPanel/components/McpPanelH5.tsx @@ -0,0 +1,338 @@ +import { useEffect } from 'react'; +import { Button, Card, List, Modal, App as AntApp, Space, Alert, Tag, Popconfirm, Tooltip } from 'antd'; +import type { McpServer } from '../../../api'; +import type { McpPanelLogicOutput } from '../McpPanelLogic'; + +interface Props { + agentId: string; + logic: McpPanelLogicOutput; +} + +export default function McpPanelH5({ agentId, logic }: Props) { + const { message } = AntApp.useApp(); + const { + servers, + statusList, + statusLoading, + createOpen, + importOpen, + importJson, + PRESETS, + handleImport, + handleDelete, + openCreate, + openEdit, + closeCreate, + setImportOpen, + setImportJson, + refreshStatus, + } = logic; + + return ( + + 🔌 MCP Servers + {servers.length} 个 + + } + extra={ + + } + style={{ borderRadius: 12 }} + > + + + + + + + + { + const status = statusList.find((x) => x.id === s.id); + return ( + openEdit(s)} + style={{ borderRadius: 6 }} + > + 编辑 + , + { + await handleDelete(s.id); + message.success('已删除'); + }} + > + + , + ]} + > + + + {s.transport} + + {s.name} + {!s.enabled && 已停用} + {status?.error && ( + + 连接失败 + + )} + {status && !status.error && ( + {status.toolCount} 工具 + )} + + } + description={ + + + {s.transport === 'stdio' + ? `${s.command} ${s.args.join(' ')}` + : s.url} + + {status?.tools && status.tools.length > 0 && ( + + {status.tools.slice(0, 4).map((t) => ( + + + {t.name} + + + ))} + {status.tools.length > 4 && +{status.tools.length - 4}} + + )} + + } + /> + + ); + }} + /> + + + { + const result = await logic.handleSave(values); + if (result.success) { + message.success(logic.editing ? '已更新' : '已创建'); + } else { + message.error(result.error); + } + }} + onCancel={closeCreate} + /> + + + setImportOpen(false)} + onOk={async () => { + const result = await handleImport(); + if (result.success) { + message.success(`已导入 ${result.imported} 个`); + } else { + message.error(result.error); + } + }} + okText="导入" + cancelText="取消" + > + + + {PRESETS.map((p) => ( + + + + ))} + +