mirror of
https://github.com/NapNeko/NapCatQQ.git
synced 2026-02-04 06:31:13 +00:00
Introduce in-memory static file support and inter-plugin exports. Add MemoryStaticFile/MemoryFileGenerator types and expose staticOnMem in PluginRouterRegistry; router registry now tracks memory routes and exposes getters. Add getPluginExports to plugin manager adapters to allow plugins to call each other's exported modules. WebUI backend gains routes to serve /plugin/:pluginId/mem/* (memory files) and /plugin/:pluginId/files/* (plugin filesystem static) without auth. Update builtin plugin to demonstrate staticOnMem and inter-plugin call, and add frontend UI to open extension pages in a new window. Note: API router no longer mounts static filesystem routes — those are handled by webui-backend.
551 lines
15 KiB
TypeScript
551 lines
15 KiB
TypeScript
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 './plugin/config';
|
|
import { PluginLoader } from './plugin/loader';
|
|
import {
|
|
PluginEntry,
|
|
PluginLogger,
|
|
PluginStatusConfig,
|
|
NapCatPluginContext,
|
|
IPluginManager,
|
|
} from './plugin/types';
|
|
import { PluginRouterRegistryImpl } from './plugin/router-registry';
|
|
|
|
export { PluginPackageJson } from './plugin/types';
|
|
export { PluginConfigItem } from './plugin/types';
|
|
export { PluginConfigUIController } from './plugin/types';
|
|
export { NapCatConfig } from './plugin/config';
|
|
export { PluginConfigSchema } from './plugin/types';
|
|
export { PluginLogger } from './plugin/types';
|
|
export { NapCatPluginContext } from './plugin/types';
|
|
export { PluginModule } from './plugin/types';
|
|
export { PluginStatusConfig } from './plugin/types';
|
|
export { PluginRouterRegistry, PluginRequestHandler, PluginApiRouteDefinition, PluginPageDefinition, HttpMethod } from './plugin/types';
|
|
export { PluginHttpRequest, PluginHttpResponse, PluginNextFunction } from './plugin/types';
|
|
export { MemoryStaticFile, MemoryFileGenerator } from './plugin/types';
|
|
export { PluginRouterRegistryImpl } from './plugin/router-registry';
|
|
export class OB11PluginMangerAdapter 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();
|
|
|
|
/** 插件路由注册表: ID -> 路由注册器 */
|
|
private pluginRouters: Map<string, PluginRouterRegistryImpl> = 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);
|
|
}
|
|
}
|
|
|
|
// 清理插件路由
|
|
const routerRegistry = this.pluginRouters.get(entry.id);
|
|
if (routerRegistry) {
|
|
routerRegistry.clear();
|
|
this.pluginRouters.delete(entry.id);
|
|
}
|
|
|
|
// 清理模块缓存
|
|
this.loader.clearCache(entry.pluginPath);
|
|
|
|
// 重置状态
|
|
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),
|
|
};
|
|
|
|
// 创建插件路由注册器
|
|
const routerRegistry = new PluginRouterRegistryImpl(entry.id, entry.pluginPath);
|
|
// 保存到路由注册表
|
|
this.pluginRouters.set(entry.id, routerRegistry);
|
|
|
|
// 创建获取其他插件导出的方法
|
|
const getPluginExports = <T = any>(pluginId: string): T | undefined => {
|
|
const targetEntry = this.plugins.get(pluginId);
|
|
if (!targetEntry || !targetEntry.loaded || targetEntry.runtime.status !== 'loaded') {
|
|
return undefined;
|
|
}
|
|
return targetEntry.runtime.module as T;
|
|
};
|
|
|
|
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,
|
|
router: routerRegistry,
|
|
getPluginExports,
|
|
};
|
|
}
|
|
|
|
// ==================== 公共 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 getPluginRouter (pluginId: string): PluginRouterRegistryImpl | undefined {
|
|
return this.pluginRouters.get(pluginId);
|
|
}
|
|
|
|
/**
|
|
* 获取所有插件路由注册器
|
|
*/
|
|
public getAllPluginRouters (): Map<string, PluginRouterRegistryImpl> {
|
|
return this.pluginRouters;
|
|
}
|
|
|
|
/**
|
|
* 设置插件状态(启用/禁用)
|
|
*/
|
|
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;
|
|
}
|
|
|
|
if (!entry.enable) {
|
|
this.logger.log(`[PluginManager] Skipping loading disabled plugin: ${pluginId}`);
|
|
return false;
|
|
}
|
|
|
|
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');
|
|
|
|
if (entry.loaded) {
|
|
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;
|
|
}
|
|
}
|