mirror of
https://github.com/NapNeko/NapCatQQ.git
synced 2026-01-17 13:50:36 +00:00
Add plugin management to WebUI backend and frontend
Implemented backend API and router for plugin management (list, reload, enable/disable, uninstall) and exposed corresponding frontend controller and dashboard page. Updated navigation and site config to include plugin management. Refactored plugin manager adapter for public methods and improved plugin metadata handling.
This commit is contained in:
parent
a7fd70ac3a
commit
ed1872a349
@ -49,10 +49,11 @@ import {
|
||||
OneBotConfigSchema,
|
||||
} from './config/config';
|
||||
import { OB11Message } from './types';
|
||||
import { existsSync } from 'node:fs';
|
||||
import { IOB11NetworkAdapter } from '@/napcat-onebot/network/adapter';
|
||||
import { OB11HttpSSEServerAdapter } from './network/http-server-sse';
|
||||
import { OB11PluginMangerAdapter } from './network/plugin-manger';
|
||||
import { existsSync } from 'node:fs';
|
||||
|
||||
import { proxiedListenerOf } from '@/napcat-core/helper/proxy-handler';
|
||||
import { OneBotFileApi } from './api/file';
|
||||
|
||||
@ -160,6 +161,7 @@ export class NapCatOneBot11Adapter {
|
||||
// this.networkManager.registerAdapter(
|
||||
// new OB11PluginAdapter('myPlugin', this.core, this,this.actions)
|
||||
// );
|
||||
// 检查插件目录是否存在,不存在则不加载插件管理器
|
||||
if (existsSync(this.context.pathWrapper.pluginPath)) {
|
||||
this.context.logger.log('[Plugins] 插件目录存在,开始加载插件');
|
||||
this.networkManager.registerAdapter(
|
||||
|
||||
@ -11,6 +11,8 @@ export interface PluginPackageJson {
|
||||
name?: string;
|
||||
version?: string;
|
||||
main?: string;
|
||||
description?: string;
|
||||
author?: string;
|
||||
}
|
||||
|
||||
export interface PluginModule<T extends OB11EmitEventContent = OB11EmitEventContent> {
|
||||
@ -85,7 +87,7 @@ export class OB11PluginMangerAdapter extends IOB11NetworkAdapter<PluginConfig> {
|
||||
/**
|
||||
* 加载单文件插件 (.mjs, .js)
|
||||
*/
|
||||
private async loadFilePlugin (filename: string): Promise<void> {
|
||||
public async loadFilePlugin (filename: string): Promise<void> {
|
||||
// 只处理支持的文件类型
|
||||
if (!this.isSupportedFile(filename)) {
|
||||
return;
|
||||
@ -117,7 +119,7 @@ export class OB11PluginMangerAdapter extends IOB11NetworkAdapter<PluginConfig> {
|
||||
/**
|
||||
* 加载目录插件
|
||||
*/
|
||||
private async loadDirectoryPlugin (dirname: string): Promise<void> {
|
||||
public async loadDirectoryPlugin (dirname: string): Promise<void> {
|
||||
const pluginDir = path.join(this.pluginPath, dirname);
|
||||
|
||||
try {
|
||||
@ -255,6 +257,14 @@ export class OB11PluginMangerAdapter extends IOB11NetworkAdapter<PluginConfig> {
|
||||
this.logger.log(`[Plugin Adapter] Unloaded plugin: ${pluginName}`);
|
||||
}
|
||||
|
||||
public async unregisterPlugin (pluginName: string): Promise<void> {
|
||||
return this.unloadPlugin(pluginName);
|
||||
}
|
||||
|
||||
public getPluginPath (): string {
|
||||
return this.pluginPath;
|
||||
}
|
||||
|
||||
async onEvent<T extends OB11EmitEventContent> (event: T) {
|
||||
if (!this.isEnable) {
|
||||
return;
|
||||
|
||||
@ -81,4 +81,4 @@ async function sendMessage (event: OB11Message, message: string, adapter: string
|
||||
}
|
||||
}
|
||||
|
||||
export { plugin_init, plugin_onmessage, actions };
|
||||
export { plugin_init, plugin_onmessage };
|
||||
|
||||
199
packages/napcat-webui-backend/src/api/Plugin.ts
Normal file
199
packages/napcat-webui-backend/src/api/Plugin.ts
Normal file
@ -0,0 +1,199 @@
|
||||
import { RequestHandler } from 'express';
|
||||
import { WebUiDataRuntime } from '@/napcat-webui-backend/src/helper/Data';
|
||||
import { sendError, sendSuccess } from '@/napcat-webui-backend/src/utils/response';
|
||||
import { NapCatOneBot11Adapter } from '@/napcat-onebot/index';
|
||||
import { OB11PluginMangerAdapter } from '@/napcat-onebot/network/plugin-manger';
|
||||
import path from 'path';
|
||||
import fs from 'fs';
|
||||
|
||||
// Helper to get the plugin manager adapter
|
||||
const getPluginManager = (): OB11PluginMangerAdapter | null => {
|
||||
const ob11 = WebUiDataRuntime.getOneBotContext() as NapCatOneBot11Adapter;
|
||||
if (!ob11) return null;
|
||||
return ob11.networkManager.findSomeAdapter('plugin_manager') as OB11PluginMangerAdapter;
|
||||
};
|
||||
|
||||
export const GetPluginListHandler: RequestHandler = async (_req, res) => {
|
||||
const pluginManager = getPluginManager();
|
||||
if (!pluginManager) {
|
||||
return sendError(res, 'Plugin Manager not found');
|
||||
}
|
||||
|
||||
const loadedPlugins = pluginManager.getLoadedPlugins().map(p => ({
|
||||
name: p.name,
|
||||
version: p.version || '0.0.0',
|
||||
description: p.packageJson?.description || '',
|
||||
author: p.packageJson?.author || '',
|
||||
status: 'active',
|
||||
}));
|
||||
|
||||
// Find disabled plugins (those with .disabled extension)
|
||||
const pluginPath = pluginManager.getPluginPath();
|
||||
const disabledPlugins: any[] = [];
|
||||
if (fs.existsSync(pluginPath)) {
|
||||
const items = fs.readdirSync(pluginPath, { withFileTypes: true });
|
||||
for (const item of items) {
|
||||
if (item.name.endsWith('.disabled')) {
|
||||
const originalName = item.name.replace(/\.disabled$/, '');
|
||||
const isDirectory = item.isDirectory();
|
||||
let version = '0.0.0';
|
||||
let description = '';
|
||||
let author = '';
|
||||
let name = originalName;
|
||||
|
||||
try {
|
||||
if (isDirectory) {
|
||||
const packageJsonPath = path.join(pluginPath, item.name, 'package.json');
|
||||
if (fs.existsSync(packageJsonPath)) {
|
||||
const pkg = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
|
||||
version = pkg.version || version;
|
||||
description = pkg.description || description;
|
||||
author = pkg.author || author;
|
||||
name = pkg.name || name;
|
||||
}
|
||||
}
|
||||
} catch (e) { }
|
||||
|
||||
disabledPlugins.push({
|
||||
name: name,
|
||||
version,
|
||||
description,
|
||||
author,
|
||||
status: 'disabled',
|
||||
filename: item.name // Store real filename for operations
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return sendSuccess(res, [...loadedPlugins, ...disabledPlugins]);
|
||||
};
|
||||
|
||||
export const ReloadPluginHandler: RequestHandler = async (req, res) => {
|
||||
const { name } = req.body;
|
||||
if (!name) return sendError(res, 'Plugin Name is required');
|
||||
|
||||
const pluginManager = getPluginManager();
|
||||
if (!pluginManager) {
|
||||
return sendError(res, 'Plugin Manager not found');
|
||||
}
|
||||
|
||||
const success = await pluginManager.reloadPlugin(name);
|
||||
if (success) {
|
||||
return sendSuccess(res, { message: 'Reloaded successfully' });
|
||||
} else {
|
||||
return sendError(res, 'Failed to reload plugin');
|
||||
}
|
||||
};
|
||||
|
||||
export const SetPluginStatusHandler: RequestHandler = async (req, res) => {
|
||||
const { name, enable, filename } = req.body; // filename required for enabling
|
||||
if (!name) return sendError(res, 'Plugin Name is required');
|
||||
|
||||
const pluginManager = getPluginManager();
|
||||
if (!pluginManager) {
|
||||
return sendError(res, 'Plugin Manager not found');
|
||||
}
|
||||
|
||||
const pluginPath = pluginManager.getPluginPath();
|
||||
|
||||
if (enable) {
|
||||
// Enable: Rename back from .disabled
|
||||
// We need the filename since we can't guess if it was a dir or file easily without scanning or passing it
|
||||
if (!filename) return sendError(res, 'Filename is required to enable');
|
||||
|
||||
const disabledPath = path.join(pluginPath, filename);
|
||||
const enabledPath = path.join(pluginPath, filename.replace(/\.disabled$/, ''));
|
||||
|
||||
if (!fs.existsSync(disabledPath)) {
|
||||
return sendError(res, 'Disabled plugin not found');
|
||||
}
|
||||
|
||||
try {
|
||||
fs.renameSync(disabledPath, enabledPath);
|
||||
// After rename, we need to load it
|
||||
const isDirectory = fs.statSync(enabledPath).isDirectory();
|
||||
if (isDirectory) {
|
||||
await pluginManager.loadDirectoryPlugin(path.basename(enabledPath));
|
||||
} else {
|
||||
await pluginManager.loadFilePlugin(path.basename(enabledPath));
|
||||
}
|
||||
return sendSuccess(res, { message: 'Enabled successfully' });
|
||||
} catch (e: any) {
|
||||
return sendError(res, 'Failed to enable: ' + e.message);
|
||||
}
|
||||
|
||||
} else {
|
||||
// Disable: Unload and rename to .disabled
|
||||
const plugin = pluginManager.getPluginInfo(name);
|
||||
if (!plugin) return sendError(res, 'Plugin not found/loaded');
|
||||
|
||||
try {
|
||||
await pluginManager.unregisterPlugin(name);
|
||||
// Determine the file/dir key in the fs
|
||||
|
||||
// plugin.pluginPath is the directory for dir plugins, and the directory containing the file for file plugins??
|
||||
// Let's check LoadedPlugin definition again.
|
||||
// pluginPath: this.pluginPath (for file plugins), pluginDir (for dir plugins)
|
||||
|
||||
// Wait, for file plugins: pluginPath = this.pluginPath, entryPath = filePath
|
||||
// For dir plugins: pluginPath = pluginDir, entryPath = join(pluginDir, entryFile)
|
||||
|
||||
let fsPath = '';
|
||||
// We need to rename the whole thing that constitutes the plugin.
|
||||
if (plugin.pluginPath === pluginManager.getPluginPath()) {
|
||||
// It's a file plugin
|
||||
fsPath = plugin.entryPath;
|
||||
} else {
|
||||
// It's a directory plugin
|
||||
fsPath = plugin.pluginPath;
|
||||
}
|
||||
|
||||
const disabledPath = fsPath + '.disabled';
|
||||
fs.renameSync(fsPath, disabledPath);
|
||||
return sendSuccess(res, { message: 'Disabled successfully' });
|
||||
} catch (e: any) {
|
||||
return sendError(res, 'Failed to disable: ' + e.message);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const UninstallPluginHandler: RequestHandler = async (req, res) => {
|
||||
const { name, filename } = req.body;
|
||||
// If it's loaded, we use name. If it's disabled, we might use filename.
|
||||
|
||||
const pluginManager = getPluginManager();
|
||||
if (!pluginManager) {
|
||||
return sendError(res, 'Plugin Manager not found');
|
||||
}
|
||||
|
||||
// Check if loaded
|
||||
const plugin = pluginManager.getPluginInfo(name);
|
||||
let fsPath = '';
|
||||
|
||||
if (plugin) {
|
||||
// Active plugin
|
||||
await pluginManager.unregisterPlugin(name);
|
||||
if (plugin.pluginPath === pluginManager.getPluginPath()) {
|
||||
fsPath = plugin.entryPath;
|
||||
} else {
|
||||
fsPath = plugin.pluginPath;
|
||||
}
|
||||
} else {
|
||||
// Disabled or not loaded
|
||||
if (filename) {
|
||||
fsPath = path.join(pluginManager.getPluginPath(), filename);
|
||||
} else {
|
||||
return sendError(res, 'Plugin not found, provide filename if disabled');
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
if (fs.existsSync(fsPath)) {
|
||||
fs.rmSync(fsPath, { recursive: true, force: true });
|
||||
}
|
||||
return sendSuccess(res, { message: 'Uninstalled successfully' });
|
||||
} catch (e: any) {
|
||||
return sendError(res, 'Failed to uninstall: ' + e.message);
|
||||
}
|
||||
};
|
||||
11
packages/napcat-webui-backend/src/router/Plugin.ts
Normal file
11
packages/napcat-webui-backend/src/router/Plugin.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import { Router } from 'express';
|
||||
import { GetPluginListHandler, ReloadPluginHandler, SetPluginStatusHandler, UninstallPluginHandler } from '@/napcat-webui-backend/src/api/Plugin';
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.get('/List', GetPluginListHandler);
|
||||
router.post('/Reload', ReloadPluginHandler);
|
||||
router.post('/SetStatus', SetPluginStatusHandler);
|
||||
router.post('/Uninstall', UninstallPluginHandler);
|
||||
|
||||
export { router as PluginRouter };
|
||||
@ -17,6 +17,7 @@ import { WebUIConfigRouter } from './WebUIConfig';
|
||||
import { UpdateNapCatRouter } from './UpdateNapCat';
|
||||
import DebugRouter from '@/napcat-webui-backend/src/api/Debug';
|
||||
import { ProcessRouter } from './Process';
|
||||
import { PluginRouter } from './Plugin';
|
||||
|
||||
const router = Router();
|
||||
|
||||
@ -47,5 +48,7 @@ router.use('/UpdateNapCat', UpdateNapCatRouter);
|
||||
router.use('/Debug', DebugRouter);
|
||||
// router:进程管理相关路由
|
||||
router.use('/Process', ProcessRouter);
|
||||
// router:插件管理相关路由
|
||||
router.use('/Plugin', PluginRouter);
|
||||
|
||||
export { router as ALLRouter };
|
||||
|
||||
@ -25,6 +25,7 @@ const FileManagerPage = lazy(() => import('@/pages/dashboard/file_manager'));
|
||||
const LogsPage = lazy(() => import('@/pages/dashboard/logs'));
|
||||
const NetworkPage = lazy(() => import('@/pages/dashboard/network'));
|
||||
const TerminalPage = lazy(() => import('@/pages/dashboard/terminal'));
|
||||
const PluginPage = lazy(() => import('@/pages/dashboard/plugin'));
|
||||
|
||||
function App () {
|
||||
return (
|
||||
@ -42,7 +43,7 @@ function App () {
|
||||
);
|
||||
}
|
||||
|
||||
function AuthChecker ({ children }: { children: React.ReactNode }) {
|
||||
function AuthChecker ({ children }: { children: React.ReactNode; }) {
|
||||
const { isAuth } = useAuth();
|
||||
const navigate = useNavigate();
|
||||
|
||||
@ -76,6 +77,7 @@ function AppRoutes () {
|
||||
</Route>
|
||||
<Route path='file_manager' element={<FileManagerPage />} />
|
||||
<Route path='terminal' element={<TerminalPage />} />
|
||||
<Route path='plugins' element={<PluginPage />} />
|
||||
<Route path='about' element={<AboutPage />} />
|
||||
</Route>
|
||||
<Route path='/qq_login' element={<QQLoginPage />} />
|
||||
|
||||
@ -0,0 +1,116 @@
|
||||
import { Button } from '@heroui/button';
|
||||
import { Switch } from '@heroui/switch';
|
||||
import clsx from 'clsx';
|
||||
import { useState } from 'react';
|
||||
import { MdDeleteForever, MdPublishedWithChanges } from 'react-icons/md';
|
||||
|
||||
import DisplayCardContainer from './container';
|
||||
import { PluginItem } from '@/controllers/plugin_manager';
|
||||
|
||||
export interface PluginDisplayCardProps {
|
||||
data: PluginItem;
|
||||
onReload: () => Promise<void>;
|
||||
onToggleStatus: () => Promise<void>;
|
||||
onUninstall: () => Promise<void>;
|
||||
}
|
||||
|
||||
const PluginDisplayCard: React.FC<PluginDisplayCardProps> = ({
|
||||
data,
|
||||
onReload,
|
||||
onToggleStatus,
|
||||
onUninstall,
|
||||
}) => {
|
||||
const { name, version, author, description, status } = data;
|
||||
const isEnabled = status === 'active';
|
||||
const [processing, setProcessing] = useState(false);
|
||||
|
||||
const handleToggle = () => {
|
||||
setProcessing(true);
|
||||
onToggleStatus().finally(() => setProcessing(false));
|
||||
};
|
||||
|
||||
const handleReload = () => {
|
||||
setProcessing(true);
|
||||
onReload().finally(() => setProcessing(false));
|
||||
};
|
||||
|
||||
const handleUninstall = () => {
|
||||
setProcessing(true);
|
||||
onUninstall().finally(() => setProcessing(false));
|
||||
};
|
||||
|
||||
return (
|
||||
<DisplayCardContainer
|
||||
className='w-full max-w-[420px]'
|
||||
action={
|
||||
<div className='flex gap-2 w-full'>
|
||||
<Button
|
||||
fullWidth
|
||||
radius='full'
|
||||
size='sm'
|
||||
variant='flat'
|
||||
className='flex-1 bg-default-100 dark:bg-default-50 text-default-600 font-medium hover:bg-primary/20 hover:text-primary transition-colors'
|
||||
startContent={<MdPublishedWithChanges size={16} />}
|
||||
onPress={handleReload}
|
||||
isDisabled={!isEnabled || processing}
|
||||
>
|
||||
重载
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
fullWidth
|
||||
radius='full'
|
||||
size='sm'
|
||||
variant='flat'
|
||||
className='flex-1 bg-default-100 dark:bg-default-50 text-default-600 font-medium hover:bg-danger/20 hover:text-danger transition-colors'
|
||||
startContent={<MdDeleteForever size={16} />}
|
||||
onPress={handleUninstall}
|
||||
isDisabled={processing}
|
||||
>
|
||||
卸载
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
enableSwitch={
|
||||
<Switch
|
||||
isDisabled={processing}
|
||||
isSelected={isEnabled}
|
||||
onChange={handleToggle}
|
||||
classNames={{
|
||||
wrapper: 'group-data-[selected=true]:bg-primary-400',
|
||||
}}
|
||||
/>
|
||||
}
|
||||
title={name}
|
||||
>
|
||||
<div className='grid grid-cols-2 gap-3'>
|
||||
<div className='flex flex-col gap-1 p-3 bg-default-100/50 dark:bg-white/10 rounded-xl border border-transparent hover:border-default-200 transition-colors'>
|
||||
<span className='text-xs text-default-500 dark:text-white/50 font-medium tracking-wide'>
|
||||
版本
|
||||
</span>
|
||||
<div className='text-sm font-medium text-default-700 dark:text-white/90 truncate'>
|
||||
{version}
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex flex-col gap-1 p-3 bg-default-100/50 dark:bg-white/10 rounded-xl border border-transparent hover:border-default-200 transition-colors'>
|
||||
<span className='text-xs text-default-500 dark:text-white/50 font-medium tracking-wide'>
|
||||
作者
|
||||
</span>
|
||||
<div className='text-sm font-medium text-default-700 dark:text-white/90 truncate'>
|
||||
{author || '未知'}
|
||||
</div>
|
||||
</div>
|
||||
<div className='col-span-2 flex flex-col gap-1 p-3 bg-default-100/50 dark:bg-white/10 rounded-xl border border-transparent hover:border-default-200 transition-colors'>
|
||||
<span className='text-xs text-default-500 dark:text-white/50 font-medium tracking-wide'>
|
||||
描述
|
||||
</span>
|
||||
<div className='text-sm font-medium text-default-700 dark:text-white/90 break-words line-clamp-2'>
|
||||
{description || '暂无描述'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</DisplayCardContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export default PluginDisplayCard;
|
||||
@ -8,6 +8,7 @@ import {
|
||||
LuSignal,
|
||||
LuTerminal,
|
||||
LuZap,
|
||||
LuPackage,
|
||||
} from 'react-icons/lu';
|
||||
|
||||
export type SiteConfig = typeof siteConfig;
|
||||
@ -59,6 +60,11 @@ export const siteConfig = {
|
||||
icon: <LuFolderOpen className='w-5 h-5' />,
|
||||
href: '/file_manager',
|
||||
},
|
||||
{
|
||||
label: '插件管理',
|
||||
icon: <LuPackage className='w-5 h-5' />,
|
||||
href: '/plugins',
|
||||
},
|
||||
{
|
||||
label: '系统终端',
|
||||
icon: <LuTerminal className='w-5 h-5' />,
|
||||
|
||||
@ -0,0 +1,35 @@
|
||||
import { serverRequest } from '@/utils/request';
|
||||
|
||||
export interface PluginItem {
|
||||
name: string;
|
||||
version: string;
|
||||
description: string;
|
||||
author: string;
|
||||
status: 'active' | 'disabled';
|
||||
filename?: string;
|
||||
}
|
||||
|
||||
export interface ServerResponse<T> {
|
||||
code: number;
|
||||
message: string;
|
||||
data: T;
|
||||
}
|
||||
|
||||
export default class PluginManager {
|
||||
public static async getPluginList () {
|
||||
const { data } = await serverRequest.get<ServerResponse<PluginItem[]>>('/Plugin/List');
|
||||
return data.data;
|
||||
}
|
||||
|
||||
public static async reloadPlugin (name: string) {
|
||||
await serverRequest.post<ServerResponse<void>>('/Plugin/Reload', { name });
|
||||
}
|
||||
|
||||
public static async setPluginStatus (name: string, enable: boolean, filename?: string) {
|
||||
await serverRequest.post<ServerResponse<void>>('/Plugin/SetStatus', { name, enable, filename });
|
||||
}
|
||||
|
||||
public static async uninstallPlugin (name: string, filename?: string) {
|
||||
await serverRequest.post<ServerResponse<void>>('/Plugin/Uninstall', { name, filename });
|
||||
}
|
||||
}
|
||||
115
packages/napcat-webui-frontend/src/pages/dashboard/plugin.tsx
Normal file
115
packages/napcat-webui-frontend/src/pages/dashboard/plugin.tsx
Normal file
@ -0,0 +1,115 @@
|
||||
import { Button } from '@heroui/button';
|
||||
import { useEffect, useState } from 'react';
|
||||
import toast from 'react-hot-toast';
|
||||
import { IoMdRefresh } from 'react-icons/io';
|
||||
|
||||
import PageLoading from '@/components/page_loading';
|
||||
import PluginDisplayCard from '@/components/display_card/plugin_card';
|
||||
import PluginManager, { PluginItem } from '@/controllers/plugin_manager';
|
||||
import useDialog from '@/hooks/use-dialog';
|
||||
|
||||
export default function PluginPage () {
|
||||
const [plugins, setPlugins] = useState<PluginItem[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const dialog = useDialog();
|
||||
|
||||
const loadPlugins = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const data = await PluginManager.getPluginList();
|
||||
setPlugins(data);
|
||||
} catch (e: any) {
|
||||
toast.error(e.message);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
loadPlugins();
|
||||
}, []);
|
||||
|
||||
const handleReload = async (name: string) => {
|
||||
const loadingToast = toast.loading('重载中...');
|
||||
try {
|
||||
await PluginManager.reloadPlugin(name);
|
||||
toast.success('重载成功', { id: loadingToast });
|
||||
loadPlugins();
|
||||
} catch (e: any) {
|
||||
toast.error(e.message, { id: loadingToast });
|
||||
}
|
||||
};
|
||||
|
||||
const handleToggle = async (plugin: PluginItem) => {
|
||||
const isEnable = plugin.status !== 'active';
|
||||
const actionText = isEnable ? '启用' : '禁用';
|
||||
const loadingToast = toast.loading(`${actionText}中...`);
|
||||
try {
|
||||
await PluginManager.setPluginStatus(plugin.name, isEnable, plugin.filename);
|
||||
toast.success(`${actionText}成功`, { id: loadingToast });
|
||||
loadPlugins();
|
||||
} catch (e: any) {
|
||||
toast.error(e.message, { id: loadingToast });
|
||||
}
|
||||
};
|
||||
|
||||
const handleUninstall = async (plugin: PluginItem) => {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
dialog.confirm({
|
||||
title: '卸载插件',
|
||||
content: `确定要卸载插件「${plugin.name}」吗? 此操作不可恢复。`,
|
||||
onConfirm: async () => {
|
||||
const loadingToast = toast.loading('卸载中...');
|
||||
try {
|
||||
await PluginManager.uninstallPlugin(plugin.name, plugin.filename);
|
||||
toast.success('卸载成功', { id: loadingToast });
|
||||
loadPlugins();
|
||||
resolve();
|
||||
} catch (e: any) {
|
||||
toast.error(e.message, { id: loadingToast });
|
||||
reject(e);
|
||||
}
|
||||
},
|
||||
onCancel: () => {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<title>插件管理 - NapCat WebUI</title>
|
||||
<div className='p-2 md:p-4 relative'>
|
||||
<PageLoading loading={loading} />
|
||||
<div className='flex mb-6 items-center gap-4'>
|
||||
<h1 className="text-2xl font-bold">插件管理</h1>
|
||||
<Button
|
||||
isIconOnly
|
||||
className="bg-default-100/50 hover:bg-default-200/50 text-default-700 backdrop-blur-md"
|
||||
radius='full'
|
||||
onPress={loadPlugins}
|
||||
>
|
||||
<IoMdRefresh size={24} />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{plugins.length === 0 ? (
|
||||
<div className="text-default-400">暂时没有安装插件</div>
|
||||
) : (
|
||||
<div className='grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5 justify-start items-stretch gap-x-2 gap-y-4'>
|
||||
{plugins.map(plugin => (
|
||||
<PluginDisplayCard
|
||||
key={plugin.name}
|
||||
data={plugin}
|
||||
onReload={() => handleReload(plugin.name)}
|
||||
onToggleStatus={() => handleToggle(plugin)}
|
||||
onUninstall={() => handleUninstall(plugin)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user