Refactor plugin manager with modular loader and types

Refactors the plugin manager by extracting configuration, loader, and type definitions into separate modules under the 'plugin' directory. Introduces a new PluginLoader class for scanning and loading plugins, and updates the main manager to use modularized logic and improved type safety. This change improves maintainability, separation of concerns, and extensibility for plugin management.
This commit is contained in:
手瓜一十雪 2026-01-30 11:50:22 +08:00
parent 65bae6b57a
commit 40409a3841
9 changed files with 1615 additions and 664 deletions

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,39 @@
import { PluginConfigItem, PluginConfigSchema } from './types';
/**
* NapCat
* 便
*/
export class NapCatConfig {
static text (key: string, label: string, defaultValue?: string, description?: string, reactive?: boolean): PluginConfigItem {
return { key, type: 'string', label, default: defaultValue, description, reactive };
}
static number (key: string, label: string, defaultValue?: number, description?: string, reactive?: boolean): PluginConfigItem {
return { key, type: 'number', label, default: defaultValue, description, reactive };
}
static boolean (key: string, label: string, defaultValue?: boolean, description?: string, reactive?: boolean): PluginConfigItem {
return { key, type: 'boolean', label, default: defaultValue, description, reactive };
}
static select (key: string, label: string, options: { label: string; value: string | number; }[], defaultValue?: string | number, description?: string, reactive?: boolean): PluginConfigItem {
return { key, type: 'select', label, options, default: defaultValue, description, reactive };
}
static multiSelect (key: string, label: string, options: { label: string; value: string | number; }[], defaultValue?: (string | number)[], description?: string, reactive?: boolean): PluginConfigItem {
return { key, type: 'multi-select', label, options, default: defaultValue, description, reactive };
}
static html (content: string): PluginConfigItem {
return { key: `_html_${Math.random().toString(36).slice(2)}`, type: 'html', label: '', default: content };
}
static plainText (content: string): PluginConfigItem {
return { key: `_text_${Math.random().toString(36).slice(2)}`, type: 'text', label: '', default: content };
}
static combine (...items: PluginConfigItem[]): PluginConfigSchema {
return items;
}
}

View File

@ -0,0 +1,23 @@
// 导出类型
export type {
PluginPackageJson,
PluginConfigItem,
PluginConfigSchema,
INapCatConfigStatic,
NapCatConfigClass,
IPluginManager,
PluginConfigUIController,
PluginLogger,
NapCatPluginContext,
PluginModule,
PluginRuntimeStatus,
PluginRuntime,
PluginEntry,
PluginStatusConfig,
} from './types';
// 导出配置构建器
export { NapCatConfig } from './config';
// 导出加载器
export { PluginLoader } from './loader';

View File

@ -0,0 +1,298 @@
import fs from 'fs';
import path from 'path';
import { LogWrapper } from 'napcat-core/helper/log';
import {
PluginPackageJson,
PluginModule,
PluginEntry,
PluginStatusConfig,
} from './types';
/**
*
*
*/
export class PluginLoader {
constructor (
private readonly pluginPath: string,
private readonly configPath: string,
private readonly logger: LogWrapper
) { }
/**
*
*/
loadPluginStatusConfig (): PluginStatusConfig {
if (fs.existsSync(this.configPath)) {
try {
return JSON.parse(fs.readFileSync(this.configPath, 'utf-8'));
} catch (e) {
this.logger.logWarn('[PluginLoader] Error parsing plugins.json', e);
}
}
return {};
}
/**
*
*/
savePluginStatusConfig (config: PluginStatusConfig): void {
try {
fs.writeFileSync(this.configPath, JSON.stringify(config, null, 2), 'utf-8');
} catch (e) {
this.logger.logError('[PluginLoader] Error saving plugins.json', e);
}
}
/**
*
* plugin_init
*/
async scanPlugins (): Promise<PluginEntry[]> {
const entries: PluginEntry[] = [];
// 确保插件目录存在
if (!fs.existsSync(this.pluginPath)) {
this.logger.logWarn(`[PluginLoader] Plugin directory does not exist: ${this.pluginPath}`);
fs.mkdirSync(this.pluginPath, { recursive: true });
return entries;
}
const items = fs.readdirSync(this.pluginPath, { withFileTypes: true });
const statusConfig = this.loadPluginStatusConfig();
for (const item of items) {
if (!item.isDirectory()) {
continue;
}
const entry = this.scanDirectoryPlugin(item.name, statusConfig);
if (!entry) {
continue;
}
// 如果没有入口文件,跳过
if (!entry.entryPath) {
this.logger.logWarn(`[PluginLoader] Skipping ${item.name}: no entry file found`);
continue;
}
// 如果插件被禁用,跳过模块验证,直接添加到列表
if (!entry.enable) {
entries.push(entry);
continue;
}
// 验证模块有效性(仅对启用的插件)
const validation = await this.validatePluginEntry(entry.entryPath);
if (!validation.valid) {
this.logger.logWarn(`[PluginLoader] Skipping ${item.name}: ${validation.error}`);
continue;
}
entries.push(entry);
}
return entries;
}
/**
*
*/
private scanDirectoryPlugin (dirname: string, statusConfig: PluginStatusConfig): PluginEntry | null {
const pluginDir = path.join(this.pluginPath, dirname);
try {
// 尝试读取 package.json
let packageJson: PluginPackageJson | undefined;
const packageJsonPath = path.join(pluginDir, 'package.json');
if (fs.existsSync(packageJsonPath)) {
try {
const packageContent = fs.readFileSync(packageJsonPath, 'utf-8');
packageJson = JSON.parse(packageContent);
} catch (error) {
this.logger.logWarn(`[PluginLoader] Invalid package.json in ${dirname}:`, error);
}
}
// 获取插件 ID包名或目录名
const pluginId = packageJson?.name || dirname;
// 确定入口文件
const entryFile = this.findEntryFile(pluginDir, packageJson);
const entryPath = entryFile ? path.join(pluginDir, entryFile) : undefined;
// 获取启用状态(默认启用)
const enable = statusConfig[pluginId] !== false;
// 创建插件条目
const entry: PluginEntry = {
id: pluginId,
fileId: dirname,
name: packageJson?.name,
version: packageJson?.version,
description: packageJson?.description,
author: packageJson?.author,
pluginPath: pluginDir,
entryPath,
packageJson,
enable,
loaded: false,
runtime: {
status: 'unloaded',
},
};
// 如果没有入口文件,标记为错误
if (!entryPath) {
entry.runtime = {
status: 'error',
error: `No valid entry file found for plugin directory: ${dirname}`,
};
}
return entry;
} catch (error: any) {
// 创建错误条目
return {
id: dirname, // 使用目录名作为 ID
fileId: dirname,
pluginPath: path.join(this.pluginPath, dirname),
enable: statusConfig[dirname] !== false,
loaded: false,
runtime: {
status: 'error',
error: error.message || 'Unknown error during scan',
},
};
}
}
/**
*
*/
private findEntryFile (pluginDir: string, packageJson?: PluginPackageJson): string | null {
const possibleEntries = [
packageJson?.main,
'index.mjs',
'index.js',
'main.mjs',
'main.js',
].filter(Boolean) as string[];
for (const entry of possibleEntries) {
const entryPath = path.join(pluginDir, entry);
if (fs.existsSync(entryPath) && fs.statSync(entryPath).isFile()) {
return entry;
}
}
return null;
}
/**
*
*/
async importModule (filePath: string): Promise<any> {
const fileUrl = `file://${filePath.replace(/\\/g, '/')}`;
const fileUrlWithQuery = `${fileUrl}?t=${Date.now()}`;
return await import(fileUrlWithQuery);
}
/**
*
*/
async loadPluginModule (entry: PluginEntry): Promise<PluginModule | null> {
if (!entry.entryPath) {
entry.runtime = {
status: 'error',
error: 'No entry path specified',
};
return null;
}
try {
const module = await this.importModule(entry.entryPath);
if (!this.isValidPluginModule(module)) {
entry.runtime = {
status: 'error',
error: 'Invalid plugin module: missing plugin_init function',
};
return null;
}
return module;
} catch (error: any) {
entry.runtime = {
status: 'error',
error: error.message || 'Failed to import module',
};
return null;
}
}
/**
*
*/
isValidPluginModule (module: any): module is PluginModule {
return module && typeof module.plugin_init === 'function';
}
/**
* plugin_init
*
*/
async validatePluginEntry (entryPath: string): Promise<{ valid: boolean; error?: string; }> {
try {
const module = await this.importModule(entryPath);
if (this.isValidPluginModule(module)) {
return { valid: true };
}
return { valid: false, error: 'Missing plugin_init function' };
} catch (error: any) {
return { valid: false, error: error.message || 'Failed to import module' };
}
}
/**
*
*/
rescanPlugin (dirname: string): PluginEntry | null {
const statusConfig = this.loadPluginStatusConfig();
return this.scanDirectoryPlugin(dirname, statusConfig);
}
/**
* ID
*/
findPluginDirById (pluginId: string): string | null {
if (!fs.existsSync(this.pluginPath)) {
return null;
}
const items = fs.readdirSync(this.pluginPath, { withFileTypes: true });
for (const item of items) {
if (!item.isDirectory()) continue;
const packageJsonPath = path.join(this.pluginPath, item.name, 'package.json');
if (fs.existsSync(packageJsonPath)) {
try {
const pkg = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
if (pkg.name === pluginId) {
return item.name;
}
} catch (e) { }
}
// 如果目录名就是 ID
if (item.name === pluginId) {
return item.name;
}
}
return null;
}
}

View File

@ -0,0 +1,487 @@
import fs from 'fs';
import path from 'path';
import { ActionMap } from '@/napcat-onebot/action';
import { NapCatCore } from 'napcat-core';
import { NapCatOneBot11Adapter, OB11Message } from '@/napcat-onebot/index';
import { OB11EmitEventContent, OB11NetworkReloadType } from '@/napcat-onebot/network/index';
import { IOB11NetworkAdapter } from '@/napcat-onebot/network/adapter';
import { PluginConfig } from '@/napcat-onebot/config/config';
import { NapCatConfig } from './config';
import { PluginLoader } from './loader';
import {
PluginEntry,
PluginLogger,
PluginStatusConfig,
NapCatPluginContext,
IPluginManager,
} from './types';
export class OB11PluginManager extends IOB11NetworkAdapter<PluginConfig> implements IPluginManager {
private readonly pluginPath: string;
private readonly configPath: string;
private readonly loader: PluginLoader;
/** 插件注册表: ID -> 插件条目 */
private plugins: Map<string, PluginEntry> = new Map();
declare config: PluginConfig;
public NapCatConfig = NapCatConfig;
override get isActive (): boolean {
return this.isEnable && this.getLoadedPlugins().length > 0;
}
constructor (
name: string,
core: NapCatCore,
obContext: NapCatOneBot11Adapter,
actions: ActionMap
) {
const config = {
name,
messagePostFormat: 'array',
reportSelfMessage: true,
enable: true,
debug: true,
};
super(name, config, core, obContext, actions);
this.pluginPath = this.core.context.pathWrapper.pluginPath;
this.configPath = path.join(this.core.context.pathWrapper.configPath, 'plugins.json');
this.loader = new PluginLoader(this.pluginPath, this.configPath, this.logger);
}
// ==================== 插件状态配置 ====================
public getPluginConfig (): PluginStatusConfig {
return this.loader.loadPluginStatusConfig();
}
private savePluginConfig (config: PluginStatusConfig): void {
this.loader.savePluginStatusConfig(config);
}
// ==================== 插件扫描与加载 ====================
/**
*
*/
private async scanAndLoadPlugins (): Promise<void> {
// 扫描所有插件目录
const entries = await this.loader.scanPlugins();
// 清空现有注册表
this.plugins.clear();
// 注册所有插件条目
for (const entry of entries) {
this.plugins.set(entry.id, entry);
}
this.logger.log(`[PluginManager] Scanned ${this.plugins.size} plugins`);
// 加载启用的插件
for (const entry of this.plugins.values()) {
if (entry.enable && entry.runtime.status !== 'error') {
await this.loadPlugin(entry);
}
}
const loadedCount = this.getLoadedPlugins().length;
this.logger.log(`[PluginManager] Loaded ${loadedCount} plugins`);
}
/**
*
*/
private async loadPlugin (entry: PluginEntry): Promise<boolean> {
if (entry.loaded) {
return true;
}
if (entry.runtime.status === 'error') {
return false;
}
// 加载模块
const module = await this.loader.loadPluginModule(entry);
if (!module) {
return false;
}
// 创建上下文
const context = this.createPluginContext(entry);
// 初始化插件
try {
await module.plugin_init(context);
entry.loaded = true;
entry.runtime = {
status: 'loaded',
module,
context,
};
this.logger.log(`[PluginManager] Initialized plugin: ${entry.id}${entry.version ? ` v${entry.version}` : ''}`);
return true;
} catch (error: any) {
entry.loaded = false;
entry.runtime = {
status: 'error',
error: error.message || 'Initialization failed',
};
this.logger.logError(`[PluginManager] Error initializing plugin ${entry.id}:`, error);
return false;
}
}
/**
*
*/
private async unloadPlugin (entry: PluginEntry): Promise<void> {
if (!entry.loaded || entry.runtime.status !== 'loaded') {
return;
}
const { module, context } = entry.runtime;
// 调用清理方法
if (module && context && typeof module.plugin_cleanup === 'function') {
try {
await module.plugin_cleanup(context);
this.logger.log(`[PluginManager] Cleaned up plugin: ${entry.id}`);
} catch (error) {
this.logger.logError(`[PluginManager] Error cleaning up plugin ${entry.id}:`, error);
}
}
// 重置状态
entry.loaded = false;
entry.runtime = {
status: 'unloaded',
};
this.logger.log(`[PluginManager] Unloaded plugin: ${entry.id}`);
}
/**
*
*/
private createPluginContext (entry: PluginEntry): NapCatPluginContext {
const dataPath = path.join(entry.pluginPath, 'data');
const configPath = path.join(dataPath, 'config.json');
// 创建插件专用日志器
const pluginPrefix = `[Plugin: ${entry.id}]`;
const coreLogger = this.logger;
const pluginLogger: PluginLogger = {
log: (...args: any[]) => coreLogger.log(pluginPrefix, ...args),
debug: (...args: any[]) => coreLogger.logDebug(pluginPrefix, ...args),
info: (...args: any[]) => coreLogger.log(pluginPrefix, ...args),
warn: (...args: any[]) => coreLogger.logWarn(pluginPrefix, ...args),
error: (...args: any[]) => coreLogger.logError(pluginPrefix, ...args),
};
return {
core: this.core,
oneBot: this.obContext,
actions: this.actions,
pluginName: entry.id,
pluginPath: entry.pluginPath,
dataPath,
configPath,
NapCatConfig,
adapterName: this.name,
pluginManager: this,
logger: pluginLogger,
};
}
// ==================== 公共 API ====================
/**
*
*/
public getPluginPath (): string {
return this.pluginPath;
}
/**
*
*/
public getAllPlugins (): PluginEntry[] {
return Array.from(this.plugins.values());
}
/**
*
*/
public getLoadedPlugins (): PluginEntry[] {
return Array.from(this.plugins.values()).filter(p => p.loaded);
}
/**
* ID
*/
public getPluginInfo (pluginId: string): PluginEntry | undefined {
return this.plugins.get(pluginId);
}
/**
* /
*/
public async setPluginStatus (pluginId: string, enable: boolean): Promise<void> {
const config = this.getPluginConfig();
config[pluginId] = enable;
this.savePluginConfig(config);
const entry = this.plugins.get(pluginId);
if (entry) {
entry.enable = enable;
if (enable && !entry.loaded) {
// 启用插件
await this.loadPlugin(entry);
} else if (!enable && entry.loaded) {
// 禁用插件
await this.unloadPlugin(entry);
}
}
}
/**
* ID
*/
public async loadPluginById (pluginId: string): Promise<boolean> {
let entry = this.plugins.get(pluginId);
if (!entry) {
// 尝试查找并扫描
const dirname = this.loader.findPluginDirById(pluginId);
if (!dirname) {
this.logger.logWarn(`[PluginManager] Plugin ${pluginId} not found in filesystem`);
return false;
}
const newEntry = this.loader.rescanPlugin(dirname);
if (!newEntry) {
return false;
}
this.plugins.set(newEntry.id, newEntry);
entry = newEntry;
}
return await this.loadPlugin(entry);
}
/**
*
*/
public async unregisterPlugin (pluginId: string): Promise<void> {
const entry = this.plugins.get(pluginId);
if (entry) {
await this.unloadPlugin(entry);
}
}
/**
*
*/
public async uninstallPlugin (pluginId: string, cleanData: boolean = false): Promise<void> {
const entry = this.plugins.get(pluginId);
if (!entry) {
throw new Error(`Plugin ${pluginId} not found`);
}
const pluginPath = entry.pluginPath;
const dataPath = path.join(pluginPath, 'data');
// 先卸载插件
await this.unloadPlugin(entry);
// 从注册表移除
this.plugins.delete(pluginId);
// 删除插件目录
if (fs.existsSync(pluginPath)) {
fs.rmSync(pluginPath, { recursive: true, force: true });
}
// 清理数据
if (cleanData && fs.existsSync(dataPath)) {
fs.rmSync(dataPath, { recursive: true, force: true });
}
}
/**
*
*/
public async reloadPlugin (pluginId: string): Promise<boolean> {
const entry = this.plugins.get(pluginId);
if (!entry) {
this.logger.logWarn(`[PluginManager] Plugin ${pluginId} not found`);
return false;
}
try {
// 卸载插件
await this.unloadPlugin(entry);
// 重新扫描
const newEntry = this.loader.rescanPlugin(entry.fileId);
if (!newEntry) {
return false;
}
// 更新注册表
this.plugins.set(newEntry.id, newEntry);
// 重新加载
if (newEntry.enable) {
await this.loadPlugin(newEntry);
}
this.logger.log(`[PluginManager] Plugin ${pluginId} reloaded successfully`);
return true;
} catch (error) {
this.logger.logError(`[PluginManager] Error reloading plugin ${pluginId}:`, error);
return false;
}
}
/**
*
*/
public async loadDirectoryPlugin (dirname: string): Promise<void> {
const entry = this.loader.rescanPlugin(dirname);
if (!entry) {
return;
}
// 检查是否已存在
if (this.plugins.has(entry.id)) {
this.logger.logWarn(`[PluginManager] Plugin ${entry.id} already exists`);
return;
}
this.plugins.set(entry.id, entry);
if (entry.enable && entry.runtime.status !== 'error') {
await this.loadPlugin(entry);
}
}
/**
*
*/
public getPluginDataPath (pluginId: string): string {
const entry = this.plugins.get(pluginId);
if (!entry) {
throw new Error(`Plugin ${pluginId} not found`);
}
return path.join(entry.pluginPath, 'data');
}
/**
*
*/
public getPluginConfigPath (pluginId: string): string {
return path.join(this.getPluginDataPath(pluginId), 'config.json');
}
// ==================== 事件处理 ====================
async onEvent<T extends OB11EmitEventContent> (event: T): Promise<void> {
if (!this.isEnable) {
return;
}
try {
await Promise.allSettled(
this.getLoadedPlugins().map((entry) =>
this.callPluginEventHandler(entry, event)
)
);
} catch (error) {
this.logger.logError('[PluginManager] Error handling event:', error);
}
}
/**
*
*/
private async callPluginEventHandler (
entry: PluginEntry,
event: OB11EmitEventContent
): Promise<void> {
if (entry.runtime.status !== 'loaded' || !entry.runtime.module || !entry.runtime.context) {
return;
}
const { module, context } = entry.runtime;
try {
// 优先使用 plugin_onevent 方法
if (typeof module.plugin_onevent === 'function') {
await module.plugin_onevent(context, event);
}
// 如果是消息事件并且插件有 plugin_onmessage 方法,也调用
if (
(event as any).message_type &&
typeof module.plugin_onmessage === 'function'
) {
await module.plugin_onmessage(context, event as OB11Message);
}
} catch (error) {
this.logger.logError(`[PluginManager] Error calling plugin ${entry.id} event handler:`, error);
}
}
// ==================== 生命周期 ====================
async open (): Promise<void> {
if (this.isEnable) {
return;
}
this.logger.log('[PluginManager] Opening plugin manager...');
this.isEnable = true;
// 扫描并加载所有插件
await this.scanAndLoadPlugins();
this.logger.log(`[PluginManager] Plugin manager opened with ${this.getLoadedPlugins().length} plugins loaded`);
}
async close (): Promise<void> {
if (!this.isEnable) {
return;
}
this.logger.log('[PluginManager] Closing plugin manager...');
this.isEnable = false;
// 卸载所有已加载的插件
for (const entry of this.plugins.values()) {
if (entry.loaded) {
await this.unloadPlugin(entry);
}
}
this.logger.log('[PluginManager] Plugin manager closed');
}
async reload (): Promise<OB11NetworkReloadType> {
this.logger.log('[PluginManager] Reloading plugin manager...');
// 先关闭然后重新打开
await this.close();
await this.open();
this.logger.log('[PluginManager] Plugin manager reloaded');
return OB11NetworkReloadType.Normal;
}
}

View File

@ -0,0 +1,222 @@
import { NapCatCore } from 'napcat-core';
import { NapCatOneBot11Adapter, OB11Message } from '@/napcat-onebot/index';
import { ActionMap } from '@/napcat-onebot/action';
import { OB11EmitEventContent } from '@/napcat-onebot/network/index';
// ==================== 插件包信息 ====================
export interface PluginPackageJson {
name?: string;
plugin?: string;
version?: string;
main?: string;
description?: string;
author?: string;
}
// ==================== 插件配置 Schema ====================
export interface PluginConfigItem {
key: string;
type: 'string' | 'number' | 'boolean' | 'select' | 'multi-select' | 'html' | 'text';
label: string;
description?: string;
default?: unknown;
options?: { label: string; value: string | number; }[];
placeholder?: string;
/** 标记此字段为响应式:值变化时触发 schema 刷新 */
reactive?: boolean;
/** 是否隐藏此字段 */
hidden?: boolean;
}
export type PluginConfigSchema = PluginConfigItem[];
// ==================== NapCatConfig 静态接口 ====================
/** NapCatConfig 类的静态方法接口(用于 typeof NapCatConfig */
export interface INapCatConfigStatic {
text (key: string, label: string, defaultValue?: string, description?: string, reactive?: boolean): PluginConfigItem;
number (key: string, label: string, defaultValue?: number, description?: string, reactive?: boolean): PluginConfigItem;
boolean (key: string, label: string, defaultValue?: boolean, description?: string, reactive?: boolean): PluginConfigItem;
select (key: string, label: string, options: { label: string; value: string | number; }[], defaultValue?: string | number, description?: string, reactive?: boolean): PluginConfigItem;
multiSelect (key: string, label: string, options: { label: string; value: string | number; }[], defaultValue?: (string | number)[], description?: string, reactive?: boolean): PluginConfigItem;
html (content: string): PluginConfigItem;
plainText (content: string): PluginConfigItem;
combine (...items: PluginConfigItem[]): PluginConfigSchema;
}
/** NapCatConfig 类型(包含静态方法) */
export type NapCatConfigClass = INapCatConfigStatic;
// ==================== 插件管理器接口 ====================
/** 插件管理器公共接口 */
export interface IPluginManager {
readonly config: unknown;
getPluginPath (): string;
getPluginConfig (): PluginStatusConfig;
getAllPlugins (): PluginEntry[];
getLoadedPlugins (): PluginEntry[];
getPluginInfo (pluginId: string): PluginEntry | undefined;
setPluginStatus (pluginId: string, enable: boolean): Promise<void>;
loadPluginById (pluginId: string): Promise<boolean>;
unregisterPlugin (pluginId: string): Promise<void>;
uninstallPlugin (pluginId: string, cleanData?: boolean): Promise<void>;
reloadPlugin (pluginId: string): Promise<boolean>;
loadDirectoryPlugin (dirname: string): Promise<void>;
getPluginDataPath (pluginId: string): string;
getPluginConfigPath (pluginId: string): string;
}
// ==================== 插件配置 UI 控制器 ====================
/** 插件配置 UI 控制器 - 用于动态控制配置界面 */
export interface PluginConfigUIController {
/** 更新整个 schema */
updateSchema: (schema: PluginConfigSchema) => void;
/** 更新单个字段 */
updateField: (key: string, field: Partial<PluginConfigItem>) => void;
/** 移除字段 */
removeField: (key: string) => void;
/** 添加字段 */
addField: (field: PluginConfigItem, afterKey?: string) => void;
/** 显示字段 */
showField: (key: string) => void;
/** 隐藏字段 */
hideField: (key: string) => void;
/** 获取当前配置值 */
getCurrentConfig: () => Record<string, unknown>;
}
// ==================== 插件日志接口 ====================
/**
* - API
*/
export interface PluginLogger {
/** 普通日志 */
log (...args: unknown[]): void;
/** 调试日志 */
debug (...args: unknown[]): void;
/** 信息日志 */
info (...args: unknown[]): void;
/** 警告日志 */
warn (...args: unknown[]): void;
/** 错误日志 */
error (...args: unknown[]): void;
}
// ==================== 插件上下文 ====================
export interface NapCatPluginContext {
core: NapCatCore;
oneBot: NapCatOneBot11Adapter;
actions: ActionMap;
pluginName: string;
pluginPath: string;
configPath: string;
dataPath: string;
/** NapCatConfig 配置构建器 */
NapCatConfig: NapCatConfigClass;
adapterName: string;
/** 插件管理器实例 */
pluginManager: IPluginManager;
/** 插件日志器 - 自动添加插件名称前缀 */
logger: PluginLogger;
}
// ==================== 插件模块接口 ====================
export interface PluginModule<T extends OB11EmitEventContent = OB11EmitEventContent> {
plugin_init: (ctx: NapCatPluginContext) => void | Promise<void>;
plugin_onmessage?: (
ctx: NapCatPluginContext,
event: OB11Message,
) => void | Promise<void>;
plugin_onevent?: (
ctx: NapCatPluginContext,
event: T,
) => void | Promise<void>;
plugin_cleanup?: (
ctx: NapCatPluginContext
) => void | Promise<void>;
plugin_config_schema?: PluginConfigSchema;
plugin_config_ui?: PluginConfigSchema;
plugin_get_config?: (ctx: NapCatPluginContext) => unknown | Promise<unknown>;
plugin_set_config?: (ctx: NapCatPluginContext, config: unknown) => void | Promise<void>;
/**
* -
*
*/
plugin_config_controller?: (
ctx: NapCatPluginContext,
ui: PluginConfigUIController,
initialConfig: Record<string, unknown>
) => void | (() => void) | Promise<void | (() => void)>;
/**
* - reactive
*/
plugin_on_config_change?: (
ctx: NapCatPluginContext,
ui: PluginConfigUIController,
key: string,
value: unknown,
currentConfig: Record<string, unknown>
) => void | Promise<void>;
}
// ==================== 插件运行时状态 ====================
export type PluginRuntimeStatus = 'loaded' | 'error' | 'unloaded';
export interface PluginRuntime {
/** 运行时状态 */
status: PluginRuntimeStatus;
/** 错误信息(当 status 为 'error' 时) */
error?: string;
/** 插件模块(当 status 为 'loaded' 时) */
module?: PluginModule;
/** 插件上下文(当 status 为 'loaded' 时) */
context?: NapCatPluginContext;
}
// ==================== 插件条目(统一管理所有插件) ====================
export interface PluginEntry {
// ===== 基础信息 =====
/** 插件 ID包名或目录名 */
id: string;
/** 文件系统目录名 */
fileId: string;
/** 显示名称 */
name?: string;
/** 版本号 */
version?: string;
/** 描述 */
description?: string;
/** 作者 */
author?: string;
/** 插件目录路径 */
pluginPath: string;
/** 入口文件路径 */
entryPath?: string;
/** package.json 内容 */
packageJson?: PluginPackageJson;
// ===== 状态 =====
/** 是否启用(用户配置) */
enable: boolean;
/** 运行时是否已加载 */
loaded: boolean;
// ===== 运行时 =====
/** 运行时信息 */
runtime: PluginRuntime;
}
// ==================== 插件状态配置(持久化) ====================
export interface PluginStatusConfig {
[key: string]: boolean; // key: pluginId, value: enabled
}

View File

@ -6,6 +6,7 @@ import { OB11PluginMangerAdapter } from '@/napcat-onebot/network/plugin-manger';
import { webUiPathWrapper } from '@/napcat-webui-backend/index';
import path from 'path';
import fs from 'fs';
import compressing from 'compressing';
// Helper to get the plugin manager adapter
const getPluginManager = (): OB11PluginMangerAdapter | null => {
@ -64,73 +65,42 @@ export const GetPluginListHandler: RequestHandler = async (_req, res) => {
return sendSuccess(res, { plugins: [], pluginManagerNotFound: true });
}
const loadedPlugins = pluginManager.getLoadedPlugins();
const loadedPluginMap = new Map<string, any>(); // Map id -> Loaded Info
const loadedPlugins = pluginManager.getAllPlugins();
const AllPlugins: Array<{
name: string;
id: string;
version: string;
description: string;
author: string;
status: string;
hasConfig: boolean;
}> = new Array();
// 1. 整理已加载的插件
for (const p of loadedPlugins) {
loadedPluginMap.set(p.name, {
name: p.packageJson?.plugin || p.name, // 优先显示 package.json 的 plugin 字段
id: p.name, // 包名,用于 API 操作
// 根据插件状态确定 status
let status: string;
if (!p.enable) {
status = 'disabled';
} else if (p.loaded) {
status = 'active';
} else {
status = 'stopped'; // 启用但未加载(可能加载失败)
}
AllPlugins.push({
name: p.packageJson?.plugin || p.name || '', // 优先显示 package.json 的 plugin 字段
id: p.id, // 包名,用于 API 操作
version: p.version || '0.0.0',
description: p.packageJson?.description || '',
author: p.packageJson?.author || '',
status: 'active',
hasConfig: !!(p.module.plugin_config_schema || p.module.plugin_config_ui)
status,
hasConfig: !!(p.runtime.module?.plugin_config_schema || p.runtime.module?.plugin_config_ui)
});
}
const pluginPath = pluginManager.getPluginPath();
const pluginConfig = pluginManager.getPluginConfig();
const allPlugins: any[] = [];
// 2. 扫描文件系统,合并状态
if (fs.existsSync(pluginPath)) {
const items = fs.readdirSync(pluginPath, { withFileTypes: true });
for (const item of items) {
if (!item.isDirectory()) continue;
// 读取 package.json 获取插件信息
let id = item.name;
let name = item.name;
let version = '0.0.0';
let description = '';
let author = '';
const packageJsonPath = path.join(pluginPath, item.name, 'package.json');
if (fs.existsSync(packageJsonPath)) {
try {
const pkg = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
id = pkg.name || id;
name = pkg.plugin || pkg.name || name;
version = pkg.version || version;
description = pkg.description || description;
author = pkg.author || author;
} catch (e) { }
}
const isActiveConfig = pluginConfig[id] !== false; // 默认为 true
if (loadedPluginMap.has(id)) {
// 已加载,使用加载的信息
const loadedInfo = loadedPluginMap.get(id);
allPlugins.push(loadedInfo);
} else {
allPlugins.push({
name,
id,
version,
description,
author,
// 如果配置是 false则为 disabled否则是 stopped (应启动但未启动)
status: isActiveConfig ? 'stopped' : 'disabled'
});
}
}
}
return sendSuccess(res, { plugins: allPlugins, pluginManagerNotFound: false });
return sendSuccess(res, { plugins: AllPlugins, pluginManagerNotFound: false });
};
export const SetPluginStatusHandler: RequestHandler = async (req, res) => {
@ -144,14 +114,14 @@ export const SetPluginStatusHandler: RequestHandler = async (req, res) => {
}
try {
// 设置插件状态
pluginManager.setPluginStatus(id, enable);
// 设置插件状态(需要 await因为内部会加载/卸载插件)
await pluginManager.setPluginStatus(id, enable);
// 如果启用,需要加载插件
// 如果启用,检查插件是否加载成功
if (enable) {
const loaded = await pluginManager.loadPluginById(id);
if (!loaded) {
return sendError(res, 'Plugin not found: ' + id);
const plugin = pluginManager.getPluginInfo(id);
if (!plugin || !plugin.loaded) {
return sendError(res, 'Plugin load failed: ' + id);
}
}
@ -191,15 +161,15 @@ export const GetPluginConfigHandler: RequestHandler = async (req, res) => {
if (!plugin) return sendError(res, 'Plugin not loaded');
// 获取配置值
let config = {};
if (plugin.module.plugin_get_config) {
let config: unknown = {};
if (plugin.runtime.module?.plugin_get_config && plugin.runtime.context) {
try {
config = await plugin.module.plugin_get_config(plugin.context);
config = await plugin.runtime.module?.plugin_get_config(plugin.runtime.context);
} catch (e) { }
} else {
// Default behavior: read from default config path
try {
const configPath = plugin.context?.configPath || pluginManager.getPluginConfigPath(id);
const configPath = plugin.runtime.context?.configPath || pluginManager.getPluginConfigPath(id);
if (fs.existsSync(configPath)) {
config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
}
@ -207,10 +177,10 @@ export const GetPluginConfigHandler: RequestHandler = async (req, res) => {
}
// 获取静态 schema
const schema = plugin.module.plugin_config_schema || plugin.module.plugin_config_ui || [];
const schema = plugin.runtime.module?.plugin_config_schema || plugin.runtime.module?.plugin_config_ui || [];
// 检查是否支持动态控制
const supportReactive = !!(plugin.module.plugin_config_controller || plugin.module.plugin_on_config_change);
const supportReactive = !!(plugin.runtime.module?.plugin_config_controller || plugin.runtime.module?.plugin_on_config_change);
return sendSuccess(res, { schema, config, supportReactive });
};
@ -302,10 +272,10 @@ export const PluginConfigSSEHandler: RequestHandler = (req, res): void => {
// 调用插件的控制器初始化(异步处理)
(async () => {
let cleanup: (() => void) | undefined;
if (plugin.module.plugin_config_controller) {
if (plugin.runtime.module?.plugin_config_controller && plugin.runtime.context) {
try {
const result = await plugin.module.plugin_config_controller(
plugin.context,
const result = await plugin.runtime.module.plugin_config_controller(
plugin.runtime.context,
uiController,
currentConfig
);
@ -368,7 +338,7 @@ export const PluginConfigChangeHandler: RequestHandler = async (req, res) => {
session.currentConfig = currentConfig || {};
// 如果插件有响应式处理器,调用它
if (plugin.module.plugin_on_config_change) {
if (plugin.runtime.module?.plugin_on_config_change) {
const uiController = {
updateSchema: (schema: any[]) => {
session.res.write(`event: schema\n`);
@ -398,13 +368,15 @@ export const PluginConfigChangeHandler: RequestHandler = async (req, res) => {
};
try {
await plugin.module.plugin_on_config_change(
plugin.context,
uiController,
key,
value,
currentConfig || {}
);
if (plugin.runtime.context) {
await plugin.runtime.module.plugin_on_config_change(
plugin.runtime.context,
uiController,
key,
value,
currentConfig || {}
);
}
} catch (e: any) {
session.res.write(`event: error\n`);
session.res.write(`data: ${JSON.stringify({ message: e.message })}\n\n`);
@ -424,17 +396,17 @@ export const SetPluginConfigHandler: RequestHandler = async (req, res) => {
const plugin = pluginManager.getPluginInfo(id);
if (!plugin) return sendError(res, 'Plugin not loaded');
if (plugin.module.plugin_set_config) {
if (plugin.runtime.module?.plugin_set_config && plugin.runtime.context) {
try {
await plugin.module.plugin_set_config(plugin.context, config);
await plugin.runtime.module.plugin_set_config(plugin.runtime.context, config);
return sendSuccess(res, { message: 'Config updated' });
} catch (e: any) {
return sendError(res, 'Error updating config: ' + e.message);
}
} else if (plugin.module.plugin_config_schema || plugin.module.plugin_config_ui || plugin.module.plugin_config_controller) {
} else if (plugin.runtime.module?.plugin_config_schema || plugin.runtime.module?.plugin_config_ui || plugin.runtime.module?.plugin_config_controller) {
// Default behavior: write to default config path
try {
const configPath = plugin.context?.configPath || pluginManager.getPluginConfigPath(id);
const configPath = plugin.runtime.context?.configPath || pluginManager.getPluginConfigPath(id);
const configDir = path.dirname(configPath);
if (!fs.existsSync(configDir)) {
@ -453,3 +425,141 @@ export const SetPluginConfigHandler: RequestHandler = async (req, res) => {
return sendError(res, 'Plugin does not support config update');
}
};
/**
* .zip
*/
export const ImportLocalPluginHandler: RequestHandler = async (req, res) => {
const pluginManager = getPluginManager();
if (!pluginManager) {
return sendError(res, 'Plugin Manager not found');
}
// multer 会将文件信息放在 req.file 中
const file = req.file;
if (!file) {
return sendError(res, 'No file uploaded');
}
const PLUGINS_DIR = webUiPathWrapper.pluginPath;
// 确保插件目录存在
if (!fs.existsSync(PLUGINS_DIR)) {
fs.mkdirSync(PLUGINS_DIR, { recursive: true });
}
const tempZipPath = file.path;
try {
// 创建临时解压目录
const tempExtractDir = path.join(PLUGINS_DIR, `_temp_extract_${Date.now()}`);
fs.mkdirSync(tempExtractDir, { recursive: true });
// 解压到临时目录
await compressing.zip.uncompress(tempZipPath, tempExtractDir);
// 检查解压后的内容
const extractedItems = fs.readdirSync(tempExtractDir);
let pluginSourceDir: string;
let pluginId: string;
// 判断解压结构:可能是直接的插件文件,或者包含一个子目录
const hasPackageJson = extractedItems.includes('package.json');
const hasIndexFile = extractedItems.some(item =>
['index.js', 'index.mjs', 'main.js', 'main.mjs'].includes(item)
);
if (hasPackageJson || hasIndexFile) {
// 直接是插件文件
pluginSourceDir = tempExtractDir;
// 尝试从 package.json 获取插件 ID
const packageJsonPath = path.join(tempExtractDir, 'package.json');
if (fs.existsSync(packageJsonPath)) {
try {
const pkg = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
pluginId = pkg.name || path.basename(file.originalname, '.zip');
} catch {
pluginId = path.basename(file.originalname, '.zip');
}
} else {
pluginId = path.basename(file.originalname, '.zip');
}
} else if (extractedItems.length === 1 && fs.statSync(path.join(tempExtractDir, extractedItems[0]!)).isDirectory()) {
// 包含一个子目录
const subDir = extractedItems[0]!;
pluginSourceDir = path.join(tempExtractDir, subDir);
// 尝试从子目录的 package.json 获取插件 ID
const packageJsonPath = path.join(pluginSourceDir, 'package.json');
if (fs.existsSync(packageJsonPath)) {
try {
const pkg = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
pluginId = pkg.name || subDir;
} catch {
pluginId = subDir;
}
} else {
pluginId = subDir;
}
} else {
// 清理临时文件
fs.rmSync(tempExtractDir, { recursive: true, force: true });
fs.unlinkSync(tempZipPath);
return sendError(res, 'Invalid plugin package structure');
}
// 目标插件目录
const targetPluginDir = path.join(PLUGINS_DIR, pluginId);
// 如果目标目录已存在,先删除
if (fs.existsSync(targetPluginDir)) {
// 先卸载已存在的插件
const existingPlugin = pluginManager.getPluginInfo(pluginId);
if (existingPlugin && existingPlugin.loaded) {
await pluginManager.unregisterPlugin(pluginId);
}
fs.rmSync(targetPluginDir, { recursive: true, force: true });
}
// 移动插件文件到目标目录
if (pluginSourceDir === tempExtractDir) {
// 直接重命名临时目录
fs.renameSync(tempExtractDir, targetPluginDir);
} else {
// 移动子目录内容
fs.renameSync(pluginSourceDir, targetPluginDir);
// 清理临时目录
fs.rmSync(tempExtractDir, { recursive: true, force: true });
}
// 删除上传的临时文件
if (fs.existsSync(tempZipPath)) {
fs.unlinkSync(tempZipPath);
}
// 加载插件
const loaded = await pluginManager.loadPluginById(pluginId);
if (loaded) {
return sendSuccess(res, {
message: 'Plugin imported and loaded successfully',
pluginId,
installPath: targetPluginDir,
});
} else {
return sendSuccess(res, {
message: 'Plugin imported but failed to load (check plugin structure)',
pluginId,
installPath: targetPluginDir,
});
}
} catch (e: any) {
// 清理临时文件
if (fs.existsSync(tempZipPath)) {
fs.unlinkSync(tempZipPath);
}
return sendError(res, 'Failed to import plugin: ' + e.message);
}
};

View File

@ -36,11 +36,6 @@ export const siteConfig = {
icon: <LuSignal className='w-5 h-5' />,
href: '/network',
},
{
label: '其他配置',
icon: <LuSettings className='w-5 h-5' />,
href: '/config',
},
{
label: '猫猫日志',
icon: <LuFileText className='w-5 h-5' />,
@ -76,6 +71,11 @@ export const siteConfig = {
icon: <LuTerminal className='w-5 h-5' />,
href: '/terminal',
},
{
label: '系统配置',
icon: <LuSettings className='w-5 h-5' />,
href: '/config',
},
{
label: '关于我们',
icon: <LuInfo className='w-5 h-5' />,