From 05d27e86ce30799d0014e5f8edd349b77e60f30c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=89=8B=E7=93=9C=E4=B8=80=E5=8D=81=E9=9B=AA?= Date: Fri, 30 Jan 2026 11:55:53 +0800 Subject: [PATCH] Add local plugin import functionality Implemented backend API and frontend UI for importing local plugin zip files. The backend now supports file uploads via a new /Plugin/Import endpoint using multer, and the frontend provides a button to upload and import plugins directly from the dashboard. Prompt to register plugin manager if not loaded Renames plugin_develop.ts to plugin-develop.ts for consistency. Updates the plugin import handler to prompt the user to register the plugin manager if it is not loaded, improving user experience and error handling. --- .../{plugin_develop.ts => plugin-develop.ts} | 0 .../napcat-webui-backend/src/router/Plugin.ts | 41 +++++++++- .../src/controllers/plugin_manager.ts | 21 ++++++ .../src/pages/dashboard/plugin.tsx | 74 ++++++++++++++++++- 4 files changed, 134 insertions(+), 2 deletions(-) rename packages/napcat-onebot/network/{plugin_develop.ts => plugin-develop.ts} (100%) diff --git a/packages/napcat-onebot/network/plugin_develop.ts b/packages/napcat-onebot/network/plugin-develop.ts similarity index 100% rename from packages/napcat-onebot/network/plugin_develop.ts rename to packages/napcat-onebot/network/plugin-develop.ts diff --git a/packages/napcat-webui-backend/src/router/Plugin.ts b/packages/napcat-webui-backend/src/router/Plugin.ts index fafe4aaa..cf9824cd 100644 --- a/packages/napcat-webui-backend/src/router/Plugin.ts +++ b/packages/napcat-webui-backend/src/router/Plugin.ts @@ -1,4 +1,8 @@ import { Router } from 'express'; +import multer from 'multer'; +import path from 'path'; +import fs from 'fs'; +import os from 'os'; import { GetPluginListHandler, SetPluginStatusHandler, @@ -7,7 +11,8 @@ import { SetPluginConfigHandler, RegisterPluginManagerHandler, PluginConfigSSEHandler, - PluginConfigChangeHandler + PluginConfigChangeHandler, + ImportLocalPluginHandler } from '@/napcat-webui-backend/src/api/Plugin'; import { GetPluginStoreListHandler, @@ -16,6 +21,39 @@ import { InstallPluginFromStoreSSEHandler } from '@/napcat-webui-backend/src/api/PluginStore'; +// 配置 multer 用于文件上传 +const uploadDir = path.join(os.tmpdir(), 'napcat-plugin-uploads'); +if (!fs.existsSync(uploadDir)) { + fs.mkdirSync(uploadDir, { recursive: true }); +} + +const storage = multer.diskStorage({ + destination: (_req, _file, cb) => { + cb(null, uploadDir); + }, + filename: (_req, file, cb) => { + const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9); + cb(null, uniqueSuffix + '-' + file.originalname); + } +}); + +const upload = multer({ + storage, + limits: { + fileSize: 50 * 1024 * 1024, // 50MB 限制 + }, + fileFilter: (_req, file, cb) => { + // 只允许 .zip 文件 + if (file.mimetype === 'application/zip' || + file.mimetype === 'application/x-zip-compressed' || + file.originalname.endsWith('.zip')) { + cb(null, true); + } else { + cb(new Error('Only .zip files are allowed')); + } + } +}); + const router: Router = Router(); router.get('/List', GetPluginListHandler); @@ -26,6 +64,7 @@ router.post('/Config', SetPluginConfigHandler); router.get('/Config/SSE', PluginConfigSSEHandler); router.post('/Config/Change', PluginConfigChangeHandler); router.post('/RegisterManager', RegisterPluginManagerHandler); +router.post('/Import', upload.single('plugin'), ImportLocalPluginHandler); // 插件商店相关路由 router.get('/Store/List', GetPluginStoreListHandler); diff --git a/packages/napcat-webui-frontend/src/controllers/plugin_manager.ts b/packages/napcat-webui-frontend/src/controllers/plugin_manager.ts index 70eb3256..f5249e10 100644 --- a/packages/napcat-webui-frontend/src/controllers/plugin_manager.ts +++ b/packages/napcat-webui-frontend/src/controllers/plugin_manager.ts @@ -96,6 +96,27 @@ export default class PluginManager { await serverRequest.post>('/Plugin/Uninstall', { id, cleanData }); } + /** + * 导入本地插件包 + * @param file 插件 zip 文件 + */ + public static async importLocalPlugin (file: File): Promise<{ message: string; pluginId: string; installPath: string; }> { + const formData = new FormData(); + formData.append('plugin', file); + + const { data } = await serverRequest.post>( + '/Plugin/Import', + formData, + { + headers: { + 'Content-Type': 'multipart/form-data', + }, + timeout: 60000, // 60秒超时 + } + ); + return data.data; + } + // ==================== 插件商店 ==================== /** diff --git a/packages/napcat-webui-frontend/src/pages/dashboard/plugin.tsx b/packages/napcat-webui-frontend/src/pages/dashboard/plugin.tsx index 96f24f52..1d40ea4d 100644 --- a/packages/napcat-webui-frontend/src/pages/dashboard/plugin.tsx +++ b/packages/napcat-webui-frontend/src/pages/dashboard/plugin.tsx @@ -1,7 +1,8 @@ import { Button } from '@heroui/button'; -import { useEffect, useState } from 'react'; +import { useEffect, useState, useRef } from 'react'; import toast from 'react-hot-toast'; import { IoMdRefresh } from 'react-icons/io'; +import { FiUpload } from 'react-icons/fi'; import { useDisclosure } from '@heroui/modal'; import PageLoading from '@/components/page_loading'; @@ -18,6 +19,7 @@ export default function PluginPage () { const { isOpen, onOpen, onOpenChange } = useDisclosure(); const [currentPluginId, setCurrentPluginId] = useState(''); + const fileInputRef = useRef(null); const loadPlugins = async () => { setLoading(true); @@ -106,6 +108,61 @@ export default function PluginPage () { onOpen(); }; + const handleImportClick = () => { + if (pluginManagerNotFound) { + dialog.confirm({ + title: '插件管理器未加载', + content: ( +
+

+ 插件管理器尚未加载,无法导入插件。 +

+

+ 是否立即注册插件管理器? +

+
+ ), + confirmText: '注册插件管理器', + cancelText: '取消', + onConfirm: async () => { + try { + await PluginManager.registerPluginManager(); + toast.success('插件管理器注册成功'); + setPluginManagerNotFound(false); + // 注册成功后打开文件选择器 + fileInputRef.current?.click(); + } catch (e: any) { + toast.error('注册失败: ' + e.message); + } + }, + }); + return; + } + fileInputRef.current?.click(); + }; + + const handleFileChange = async (e: React.ChangeEvent) => { + const file = e.target.files?.[0]; + if (!file) return; + + // 重置 input,允许重复选择同一文件 + e.target.value = ''; + + if (!file.name.endsWith('.zip')) { + toast.error('请选择 .zip 格式的插件包'); + return; + } + + const loadingToast = toast.loading('正在导入插件...'); + try { + const result = await PluginManager.importLocalPlugin(file); + toast.success(result.message, { id: loadingToast }); + loadPlugins(); + } catch (err: any) { + toast.error(err.message || '导入失败', { id: loadingToast }); + } + }; + return ( <> 插件管理 - NapCat WebUI @@ -127,6 +184,21 @@ export default function PluginPage () { > + + {pluginManagerNotFound ? (