From 4b693bf6e22093acbcadd414eb1403eff12c4a8a 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:38:11 +0800 Subject: [PATCH] Refactor plugin manager to support only directory plugins Removed support for single-file plugins in OB11PluginMangerAdapter, simplifying plugin identification to use directory names as unique IDs. Updated related logic in the backend API to align with this change, ensuring consistent plugin management and status handling. --- .../napcat-onebot/network/plugin-manger.ts | 139 +++++++----------- packages/napcat-plugin-builtin/index.ts | 75 +++++----- packages/napcat-plugin-builtin/package.json | 2 +- packages/napcat-plugin/README.md | 3 - packages/napcat-plugin/index.ts | 22 --- packages/napcat-plugin/package.json | 16 -- packages/napcat-plugin/tsconfig.json | 11 -- packages/napcat-plugin/vite.config.ts | 30 ---- packages/napcat-types/package.public.json | 2 +- .../napcat-webui-backend/src/api/Plugin.ts | 10 +- pnpm-lock.yaml | 10 +- 11 files changed, 107 insertions(+), 213 deletions(-) delete mode 100644 packages/napcat-plugin/README.md delete mode 100644 packages/napcat-plugin/index.ts delete mode 100644 packages/napcat-plugin/package.json delete mode 100644 packages/napcat-plugin/tsconfig.json delete mode 100644 packages/napcat-plugin/vite.config.ts diff --git a/packages/napcat-onebot/network/plugin-manger.ts b/packages/napcat-onebot/network/plugin-manger.ts index 123016b7..a83d4ee7 100644 --- a/packages/napcat-onebot/network/plugin-manger.ts +++ b/packages/napcat-onebot/network/plugin-manger.ts @@ -54,39 +54,36 @@ export class NapCatConfig { export type PluginConfigSchema = PluginConfigItem[]; +export interface NapCatPluginContext { + core: NapCatCore; + oneBot: NapCatOneBot11Adapter; + actions: ActionMap; + pluginName: string; + pluginPath: string; + configPath: string; + dataPath: string; + NapCatConfig: typeof NapCatConfig; + adapterName: string; + pluginManager: OB11PluginMangerAdapter; +} + export interface PluginModule { - plugin_init: ( - core: NapCatCore, - obContext: NapCatOneBot11Adapter, - actions: ActionMap, - instance: OB11PluginMangerAdapter - ) => void | Promise; + plugin_init: (ctx: NapCatPluginContext) => void | Promise; plugin_onmessage?: ( - adapter: string, - core: NapCatCore, - obCtx: NapCatOneBot11Adapter, + ctx: NapCatPluginContext, event: OB11Message, - actions: ActionMap, - instance: OB11PluginMangerAdapter ) => void | Promise; plugin_onevent?: ( - adapter: string, - core: NapCatCore, - obCtx: NapCatOneBot11Adapter, + ctx: NapCatPluginContext, event: T, - actions: ActionMap, - instance: OB11PluginMangerAdapter ) => void | Promise; plugin_cleanup?: ( - core: NapCatCore, - obContext: NapCatOneBot11Adapter, - actions: ActionMap, - instance: OB11PluginMangerAdapter + ctx: NapCatPluginContext ) => void | Promise; plugin_config_schema?: PluginConfigSchema; plugin_config_ui?: PluginConfigSchema; - plugin_get_config?: () => any | Promise; - plugin_set_config?: (config: any) => void | Promise; + plugin_get_config?: (ctx: NapCatPluginContext) => any | Promise; + plugin_set_config?: (ctx: NapCatPluginContext, config: any) => void | Promise; } export interface LoadedPlugin { @@ -96,6 +93,7 @@ export interface LoadedPlugin { entryPath: string; packageJson?: PluginPackageJson; module: PluginModule; + context: NapCatPluginContext; // Store context } export interface PluginStatusConfig { @@ -106,6 +104,8 @@ export class OB11PluginMangerAdapter extends IOB11NetworkAdapter { private readonly pluginPath: string; private readonly configPath: string; private loadedPlugins: Map = new Map(); + // Track failed plugins: ID -> Error Message + private failedPlugins: Map = new Map(); declare config: PluginConfig; public NapCatConfig = NapCatConfig; @@ -251,6 +251,7 @@ export class OB11PluginMangerAdapter extends IOB11NetworkAdapter { entryPath, packageJson, module, + context: {} as NapCatPluginContext // Will be populated in registerPlugin }; await this.registerPlugin(plugin); @@ -308,6 +309,9 @@ export class OB11PluginMangerAdapter extends IOB11NetworkAdapter { return module && typeof module.plugin_init === 'function'; } + /** + * 注册插件 + */ /** * 注册插件 */ @@ -320,6 +324,26 @@ export class OB11PluginMangerAdapter extends IOB11NetworkAdapter { return; } + // Create Context + const id = plugin.name; // directory name as ID + const dataPath = path.join(this.pluginPath, id, 'data'); + const configPath = path.join(dataPath, 'config.json'); + + const context: NapCatPluginContext = { + core: this.core, + oneBot: this.obContext, + actions: this.actions, + pluginName: id, + pluginPath: plugin.pluginPath, + dataPath: dataPath, + configPath: configPath, + NapCatConfig: NapCatConfig, + adapterName: this.name, + pluginManager: this + }; + + plugin.context = context; // Store context on plugin object + this.loadedPlugins.set(plugin.name, plugin); this.logger.log( `[Plugin Adapter] Registered plugin: ${plugin.name}${plugin.version ? ` v${plugin.version}` : '' @@ -328,18 +352,16 @@ export class OB11PluginMangerAdapter extends IOB11NetworkAdapter { // 调用插件初始化方法(必须存在) try { - await plugin.module.plugin_init( - this.core, - this.obContext, - this.actions, - this - ); + await plugin.module.plugin_init(context); this.logger.log(`[Plugin Adapter] Initialized plugin: ${plugin.name}`); - } catch (error) { + } catch (error: any) { this.logger.logError( `[Plugin Adapter] Error initializing plugin ${plugin.name}:`, error ); + // Mark as failed + this.failedPlugins.set(plugin.name, error.message || 'Initialization failed'); + this.loadedPlugins.delete(plugin.name); } } @@ -355,12 +377,7 @@ export class OB11PluginMangerAdapter extends IOB11NetworkAdapter { // 调用插件清理方法 if (typeof plugin.module.plugin_cleanup === 'function') { try { - await plugin.module.plugin_cleanup( - this.core, - this.obContext, - this.actions, - this - ); + await plugin.module.plugin_cleanup(plugin.context); this.logger.log(`[Plugin Adapter] Cleaned up plugin: ${pluginName}`); } catch (error) { this.logger.logError( @@ -391,50 +408,17 @@ export class OB11PluginMangerAdapter extends IOB11NetworkAdapter { config[pluginName] = enable; this.savePluginConfig(config); - // If disabling, unload immediately if loaded if (!enable) { - // Note: pluginName passed here might be the package name or the filename/dirname - // But our registerPlugin uses plugin.name which comes from package.json or dirname. - // This mismatch is tricky. - // Ideally, we should use a consistent ID. - // Let's assume pluginName passed here effectively matches the ID used in loadedPlugins. - // But wait, loadDirectoryPlugin logic: name = packageJson.name || dirname. - // config key = dirname. - // If packageJson.name != dirname, we have a problem. - // To fix this properly: - // 1. We need to know which LoadedPlugin corresponds to the enabled/disabled item. - // 2. Or we iterate loadedPlugins and find match. - for (const [_, loaded] of this.loadedPlugins.entries()) { const dirOrFile = path.basename(loaded.pluginPath === this.pluginPath ? loaded.entryPath : loaded.pluginPath); const ext = path.extname(dirOrFile); - const simpleName = ext ? path.parse(dirOrFile).name : dirOrFile; // filename without ext - - // But wait, config key is the FILENAME (with ext for files?). - // In Scan loop: - // pluginName = path.parse(item.name).name (for file) - // pluginName = item.name (for dir) - // config[pluginName] check. - - // So if file is "test.js", pluginName is "test". Config key "test". - // If dir is "test-plugin", pluginName is "test-plugin". Config key "test-plugin". - - // loadedPlugin.name might be distinct. - // So we need to match loadedPlugin back to its fs source to unload it? - - // loadedPlugin.entryPath or pluginPath helps. - // If it's a file plugin: loaded.entryPath ends with pluginName + ext. - // If it's a dir plugin: loaded.pluginPath ends with pluginName. + const simpleName = ext ? path.parse(dirOrFile).name : dirOrFile; if (pluginName === simpleName) { this.unloadPlugin(loaded.name).catch(e => this.logger.logError('Error unloading', e)); } } } - // If enabling, we need to load it. - // But we can just rely on the API handler to call loadFile/DirectoryPlugin which now checks config. - // Wait, if I call loadFilePlugin("test.js") and config says enable=true, it loads. - // API handler needs to change to pass filename/dirname. } async onEvent (event: T) { @@ -442,7 +426,6 @@ export class OB11PluginMangerAdapter extends IOB11NetworkAdapter { return; } - // 遍历所有已加载的插件,调用它们的事件处理方法 try { await Promise.allSettled( Array.from(this.loadedPlugins.values()).map((plugin) => @@ -465,12 +448,8 @@ export class OB11PluginMangerAdapter extends IOB11NetworkAdapter { // 优先使用 plugin_onevent 方法 if (typeof plugin.module.plugin_onevent === 'function') { await plugin.module.plugin_onevent( - this.name, - this.core, - this.obContext, - event, - this.actions, - this + plugin.context, + event ); } @@ -480,12 +459,8 @@ export class OB11PluginMangerAdapter extends IOB11NetworkAdapter { typeof plugin.module.plugin_onmessage === 'function' ) { await plugin.module.plugin_onmessage( - this.name, - this.core, - this.obContext, - event as OB11Message, - this.actions, - this + plugin.context, + event as OB11Message ); } } catch (error) { diff --git a/packages/napcat-plugin-builtin/index.ts b/packages/napcat-plugin-builtin/index.ts index 5ee62a4c..0652a277 100644 --- a/packages/napcat-plugin-builtin/index.ts +++ b/packages/napcat-plugin-builtin/index.ts @@ -4,11 +4,11 @@ import type { PluginModule } from 'napcat-types/napcat-onebot/network/plugin-man 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'; +import path from 'path'; +import type { PluginConfigSchema } 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; @@ -25,62 +25,61 @@ let currentConfig: BuiltinPluginConfig = { description: '这是一个内置插件的配置示例' }; -const PLUGIN_NAME = 'napcat-plugin-builtin'; export let plugin_config_ui: PluginConfigSchema = []; /** * 插件初始化 */ -const plugin_init: PluginModule['plugin_init'] = async (_core, _obContext, _actions, _instance) => { +/** + * 插件初始化 + */ +const plugin_init: PluginModule['plugin_init'] = async (ctx) => { console.log('[Plugin: builtin] NapCat 内置插件已初始化'); - actions = _actions; - platformInstance = _instance; + actions = ctx.actions; + // platformInstance = _instance; // No longer valid or needed like this - 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') - ); - } + const NapCatConfig = ctx.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); - } + // Use ctx.configPath + if (fs.existsSync(ctx.configPath)) { + const savedConfig = JSON.parse(fs.readFileSync(ctx.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) => { +export const plugin_set_config = async (ctx: any, config: BuiltinPluginConfig) => { currentConfig = config; - if (platformInstance && platformInstance.getPluginConfigPath) { + if (ctx && ctx.configPath) { try { - const configPath = platformInstance.getPluginConfigPath(PLUGIN_NAME); - const configDir = configPath.substring(0, configPath.lastIndexOf(process.platform === 'win32' ? '\\' : '/')); + const configPath = ctx.configPath; + const configDir = path.dirname(configPath); if (!fs.existsSync(configDir)) { fs.mkdirSync(configDir, { recursive: true }); } @@ -96,7 +95,7 @@ export const plugin_set_config = async (config: BuiltinPluginConfig) => { * 消息处理 * 当收到包含 #napcat 的消息时,回复版本信息 */ -const plugin_onmessage: PluginModule['plugin_onmessage'] = async (adapter, _core, _obCtx, event, _actions, instance) => { +const plugin_onmessage: PluginModule['plugin_onmessage'] = async (_ctx, event) => { // Use config logic const prefix = currentConfig.prefix || '#napcat'; if (currentConfig.enableReply === false) { @@ -108,11 +107,11 @@ const plugin_onmessage: PluginModule['plugin_onmessage'] = async (adapter, _core } try { - const versionInfo = await getVersionInfo(adapter, instance.config); + const versionInfo = await getVersionInfo('ob11', {}); if (!versionInfo) return; const message = formatVersionMessage(versionInfo); - await sendMessage(event, message, adapter, instance.config); + await sendMessage(event, message, 'ob11', {}); console.log('[Plugin: builtin] 已回复版本信息'); } catch (error) { diff --git a/packages/napcat-plugin-builtin/package.json b/packages/napcat-plugin-builtin/package.json index d695b0a7..37b902f9 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.6" + "napcat-types": "0.0.8" }, "devDependencies": { "@types/node": "^22.0.1" diff --git a/packages/napcat-plugin/README.md b/packages/napcat-plugin/README.md deleted file mode 100644 index 18552ba8..00000000 --- a/packages/napcat-plugin/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# Example Plugin -## Install -安装只需要将dist产物 `index.mjs` 目录放入 napcat根目录下`plugins/example`,如果没有这个目录请创建。 \ No newline at end of file diff --git a/packages/napcat-plugin/index.ts b/packages/napcat-plugin/index.ts deleted file mode 100644 index e3c64029..00000000 --- a/packages/napcat-plugin/index.ts +++ /dev/null @@ -1,22 +0,0 @@ -import type { createActionMap } from 'napcat-types/dist/napcat-onebot/action/index.js'; -import { EventType } from 'napcat-types/dist/napcat-onebot/event/index.js'; -import type { PluginModule } from 'napcat-types/dist/napcat-onebot/network/plugin-manger'; - -/** - * 导入 napcat 包时候不使用 @/napcat...,直接使用 napcat... - * 因为 @/napcat... 会导致打包时包含整个 napcat 包,而不是只包含需要的部分 - */ - -// action 作为参数传递时请用这个 -let actionMap: ReturnType | undefined = undefined; - -const plugin_init: PluginModule['plugin_init'] = async (_core, _obContext, _actions, _instance) => { - console.log('[Plugin: example] 插件已初始化'); - actionMap = _actions; -}; -const plugin_onmessage: PluginModule['plugin_onmessage'] = async (adapter, _core, _obCtx, event, actions, instance) => { - if (event.post_type === EventType.MESSAGE && event.raw_message.includes('ping')) { - await actions.get('send_group_msg')?.handle({ group_id: String(event.group_id), message: 'pong' }, adapter, instance.config); - } -}; -export { plugin_init, plugin_onmessage, actionMap }; diff --git a/packages/napcat-plugin/package.json b/packages/napcat-plugin/package.json deleted file mode 100644 index e89cd6b0..00000000 --- a/packages/napcat-plugin/package.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "name": "napcat-plugin", - "version": "1.0.0", - "type": "module", - "main": "index.mjs", - "description": "一个高级的 NapCat 插件示例", - "dependencies": { - "napcat-types": "workspace:*" - }, - "devDependencies": { - "@types/node": "^22.0.1" - }, - "scripts": { - "build": "vite build" - } -} \ No newline at end of file diff --git a/packages/napcat-plugin/tsconfig.json b/packages/napcat-plugin/tsconfig.json deleted file mode 100644 index 48d3a03b..00000000 --- a/packages/napcat-plugin/tsconfig.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "extends": "../../tsconfig.base.json", - "include": [ - "*.ts", - "**/*.ts" - ], - "exclude": [ - "node_modules", - "dist" - ] -} \ No newline at end of file diff --git a/packages/napcat-plugin/vite.config.ts b/packages/napcat-plugin/vite.config.ts deleted file mode 100644 index 5a8de9cb..00000000 --- a/packages/napcat-plugin/vite.config.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { defineConfig } from 'vite'; -import { resolve } from 'path'; -import nodeResolve from '@rollup/plugin-node-resolve'; -import { builtinModules } from 'module'; - -const nodeModules = [...builtinModules, builtinModules.map((m) => `node:${m}`)].flat(); - -export default defineConfig({ - resolve: { - conditions: ['node', 'default'], - alias: { - '@/napcat-core': resolve(__dirname, '../napcat-core'), - '@': resolve(__dirname, '../'), - }, - }, - build: { - sourcemap: false, - target: 'esnext', - minify: false, - lib: { - entry: 'index.ts', - formats: ['es'], - fileName: () => 'index.mjs', - }, - rollupOptions: { - external: [...nodeModules], - }, - }, - plugins: [nodeResolve()], -}); diff --git a/packages/napcat-types/package.public.json b/packages/napcat-types/package.public.json index 93899a5c..ad583299 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.6", + "version": "0.0.8", "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 968bc54a..221dd703 100644 --- a/packages/napcat-webui-backend/src/api/Plugin.ts +++ b/packages/napcat-webui-backend/src/api/Plugin.ts @@ -226,12 +226,13 @@ export const GetPluginConfigHandler: RequestHandler = async (req, res) => { if (plugin.module.plugin_get_config) { try { - config = await plugin.module.plugin_get_config(); + config = await plugin.module.plugin_get_config(plugin.context); } catch (e) { } } else if (schema) { // Default behavior: read from default config path try { - const configPath = pluginManager.getPluginConfigPath(name); + // Use context configPath if available + const configPath = plugin.context?.configPath || pluginManager.getPluginConfigPath(name); if (fs.existsSync(configPath)) { config = JSON.parse(fs.readFileSync(configPath, 'utf-8')); } @@ -253,7 +254,7 @@ export const SetPluginConfigHandler: RequestHandler = async (req, res) => { if (plugin.module.plugin_set_config) { try { - await plugin.module.plugin_set_config(config); + await plugin.module.plugin_set_config(plugin.context, config); return sendSuccess(res, { message: 'Config updated' }); } catch (e: any) { return sendError(res, 'Error updating config: ' + e.message); @@ -261,7 +262,8 @@ export const SetPluginConfigHandler: RequestHandler = async (req, res) => { } else if (plugin.module.plugin_config_schema || plugin.module.plugin_config_ui) { // Default behavior: write to default config path try { - const configPath = pluginManager.getPluginConfigPath(name); + const configPath = plugin.context?.configPath || pluginManager.getPluginConfigPath(name); + const configDir = path.dirname(configPath); if (!fs.existsSync(configDir)) { fs.mkdirSync(configDir, { recursive: true }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c5b29048..78bd2bdb 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -223,8 +223,8 @@ importers: packages/napcat-plugin-builtin: dependencies: napcat-types: - specifier: 0.0.6 - version: 0.0.6 + specifier: 0.0.7 + version: 0.0.7 devDependencies: '@types/node': specifier: ^22.0.1 @@ -5448,8 +5448,8 @@ packages: engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true - napcat-types@0.0.6: - resolution: {integrity: sha512-KyIEr/uFC8w1bGF2Oyvk+2Kdr6ENklWK9bHwrGGbAKnUUJ4GRhsUYQdX/dDhhiZrLFWisYslQyLFD6530YtTlg==} + napcat-types@0.0.7: + resolution: {integrity: sha512-PIDaQ6YnTxxpC1yb+VIDsQDamV6Ry+fUhHUBCXUVSzj1outisf6jSKFr2O7c9wX9zdX6Pi6NaVECNRy0ob4dmg==} napcat.protobuf@1.1.4: resolution: {integrity: sha512-z7XtLSBJ/PxmYb0VD/w+eYr/X3LyGz+SZ2QejFTOczwt6zWNxy2yV1mTMTvJoc3BWkI3ESVFRxkuT6+pj1tb1Q==} @@ -12819,7 +12819,7 @@ snapshots: nanoid@3.3.11: {} - napcat-types@0.0.6: + napcat-types@0.0.7: dependencies: '@sinclair/typebox': 0.34.41 '@types/cors': 2.8.19