'use client'; import PluginInstalledComponent, { PluginInstalledComponentRef, } from '@/app/home/plugins/plugin-installed/PluginInstalledComponent'; import MarketPage from '@/app/home/plugins/plugin-market/PluginMarketComponent'; // import PluginSortDialog from '@/app/home/plugins/plugin-sort/PluginSortDialog'; import PluginMarketComponent from '@/app/home/plugins/plugin-market/PluginMarketComponent'; import MCPComponent, { MCPComponentRef, } from '@/app/home/plugins/mcp/MCPComponent'; import MCPMarketComponent from '@/app/home/plugins/mcp-market/MCPMarketComponent'; import styles from './plugins.module.css'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; import { Button } from '@/components/ui/button'; import { PlusIcon, ChevronDownIcon, UploadIcon, StoreIcon, Download, Power, } from 'lucide-react'; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from '@/components/ui/dropdown-menu'; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, } from '@/components/ui/dialog'; import { Input } from '@/components/ui/input'; import React, { useState, useRef, useCallback, useEffect, use } from 'react'; import { httpClient } from '@/app/infra/http/HttpClient'; import { toast } from 'sonner'; import { useTranslation } from 'react-i18next'; import { PluginV4 } from '@/app/infra/entities/plugin'; import { systemInfo } from '@/app/infra/http/HttpClient'; import { ApiRespPluginSystemStatus } from '@/app/infra/entities/api'; import { set } from 'lodash'; import { passiveEventSupported } from '@tanstack/react-table'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@radix-ui/react-select'; import { useForm } from 'react-hook-form'; import { zodResolver } from '@hookform/resolvers/zod'; import { number, z } from 'zod'; import { DialogDescription } from '@radix-ui/react-dialog'; import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage, } from '@/components/ui/form'; enum PluginInstallStatus { WAIT_INPUT = 'wait_input', ASK_CONFIRM = 'ask_confirm', INSTALLING = 'installing', ERROR = 'error', } export default function PluginConfigPage( { editMode = false, initMCPId, onFormSubmit, onFormCancel, onMcpDeleted, }: { editMode?: boolean; initMCPId?: string; onFormSubmit?: () => void; onFormCancel?: () => void; onMcpDeleted?: () => void; } = {} ) { const { t } = useTranslation(); const [activeTab, setActiveTab] = useState('installed'); const [modalOpen, setModalOpen] = useState(false); // const [sortModalOpen, setSortModalOpen] = useState(false); const [installSource, setInstallSource] = useState('local'); const [installInfo, setInstallInfo] = useState>({}); // eslint-disable-line @typescript-eslint/no-explicit-any const [sortModalOpen, setSortModalOpen] = useState(false); // const [mcpModalOpen, setMcpModalOpen] = useState(false); const [mcpMarketInstallModalOpen, setMcpMarketInstallModalOpen] = useState(false); const [mcpSSEModalOpen, setMcpSSEModalOpen] = useState(false); const [pluginInstallStatus, setPluginInstallStatus] = useState(PluginInstallStatus.WAIT_INPUT); const [installError, setInstallError] = useState(null); const [mcpInstallError, setMcpInstallError] = useState(null); const [githubURL, setGithubURL] = useState(''); const [isDragOver, setIsDragOver] = useState(false); const [pluginSystemStatus, setPluginSystemStatus] = useState(null); const [statusLoading, setStatusLoading] = useState(true); const fileInputRef = useRef(null); const addExtraArg = () => { setExtraArgs([...extraArgs, { key: '', type: 'string', value: '' }]); }; const getExtraArgSchema = (t: (key: string) => string) => z .object({ key: z.string().min(1, { message: t('models.keyNameRequired') }), type: z.enum(['string', 'number', 'boolean']), value: z.string(), }) .superRefine((data, ctx) => { if (data.type === 'number' && isNaN(Number(data.value))) { ctx.addIssue({ code: z.ZodIssueCode.custom, message: t('models.mustBeValidNumber'), path: ['value'], }); } if ( data.type === 'boolean' && data.value !== 'true' && data.value !== 'false' ) { ctx.addIssue({ code: z.ZodIssueCode.custom, message: t('models.mustBeTrueOrFalse'), path: ['value'], }); } }); const removeExtraArg = (index: number) => { const newArgs = extraArgs.filter((_, i) => i !== index); setExtraArgs(newArgs); form.setValue('extra_args', newArgs); }; const getFormSchema = (t: (key: string) => string) => z.object({ name: z.string().min(1, { message: t('mcp.nameRequired') }), timeout: z.number().min(30, { message: t('mcp.timeoutMin30') }), ssereadtimeout: z.number().min(300, { message: t('mcp.sseTimeoutMin300') }), url: z.string().min(1, { message: t('mcp.requestURLRequired') }), extra_args: z.array(getExtraArgSchema(t)).optional(), }); const formSchema = getFormSchema(t); const form = useForm>({ resolver: zodResolver(formSchema), defaultValues: { name: '', url: '', timeout: 30, ssereadtimeout: 300, extra_args: [], }, }); const [extraArgs, setExtraArgs] = useState< { key: string; type: 'string' | 'number' | 'boolean'; value: string }[] >([]); const updateExtraArg = ( index: number, field: 'key' | 'type' | 'value', value: string, ) => { const newArgs = [...extraArgs]; newArgs[index] = { ...newArgs[index], [field]: value, }; setExtraArgs(newArgs); form.setValue('extra_args', newArgs); }; const [showDeleteConfirmModal, setShowDeleteConfirmModal] = useState(false); useEffect(() => { const fetchPluginSystemStatus = async () => { try { setStatusLoading(true); const status = await httpClient.getPluginSystemStatus(); setPluginSystemStatus(status); } catch (error) { console.error('Failed to fetch plugin system status:', error); toast.error(t('plugins.failedToGetStatus')); } finally { setStatusLoading(false); } }; fetchPluginSystemStatus(); }, [t]); function watchTask(taskId: number) { let alreadySuccess = false; console.log('taskId:', taskId); // 每秒拉取一次任务状态 const interval = setInterval(() => { httpClient.getAsyncTask(taskId).then((resp) => { console.log('task status:', resp); if (resp.runtime.done) { clearInterval(interval); if (resp.runtime.exception) { setInstallError(resp.runtime.exception); setPluginInstallStatus(PluginInstallStatus.ERROR); } else { // success if (!alreadySuccess) { toast.success(t('plugins.installSuccess')); alreadySuccess = true; } setGithubURL(''); setModalOpen(false); pluginInstalledRef.current?.refreshPluginList(); } } }); }, 1000); } const [mcpGithubURL, setMcpGithubURL] = useState(''); const [mcpSSEURL, setMcpSSEURL] = useState(''); const [mcpSSEConfig, setMcpSSEConfig] = useState | null>(null); const [mcpInstallConfig, setMcpInstallConfig] = useState | null>(null); const pluginInstalledRef = useRef(null); const mcpComponentRef = useRef(null); const [mcpTesting, setMcpTesting] = useState(false); // 强制清理 body 样式以修复 Dialog 关闭后点击失效的问题 useEffect(() => { console.log('[Dialog Debug] States:', { mcpSSEModalOpen, modalOpen, showDeleteConfirmModal }); if (!mcpSSEModalOpen && !modalOpen && !showDeleteConfirmModal) { console.log('[Dialog Debug] All dialogs closed, cleaning up body styles...'); console.log('[Dialog Debug] Before cleanup - body.style.pointerEvents:', document.body.style.pointerEvents); console.log('[Dialog Debug] Before cleanup - body.style.overflow:', document.body.style.overflow); const cleanup = () => { // 强制移除 body 上可能残留的样式 document.body.style.removeProperty('pointer-events'); document.body.style.removeProperty('overflow'); // 如果 removeProperty 不起作用,强制设置为空字符串 if (document.body.style.pointerEvents === 'none') { document.body.style.pointerEvents = ''; } if (document.body.style.overflow === 'hidden') { document.body.style.overflow = ''; } console.log('[Dialog Debug] After cleanup - body.style.pointerEvents:', document.body.style.pointerEvents); console.log('[Dialog Debug] After cleanup - body.style.overflow:', document.body.style.overflow); // 检查计算后的样式 const computedStyle = window.getComputedStyle(document.body); console.log('[Dialog Debug] Computed pointerEvents:', computedStyle.pointerEvents); }; // 多次清理以确保覆盖 Radix 的设置 cleanup(); const timer1 = setTimeout(cleanup, 0); const timer2 = setTimeout(cleanup, 50); const timer3 = setTimeout(cleanup, 100); const timer4 = setTimeout(cleanup, 200); const timer5 = setTimeout(cleanup, 300); return () => { clearTimeout(timer1); clearTimeout(timer2); clearTimeout(timer3); clearTimeout(timer4); clearTimeout(timer5); }; } }, [mcpSSEModalOpen, modalOpen, showDeleteConfirmModal]); // 额外的全局清理:定期检查并清理 useEffect(() => { const interval = setInterval(() => { if (!mcpSSEModalOpen && !modalOpen && !showDeleteConfirmModal) { if (document.body.style.pointerEvents === 'none') { console.log('[Global Cleanup] Found stale pointer-events, cleaning...'); document.body.style.removeProperty('pointer-events'); document.body.style.pointerEvents = ''; } } }, 500); return () => clearInterval(interval); }, [mcpSSEModalOpen, modalOpen, showDeleteConfirmModal]); // MutationObserver:监视 body 的 style 变化 useEffect(() => { const observer = new MutationObserver((mutations) => { mutations.forEach((mutation) => { if (mutation.type === 'attributes' && mutation.attributeName === 'style') { if (!mcpSSEModalOpen && !modalOpen && !showDeleteConfirmModal) { if (document.body.style.pointerEvents === 'none') { console.log('[MutationObserver] Detected pointer-events being set to none, reverting...'); document.body.style.removeProperty('pointer-events'); document.body.style.pointerEvents = ''; } } } }); }); observer.observe(document.body, { attributes: true, attributeFilter: ['style'], }); return () => observer.disconnect(); }, [mcpSSEModalOpen, modalOpen, showDeleteConfirmModal]); function handleModalConfirm() { installPlugin(installSource, installInfo as Record); // eslint-disable-line @typescript-eslint/no-explicit-any } function installPlugin( installSource: string, installInfo: Record, // eslint-disable-line @typescript-eslint/no-explicit-any ) { setPluginInstallStatus(PluginInstallStatus.INSTALLING); if (installSource === 'github') { httpClient .installPluginFromGithub(installInfo.url) .then((resp) => { const taskId = resp.task_id; watchTask(taskId); }) .catch((err) => { console.log('error when install plugin:', err); setInstallError(err.message); setPluginInstallStatus(PluginInstallStatus.ERROR); }); } else if (installSource === 'local') { httpClient .installPluginFromLocal(installInfo.file) .then((resp) => { const taskId = resp.task_id; watchTask(taskId); }) .catch((err) => { console.log('error when install plugin:', err); setInstallError(err.message); setPluginInstallStatus(PluginInstallStatus.ERROR); }); } else if (installSource === 'marketplace') { httpClient .installPluginFromMarketplace( installInfo.plugin_author, installInfo.plugin_name, installInfo.plugin_version, ) .then((resp) => { const taskId = resp.task_id; watchTask(taskId); }); } } function deleteMCPServer() { } async function handleFormSubmit(value: z.infer) { const extraArgsObj: Record = {}; value.extra_args?.forEach( (arg: { key: string; type: string; value: string }) => { if (arg.type === 'number') { extraArgsObj[arg.key] = Number(arg.value); } else if (arg.type === 'boolean') { extraArgsObj[arg.key] = arg.value === 'true'; } else { extraArgsObj[arg.key] = arg.value; } }, ); try { // 构造符合 MCPServerConfig 类型的数据 const serverConfig = { name: value.name, mode: 'sse' as const, enable: true, url: value.url, headers: extraArgsObj as Record, timeout: value.timeout, }; await httpClient.createMCPServer(serverConfig); toast.success(t('mcp.createSuccess')); // 只有在异步操作成功后才关闭对话框 setMcpSSEModalOpen(false); // 重置表单 form.reset(); setExtraArgs([]); // 调用回调通知父组件刷新 onFormSubmit?.(); } catch (error) { console.error('Failed to create MCP server:', error); toast.error(t('mcp.createFailed')); } } function testMcp() { setMcpTesting(true); const extraArgsObj: Record = {}; form .getValues('extra_args') ?.forEach((arg: { key: string; type: string; value: string }) => { if (arg.type === 'number') { extraArgsObj[arg.key] = Number(arg.value); } else if (arg.type === 'boolean') { extraArgsObj[arg.key] = arg.value === 'true'; } else { extraArgsObj[arg.key] = arg.value; } }); httpClient.testMCPServer( form.getValues('name'), ).then((res) => { console.log(res); toast.success(t('models.testSuccess')); }) .catch(() => { toast.error(t('models.testError')); }) .finally(() => { setMcpTesting(false); }); } const validateFileType = (file: File): boolean => { const allowedExtensions = ['.lbpkg', '.zip']; const fileName = file.name.toLowerCase(); return allowedExtensions.some((ext) => fileName.endsWith(ext)); }; const uploadPluginFile = useCallback( async (file: File) => { if (!pluginSystemStatus?.is_enable || !pluginSystemStatus?.is_connected) { toast.error(t('plugins.pluginSystemNotReady')); return; } if (!validateFileType(file)) { toast.error(t('plugins.unsupportedFileType')); return; } setModalOpen(true); setPluginInstallStatus(PluginInstallStatus.INSTALLING); setInstallError(null); installPlugin('local', { file }); }, [t, pluginSystemStatus], ); const handleFileSelect = useCallback(() => { if (fileInputRef.current) { fileInputRef.current.click(); } }, []); const handleFileChange = useCallback( (event: React.ChangeEvent) => { const file = event.target.files?.[0]; if (file) { uploadPluginFile(file); } // 清空input值,以便可以重复选择同一个文件 event.target.value = ''; }, [uploadPluginFile], ); const isPluginSystemReady = pluginSystemStatus?.is_enable && pluginSystemStatus?.is_connected; const handleDragOver = useCallback( (event: React.DragEvent) => { event.preventDefault(); if (isPluginSystemReady) { setIsDragOver(true); } }, [isPluginSystemReady], ); const handleDragLeave = useCallback((event: React.DragEvent) => { event.preventDefault(); setIsDragOver(false); }, []); const handleDrop = useCallback( (event: React.DragEvent) => { event.preventDefault(); setIsDragOver(false); if (!isPluginSystemReady) { toast.error(t('plugins.pluginSystemNotReady')); return; } const files = Array.from(event.dataTransfer.files); if (files.length > 0) { uploadPluginFile(files[0]); } }, [uploadPluginFile, isPluginSystemReady, t], ); // 插件系统未启用的状态显示 const renderPluginDisabledState = () => (

{t('plugins.systemDisabled')}

{t('plugins.systemDisabledDesc')}

); // 插件系统连接异常的状态显示 const renderPluginConnectionErrorState = () => (

{t('plugins.connectionError')}

{t('plugins.connectionErrorDesc')}

); // 加载状态显示 const renderLoadingState = () => (

{t('plugins.loadingStatus')}

); // 根据状态返回不同的内容 if (statusLoading) { return renderLoadingState(); } if (!pluginSystemStatus?.is_enable) { return renderPluginDisabledState(); } if (!pluginSystemStatus?.is_connected) { return renderPluginConnectionErrorState(); } return (
{t('plugins.installed')} {systemInfo.enable_marketplace && ( {t('plugins.marketplace')} )} {t('mcp.marketplace')}
{/* */} {activeTab === 'mcp-market' ? ( <> {/* { setActiveTab('mcp-market'); setMcpMarketInstallModalOpen(true); setMcpInstallStatus(PluginInstallStatus.WAIT_INPUT); setMcpInstallError(null); setMcpGithubURL(''); }} > {t('mcp.installFromGithub')} */} { setActiveTab('mcp-market'); setMcpSSEModalOpen(true); }} > {t('mcp.createServer')} ) : ( <> {t('plugins.uploadLocal')} {systemInfo.enable_marketplace && ( { setActiveTab('market'); }} > {t('plugins.marketplace')} )} )}
{ setInstallSource('marketplace'); setInstallInfo({ plugin_author: plugin.author, plugin_name: plugin.name, plugin_version: plugin.latest_version, }); setPluginInstallStatus(PluginInstallStatus.ASK_CONFIRM); setModalOpen(true); }} /> { setMcpGithubURL(githubURL); setMcpMarketInstallModalOpen(true); // setMcpInstallStatus(PluginInstallStatus.WAIT_INPUT); setMcpInstallError(null); }} />
{t('plugins.installPlugin')} {pluginInstallStatus === PluginInstallStatus.WAIT_INPUT && (

{t('plugins.onlySupportGithub')}

setGithubURL(e.target.value)} className="mb-4" />
)} {pluginInstallStatus === PluginInstallStatus.ASK_CONFIRM && (

{t('plugins.askConfirm', { name: installInfo.plugin_name, version: installInfo.plugin_version, })}

)} {pluginInstallStatus === PluginInstallStatus.INSTALLING && (

{t('plugins.installing')}

)} {pluginInstallStatus === PluginInstallStatus.ERROR && (

{t('plugins.installFailed')}

{installError}

)} {(pluginInstallStatus === PluginInstallStatus.WAIT_INPUT || pluginInstallStatus === PluginInstallStatus.ASK_CONFIRM) && ( <> )} {pluginInstallStatus === PluginInstallStatus.ERROR && ( )}
{/* 拖拽提示覆盖层 */} {isDragOver && (

{t('plugins.dragToUpload')}

)}
{t('plugins.confirmDeleteTitle')} {t('plugins.deleteConfirmation')} {t('mcp.createServer')}
( {t('mcp.name')} )} /> ( {t('mcp.url')} )} /> ( {t('mcp.timeout')} ) } /> ( {t('mcp.ssereadtimeout')} ) } /> {t('models.extraParameters')}
{extraArgs.map((arg, index) => (
updateExtraArg(index, 'key', e.target.value) } /> updateExtraArg(index, 'value', e.target.value) } />
))}
{t('llm.extraParametersDescription')}
{editMode && ( )}
); }