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.
This commit is contained in:
手瓜一十雪 2026-01-28 14:38:11 +08:00
parent 574c257591
commit 4b693bf6e2
11 changed files with 107 additions and 213 deletions

View File

@ -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<T extends OB11EmitEventContent = OB11EmitEventContent> {
plugin_init: (
core: NapCatCore,
obContext: NapCatOneBot11Adapter,
actions: ActionMap,
instance: OB11PluginMangerAdapter
) => void | Promise<void>;
plugin_init: (ctx: NapCatPluginContext) => void | Promise<void>;
plugin_onmessage?: (
adapter: string,
core: NapCatCore,
obCtx: NapCatOneBot11Adapter,
ctx: NapCatPluginContext,
event: OB11Message,
actions: ActionMap,
instance: OB11PluginMangerAdapter
) => void | Promise<void>;
plugin_onevent?: (
adapter: string,
core: NapCatCore,
obCtx: NapCatOneBot11Adapter,
ctx: NapCatPluginContext,
event: T,
actions: ActionMap,
instance: OB11PluginMangerAdapter
) => void | Promise<void>;
plugin_cleanup?: (
core: NapCatCore,
obContext: NapCatOneBot11Adapter,
actions: ActionMap,
instance: OB11PluginMangerAdapter
ctx: NapCatPluginContext
) => void | Promise<void>;
plugin_config_schema?: PluginConfigSchema;
plugin_config_ui?: PluginConfigSchema;
plugin_get_config?: () => any | Promise<any>;
plugin_set_config?: (config: any) => void | Promise<void>;
plugin_get_config?: (ctx: NapCatPluginContext) => any | Promise<any>;
plugin_set_config?: (ctx: NapCatPluginContext, config: any) => void | Promise<void>;
}
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<PluginConfig> {
private readonly pluginPath: string;
private readonly configPath: string;
private loadedPlugins: Map<string, LoadedPlugin> = new Map();
// Track failed plugins: ID -> Error Message
private failedPlugins: Map<string, string> = new Map();
declare config: PluginConfig;
public NapCatConfig = NapCatConfig;
@ -251,6 +251,7 @@ export class OB11PluginMangerAdapter extends IOB11NetworkAdapter<PluginConfig> {
entryPath,
packageJson,
module,
context: {} as NapCatPluginContext // Will be populated in registerPlugin
};
await this.registerPlugin(plugin);
@ -308,6 +309,9 @@ export class OB11PluginMangerAdapter extends IOB11NetworkAdapter<PluginConfig> {
return module && typeof module.plugin_init === 'function';
}
/**
*
*/
/**
*
*/
@ -320,6 +324,26 @@ export class OB11PluginMangerAdapter extends IOB11NetworkAdapter<PluginConfig> {
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<PluginConfig> {
// 调用插件初始化方法(必须存在)
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<PluginConfig> {
// 调用插件清理方法
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<PluginConfig> {
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<T extends OB11EmitEventContent> (event: T) {
@ -442,7 +426,6 @@ export class OB11PluginMangerAdapter extends IOB11NetworkAdapter<PluginConfig> {
return;
}
// 遍历所有已加载的插件,调用它们的事件处理方法
try {
await Promise.allSettled(
Array.from(this.loadedPlugins.values()).map((plugin) =>
@ -465,12 +448,8 @@ export class OB11PluginMangerAdapter extends IOB11NetworkAdapter<PluginConfig> {
// 优先使用 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<PluginConfig> {
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) {

View File

@ -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('<div style="padding: 10px; background: rgba(0,0,0,0.05); border-radius: 8px;"><h3>👋 Welcome to NapCat Builtin Plugin</h3><p>This is a demonstration of the plugin configuration interface.</p></div>'),
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('<div style="padding: 10px; background: rgba(0,0,0,0.05); border-radius: 8px;"><h3>👋 Welcome to NapCat Builtin Plugin</h3><p>This is a demonstration of the plugin configuration interface.</p></div>'),
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) {

View File

@ -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"

View File

@ -1,3 +0,0 @@
# Example Plugin
## Install
安装只需要将dist产物 `index.mjs` 目录放入 napcat根目录下`plugins/example`,如果没有这个目录请创建。

View File

@ -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<typeof createActionMap> | 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 };

View File

@ -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"
}
}

View File

@ -1,11 +0,0 @@
{
"extends": "../../tsconfig.base.json",
"include": [
"*.ts",
"**/*.ts"
],
"exclude": [
"node_modules",
"dist"
]
}

View File

@ -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()],
});

View File

@ -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",

View File

@ -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 });

View File

@ -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