From d68032876290bd2acca9a3d227900ab71806a77b 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: Wed, 28 Jan 2026 14:13:48 +0800 Subject: [PATCH] Add config UI and persistence to builtin plugin Introduces a configuration UI schema and persistent config storage for the napcat-plugin-builtin. The plugin now loads and saves its configuration, supports dynamic prefix and reply toggling, and updates dependencies to napcat-types v0.0.6. --- .../napcat-onebot/network/plugin-manger.ts | 1 + packages/napcat-plugin-builtin/index.ts | 86 ++++++++++++++++++- packages/napcat-plugin-builtin/package.json | 2 +- packages/napcat-types/package.public.json | 2 +- .../napcat-webui-backend/src/api/Plugin.ts | 25 ++---- .../napcat-webui-backend/src/router/Plugin.ts | 2 - .../components/display_card/plugin_card.tsx | 22 +---- .../src/controllers/plugin_manager.ts | 4 +- .../src/pages/dashboard/plugin.tsx | 12 +-- pnpm-lock.yaml | 10 +-- 10 files changed, 102 insertions(+), 64 deletions(-) diff --git a/packages/napcat-onebot/network/plugin-manger.ts b/packages/napcat-onebot/network/plugin-manger.ts index 740831cb..72f03e10 100644 --- a/packages/napcat-onebot/network/plugin-manger.ts +++ b/packages/napcat-onebot/network/plugin-manger.ts @@ -107,6 +107,7 @@ export class OB11PluginMangerAdapter extends IOB11NetworkAdapter { private readonly configPath: string; private loadedPlugins: Map = new Map(); declare config: PluginConfig; + public NapCatConfig = NapCatConfig; override get isActive (): boolean { return this.isEnable && this.loadedPlugins.size > 0; diff --git a/packages/napcat-plugin-builtin/index.ts b/packages/napcat-plugin-builtin/index.ts index a717cadc..5ee62a4c 100644 --- a/packages/napcat-plugin-builtin/index.ts +++ b/packages/napcat-plugin-builtin/index.ts @@ -3,8 +3,31 @@ import { EventType } from 'napcat-types/napcat-onebot/event/index'; import type { PluginModule } from 'napcat-types/napcat-onebot/network/plugin-manger'; import type { OB11Message, OB11PostSendMsg } from 'napcat-types/napcat-onebot/types/index'; +import fs from 'fs'; +import type { PluginConfigSchema, OB11PluginMangerAdapter } from 'napcat-types/napcat-onebot/network/plugin-manger'; + let actions: ActionMap | undefined = undefined; let startTime: number = Date.now(); +let platformInstance: OB11PluginMangerAdapter | undefined = undefined; + +interface BuiltinPluginConfig { + prefix: string; + enableReply: boolean; + description: string; + theme?: string; + features?: string[]; + [key: string]: unknown; +} + +let currentConfig: BuiltinPluginConfig = { + prefix: '#napcat', + enableReply: true, + description: '这是一个内置插件的配置示例' +}; + +const PLUGIN_NAME = 'napcat-plugin-builtin'; + +export let plugin_config_ui: PluginConfigSchema = []; /** * 插件初始化 @@ -12,6 +35,61 @@ let startTime: number = Date.now(); const plugin_init: PluginModule['plugin_init'] = async (_core, _obContext, _actions, _instance) => { console.log('[Plugin: builtin] NapCat 内置插件已初始化'); actions = _actions; + platformInstance = _instance; + + if (_instance.NapCatConfig) { + const NapCatConfig = _instance.NapCatConfig; + plugin_config_ui = NapCatConfig.combine( + NapCatConfig.html('

👋 Welcome to NapCat Builtin Plugin

This is a demonstration of the plugin configuration interface.

'), + NapCatConfig.text('prefix', 'Command Prefix', '#napcat', 'The prefix to trigger the version info command'), + NapCatConfig.boolean('enableReply', 'Enable Reply', true, 'Switch to enable or disable the reply functionality'), + NapCatConfig.select('theme', 'Theme Selection', [ + { label: 'Light Mode', value: 'light' }, + { label: 'Dark Mode', value: 'dark' }, + { label: 'Auto', value: 'auto' } + ], 'light', 'Select a theme for the response (Demo purpose only)'), + NapCatConfig.multiSelect('features', 'Enabled Features', [ + { label: 'Version Info', value: 'version' }, + { label: 'Status Report', value: 'status' }, + { label: 'Debug Log', value: 'debug' } + ], ['version'], 'Select features to enable'), + NapCatConfig.text('description', 'Description', '这是一个内置插件的配置示例', 'A multi-line text area for notes') + ); + } + + // Try to load config + try { + if (platformInstance && platformInstance.getPluginConfigPath) { + const configPath = platformInstance.getPluginConfigPath(PLUGIN_NAME); + if (fs.existsSync(configPath)) { + const savedConfig = JSON.parse(fs.readFileSync(configPath, 'utf-8')); + Object.assign(currentConfig, savedConfig); + } + } + } catch (e) { + console.warn('[Plugin: builtin] Failed to load config', e); + } +}; + +export const plugin_get_config = async () => { + return currentConfig; +}; + +export const plugin_set_config = async (config: BuiltinPluginConfig) => { + currentConfig = config; + if (platformInstance && platformInstance.getPluginConfigPath) { + try { + const configPath = platformInstance.getPluginConfigPath(PLUGIN_NAME); + const configDir = configPath.substring(0, configPath.lastIndexOf(process.platform === 'win32' ? '\\' : '/')); + if (!fs.existsSync(configDir)) { + fs.mkdirSync(configDir, { recursive: true }); + } + fs.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf-8'); + } catch (e) { + console.error('[Plugin: builtin] Failed to save config', e); + throw e; + } + } }; /** @@ -19,7 +97,13 @@ const plugin_init: PluginModule['plugin_init'] = async (_core, _obContext, _acti * 当收到包含 #napcat 的消息时,回复版本信息 */ const plugin_onmessage: PluginModule['plugin_onmessage'] = async (adapter, _core, _obCtx, event, _actions, instance) => { - if (event.post_type !== EventType.MESSAGE || !event.raw_message.startsWith('#napcat')) { + // Use config logic + const prefix = currentConfig.prefix || '#napcat'; + if (currentConfig.enableReply === false) { + return; + } + + if (event.post_type !== EventType.MESSAGE || !event.raw_message.startsWith(prefix)) { return; } diff --git a/packages/napcat-plugin-builtin/package.json b/packages/napcat-plugin-builtin/package.json index c3006c5c..d695b0a7 100644 --- a/packages/napcat-plugin-builtin/package.json +++ b/packages/napcat-plugin-builtin/package.json @@ -6,7 +6,7 @@ "description": "NapCat 内置插件", "author": "NapNeko", "dependencies": { - "napcat-types": "0.0.5" + "napcat-types": "0.0.6" }, "devDependencies": { "@types/node": "^22.0.1" diff --git a/packages/napcat-types/package.public.json b/packages/napcat-types/package.public.json index fa2b6af2..93899a5c 100644 --- a/packages/napcat-types/package.public.json +++ b/packages/napcat-types/package.public.json @@ -1,6 +1,6 @@ { "name": "napcat-types", - "version": "0.0.5", + "version": "0.0.6", "private": false, "type": "module", "types": "./napcat-types/index.d.ts", diff --git a/packages/napcat-webui-backend/src/api/Plugin.ts b/packages/napcat-webui-backend/src/api/Plugin.ts index ceb2ae3e..798ace47 100644 --- a/packages/napcat-webui-backend/src/api/Plugin.ts +++ b/packages/napcat-webui-backend/src/api/Plugin.ts @@ -118,24 +118,7 @@ export const GetPluginListHandler: RequestHandler = async (_req, res) => { return sendSuccess(res, { plugins: allPlugins, pluginManagerNotFound: false }); }; -export const ReloadPluginHandler: RequestHandler = async (req, res) => { - const { name } = req.body; - // Note: we should probably accept ID or Name. But ReloadPlugin uses valid loaded name. - // Let's stick to name for now, but be aware of ambiguity. - if (!name) return sendError(res, 'Plugin Name is required'); - - const pluginManager = getPluginManager(); - if (!pluginManager) { - return sendError(res, '插件管理器未加载,请检查 plugins 目录是否存在'); - } - - const success = await pluginManager.reloadPlugin(name); - if (success) { - return sendSuccess(res, { message: 'Reloaded successfully' }); - } else { - return sendError(res, 'Failed to reload plugin'); - } -}; +// ReloadPluginHandler removed export const SetPluginStatusHandler: RequestHandler = async (req, res) => { const { enable, filename } = req.body; @@ -291,7 +274,11 @@ export const SetPluginConfigHandler: RequestHandler = async (req, res) => { fs.mkdirSync(configDir, { recursive: true }); } fs.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf-8'); - return sendSuccess(res, { message: 'Config saved' }); + + // Auto-Reload plugin to apply changes + await pluginManager.reloadPlugin(name); + + return sendSuccess(res, { message: 'Config saved and plugin reloaded' }); } catch (e: any) { return sendError(res, 'Error saving config: ' + e.message); } diff --git a/packages/napcat-webui-backend/src/router/Plugin.ts b/packages/napcat-webui-backend/src/router/Plugin.ts index 17b80d64..c175cfa9 100644 --- a/packages/napcat-webui-backend/src/router/Plugin.ts +++ b/packages/napcat-webui-backend/src/router/Plugin.ts @@ -1,7 +1,6 @@ import { Router } from 'express'; import { GetPluginListHandler, - ReloadPluginHandler, SetPluginStatusHandler, UninstallPluginHandler, GetPluginConfigHandler, @@ -17,7 +16,6 @@ import { const router: Router = Router(); router.get('/List', GetPluginListHandler); -router.post('/Reload', ReloadPluginHandler); router.post('/SetStatus', SetPluginStatusHandler); router.post('/Uninstall', UninstallPluginHandler); router.get('/Config', GetPluginConfigHandler); diff --git a/packages/napcat-webui-frontend/src/components/display_card/plugin_card.tsx b/packages/napcat-webui-frontend/src/components/display_card/plugin_card.tsx index 4c6a2de0..bf2f4498 100644 --- a/packages/napcat-webui-frontend/src/components/display_card/plugin_card.tsx +++ b/packages/napcat-webui-frontend/src/components/display_card/plugin_card.tsx @@ -3,14 +3,13 @@ import { Switch } from '@heroui/switch'; import { Chip } from '@heroui/chip'; import { useState } from 'react'; -import { MdDeleteForever, MdPublishedWithChanges, MdSettings } from 'react-icons/md'; +import { MdDeleteForever, MdSettings } from 'react-icons/md'; import DisplayCardContainer from './container'; import { PluginItem } from '@/controllers/plugin_manager'; export interface PluginDisplayCardProps { data: PluginItem; - onReload: () => Promise; onToggleStatus: () => Promise; onUninstall: () => Promise; onConfig?: () => void; @@ -19,7 +18,6 @@ export interface PluginDisplayCardProps { const PluginDisplayCard: React.FC = ({ data, - onReload, onToggleStatus, onUninstall, onConfig, @@ -34,11 +32,6 @@ const PluginDisplayCard: React.FC = ({ onToggleStatus().finally(() => setProcessing(false)); }; - const handleReload = () => { - setProcessing(true); - onReload().finally(() => setProcessing(false)); - }; - const handleUninstall = () => { setProcessing(true); onUninstall().finally(() => setProcessing(false)); @@ -50,19 +43,6 @@ const PluginDisplayCard: React.FC = ({ action={
- -