import React, { useEffect, useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; import { toast } from "sonner"; import { Plus, Server, Check } from "lucide-react"; import { Button } from "@/components/ui/button"; import { Dialog, DialogContent, DialogHeader, DialogTitle, } from "@/components/ui/dialog"; import { mcpApi, type AppType } from "@/lib/api"; import { McpServer } from "@/types"; import McpListItem from "./McpListItem"; import McpFormModal from "./McpFormModal"; import { ConfirmDialog } from "../ConfirmDialog"; import { extractErrorMessage, translateMcpBackendError, } from "@/utils/errorUtils"; interface McpPanelProps { open: boolean; onOpenChange: (open: boolean) => void; appType: AppType; } /** * MCP 管理面板 * 采用与主界面一致的设计风格,右上角添加按钮,每个 MCP 占一行 */ const McpPanel: React.FC = ({ open, onOpenChange, appType, }) => { const { t } = useTranslation(); const [servers, setServers] = useState>({}); const [loading, setLoading] = useState(true); const [isFormOpen, setIsFormOpen] = useState(false); const [editingId, setEditingId] = useState(null); const [confirmDialog, setConfirmDialog] = useState<{ isOpen: boolean; title: string; message: string; onConfirm: () => void; } | null>(null); const reload = async () => { setLoading(true); try { const cfg = await mcpApi.getConfig(appType); setServers(cfg.servers || {}); } finally { setLoading(false); } }; useEffect(() => { const setup = async () => { try { // 初始化:仅从对应客户端导入已有 MCP,不做“预设落库” if (appType === "claude") { await mcpApi.importFromClaude(); } else if (appType === "codex") { await mcpApi.importFromCodex(); } } catch (e) { console.warn("MCP 初始化导入失败(忽略继续)", e); } finally { await reload(); } }; setup(); // appType 改变时重新初始化 }, [appType]); const handleToggle = async (id: string, enabled: boolean) => { // 乐观更新:立即更新 UI const previousServers = servers; setServers((prev) => ({ ...prev, [id]: { ...prev[id], enabled, }, })); try { // 后台调用 API await mcpApi.setEnabled(appType, id, enabled); toast.success( enabled ? t("mcp.msg.enabled") : t("mcp.msg.disabled"), { duration: 1500 }, ); } catch (e: any) { // 失败时回滚 setServers(previousServers); const detail = extractErrorMessage(e); const mapped = translateMcpBackendError(detail, t); toast.error( mapped || detail || t("mcp.error.saveFailed"), { duration: mapped || detail ? 6000 : 5000 }, ); } }; const handleEdit = (id: string) => { setEditingId(id); setIsFormOpen(true); }; const handleAdd = () => { setEditingId(null); setIsFormOpen(true); }; const handleDelete = (id: string) => { setConfirmDialog({ isOpen: true, title: t("mcp.confirm.deleteTitle"), message: t("mcp.confirm.deleteMessage", { id }), onConfirm: async () => { try { await mcpApi.deleteServerInConfig(appType, id); await reload(); setConfirmDialog(null); toast.success(t("mcp.msg.deleted"), { duration: 1500 }); } catch (e: any) { const detail = extractErrorMessage(e); const mapped = translateMcpBackendError(detail, t); toast.error( mapped || detail || t("mcp.error.deleteFailed"), { duration: mapped || detail ? 6000 : 5000 }, ); } }, }); }; const handleSave = async ( id: string, server: McpServer, options?: { syncOtherSide?: boolean }, ) => { try { const payload: McpServer = { ...server, id }; await mcpApi.upsertServerInConfig(appType, id, payload, { syncOtherSide: options?.syncOtherSide, }); await reload(); setIsFormOpen(false); setEditingId(null); toast.success(t("mcp.msg.saved"), { duration: 1500 }); } catch (e: any) { const detail = extractErrorMessage(e); const mapped = translateMcpBackendError(detail, t); toast.error( mapped || detail || t("mcp.error.saveFailed"), { duration: mapped || detail ? 6000 : 5000 }, ); // 继续抛出错误,让表单层可以给到直观反馈(避免被更高层遮挡) throw e; } }; const handleCloseForm = () => { setIsFormOpen(false); setEditingId(null); }; const serverEntries = useMemo( () => Object.entries(servers) as Array<[string, McpServer]>, [servers], ); const enabledCount = useMemo( () => serverEntries.filter(([_, server]) => server.enabled).length, [serverEntries], ); const panelTitle = appType === "claude" ? t("mcp.claudeTitle") : t("mcp.codexTitle"); return ( <>
{panelTitle}
{/* Info Section */}
{t("mcp.serverCount", { count: Object.keys(servers).length })} ·{" "} {t("mcp.enabledCount", { count: enabledCount })}
{/* Content - Scrollable */}
{loading ? (
{t("mcp.loading")}
) : ( (() => { const hasAny = serverEntries.length > 0; if (!hasAny) { return (

{t("mcp.empty")}

{t("mcp.emptyDescription")}

); } return (
{/* 已安装 */} {serverEntries.map(([id, server]) => ( ))} {/* 预设已移至"新增 MCP"面板中展示与套用 */}
); })() )}
{/* Footer */}
{/* Form Modal */} {isFormOpen && ( )} {/* Confirm Dialog */} {confirmDialog && ( setConfirmDialog(null)} /> )} ); }; export default McpPanel;