mirror of
https://github.com/NapNeko/NapCatQQ.git
synced 2026-02-04 06:31:13 +00:00
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.
This commit is contained in:
parent
40409a3841
commit
05d27e86ce
@ -1,4 +1,8 @@
|
|||||||
import { Router } from 'express';
|
import { Router } from 'express';
|
||||||
|
import multer from 'multer';
|
||||||
|
import path from 'path';
|
||||||
|
import fs from 'fs';
|
||||||
|
import os from 'os';
|
||||||
import {
|
import {
|
||||||
GetPluginListHandler,
|
GetPluginListHandler,
|
||||||
SetPluginStatusHandler,
|
SetPluginStatusHandler,
|
||||||
@ -7,7 +11,8 @@ import {
|
|||||||
SetPluginConfigHandler,
|
SetPluginConfigHandler,
|
||||||
RegisterPluginManagerHandler,
|
RegisterPluginManagerHandler,
|
||||||
PluginConfigSSEHandler,
|
PluginConfigSSEHandler,
|
||||||
PluginConfigChangeHandler
|
PluginConfigChangeHandler,
|
||||||
|
ImportLocalPluginHandler
|
||||||
} from '@/napcat-webui-backend/src/api/Plugin';
|
} from '@/napcat-webui-backend/src/api/Plugin';
|
||||||
import {
|
import {
|
||||||
GetPluginStoreListHandler,
|
GetPluginStoreListHandler,
|
||||||
@ -16,6 +21,39 @@ import {
|
|||||||
InstallPluginFromStoreSSEHandler
|
InstallPluginFromStoreSSEHandler
|
||||||
} from '@/napcat-webui-backend/src/api/PluginStore';
|
} 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();
|
const router: Router = Router();
|
||||||
|
|
||||||
router.get('/List', GetPluginListHandler);
|
router.get('/List', GetPluginListHandler);
|
||||||
@ -26,6 +64,7 @@ router.post('/Config', SetPluginConfigHandler);
|
|||||||
router.get('/Config/SSE', PluginConfigSSEHandler);
|
router.get('/Config/SSE', PluginConfigSSEHandler);
|
||||||
router.post('/Config/Change', PluginConfigChangeHandler);
|
router.post('/Config/Change', PluginConfigChangeHandler);
|
||||||
router.post('/RegisterManager', RegisterPluginManagerHandler);
|
router.post('/RegisterManager', RegisterPluginManagerHandler);
|
||||||
|
router.post('/Import', upload.single('plugin'), ImportLocalPluginHandler);
|
||||||
|
|
||||||
// 插件商店相关路由
|
// 插件商店相关路由
|
||||||
router.get('/Store/List', GetPluginStoreListHandler);
|
router.get('/Store/List', GetPluginStoreListHandler);
|
||||||
|
|||||||
@ -96,6 +96,27 @@ export default class PluginManager {
|
|||||||
await serverRequest.post<ServerResponse<void>>('/Plugin/Uninstall', { id, cleanData });
|
await serverRequest.post<ServerResponse<void>>('/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<ServerResponse<{ message: string; pluginId: string; installPath: string; }>>(
|
||||||
|
'/Plugin/Import',
|
||||||
|
formData,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'multipart/form-data',
|
||||||
|
},
|
||||||
|
timeout: 60000, // 60秒超时
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return data.data;
|
||||||
|
}
|
||||||
|
|
||||||
// ==================== 插件商店 ====================
|
// ==================== 插件商店 ====================
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -1,7 +1,8 @@
|
|||||||
import { Button } from '@heroui/button';
|
import { Button } from '@heroui/button';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState, useRef } from 'react';
|
||||||
import toast from 'react-hot-toast';
|
import toast from 'react-hot-toast';
|
||||||
import { IoMdRefresh } from 'react-icons/io';
|
import { IoMdRefresh } from 'react-icons/io';
|
||||||
|
import { FiUpload } from 'react-icons/fi';
|
||||||
import { useDisclosure } from '@heroui/modal';
|
import { useDisclosure } from '@heroui/modal';
|
||||||
|
|
||||||
import PageLoading from '@/components/page_loading';
|
import PageLoading from '@/components/page_loading';
|
||||||
@ -18,6 +19,7 @@ export default function PluginPage () {
|
|||||||
|
|
||||||
const { isOpen, onOpen, onOpenChange } = useDisclosure();
|
const { isOpen, onOpen, onOpenChange } = useDisclosure();
|
||||||
const [currentPluginId, setCurrentPluginId] = useState<string>('');
|
const [currentPluginId, setCurrentPluginId] = useState<string>('');
|
||||||
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
const loadPlugins = async () => {
|
const loadPlugins = async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
@ -106,6 +108,61 @@ export default function PluginPage () {
|
|||||||
onOpen();
|
onOpen();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleImportClick = () => {
|
||||||
|
if (pluginManagerNotFound) {
|
||||||
|
dialog.confirm({
|
||||||
|
title: '插件管理器未加载',
|
||||||
|
content: (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<p className="text-sm text-default-600">
|
||||||
|
插件管理器尚未加载,无法导入插件。
|
||||||
|
</p>
|
||||||
|
<p className="text-sm text-default-600">
|
||||||
|
是否立即注册插件管理器?
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
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<HTMLInputElement>) => {
|
||||||
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<title>插件管理 - NapCat WebUI</title>
|
<title>插件管理 - NapCat WebUI</title>
|
||||||
@ -127,6 +184,21 @@ export default function PluginPage () {
|
|||||||
>
|
>
|
||||||
<IoMdRefresh size={24} />
|
<IoMdRefresh size={24} />
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button
|
||||||
|
className="bg-primary-100/50 hover:bg-primary-200/50 text-primary-700 backdrop-blur-md"
|
||||||
|
radius='full'
|
||||||
|
startContent={<FiUpload size={18} />}
|
||||||
|
onPress={handleImportClick}
|
||||||
|
>
|
||||||
|
导入插件
|
||||||
|
</Button>
|
||||||
|
<input
|
||||||
|
ref={fileInputRef}
|
||||||
|
type="file"
|
||||||
|
accept=".zip"
|
||||||
|
className="hidden"
|
||||||
|
onChange={handleFileChange}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{pluginManagerNotFound ? (
|
{pluginManagerNotFound ? (
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user