diff --git a/packages/napcat-onebot/network/plugin-manger.ts b/packages/napcat-onebot/network/plugin-manger.ts index bc16e9bb..68afa0b8 100644 --- a/packages/napcat-onebot/network/plugin-manger.ts +++ b/packages/napcat-onebot/network/plugin-manger.ts @@ -1,178 +1,43 @@ -import { ActionMap } from '../action'; -import { NapCatCore } from 'napcat-core'; -import { NapCatOneBot11Adapter, OB11Message } from '@/napcat-onebot/index'; -import { OB11EmitEventContent, OB11NetworkReloadType } from './index'; -import { IOB11NetworkAdapter } from '@/napcat-onebot/network/adapter'; -import { PluginConfig } from '../config/config'; 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'; -export interface PluginPackageJson { - name?: string; - plugin?: string; - version?: string; - main?: string; - description?: string; - author?: string; -} - -export interface PluginConfigItem { - key: string; - type: 'string' | 'number' | 'boolean' | 'select' | 'multi-select' | 'html' | 'text'; - label: string; - description?: string; - default?: any; - options?: { label: string; value: string | number; }[]; - placeholder?: string; - /** 标记此字段为响应式:值变化时触发 schema 刷新 */ - reactive?: boolean; - /** 是否隐藏此字段 */ - hidden?: boolean; -} - -/** 插件配置 UI 控制器 - 用于动态控制配置界面 */ -export interface PluginConfigUIController { - /** 更新整个 schema */ - updateSchema: (schema: PluginConfigSchema) => void; - /** 更新单个字段 */ - updateField: (key: string, field: Partial) => void; - /** 移除字段 */ - removeField: (key: string) => void; - /** 添加字段 */ - addField: (field: PluginConfigItem, afterKey?: string) => void; - /** 显示字段 */ - showField: (key: string) => void; - /** 隐藏字段 */ - hideField: (key: string) => void; - /** 获取当前配置值 */ - getCurrentConfig: () => Record; -} - -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; - } -} - -export type PluginConfigSchema = PluginConfigItem[]; - -/** - * 插件日志接口 - 简化的日志 API - */ -export interface PluginLogger { - /** 普通日志 */ - log (...args: any[]): void; - /** 调试日志 */ - debug (...args: any[]): void; - /** 信息日志 */ - info (...args: any[]): void; - /** 警告日志 */ - warn (...args: any[]): void; - /** 错误日志 */ - error (...args: any[]): void; -} - -export interface NapCatPluginContext { - core: NapCatCore; - oneBot: NapCatOneBot11Adapter; - actions: ActionMap; - pluginName: string; - pluginPath: string; - configPath: string; - dataPath: string; - NapCatConfig: typeof NapCatConfig; - adapterName: string; - pluginManager: OB11PluginMangerAdapter; - /** 插件日志器 - 自动添加插件名称前缀 */ - logger: PluginLogger; -} - -export interface PluginModule { - plugin_init: (ctx: NapCatPluginContext) => void | Promise; - plugin_onmessage?: ( - ctx: NapCatPluginContext, - event: OB11Message, - ) => void | Promise; - plugin_onevent?: ( - ctx: NapCatPluginContext, - event: T, - ) => void | Promise; - plugin_cleanup?: ( - ctx: NapCatPluginContext - ) => void | Promise; - plugin_config_schema?: PluginConfigSchema; - plugin_config_ui?: PluginConfigSchema; - plugin_get_config?: (ctx: NapCatPluginContext) => any | Promise; - plugin_set_config?: (ctx: NapCatPluginContext, config: any) => void | Promise; - /** - * 配置界面控制器 - 当配置界面打开时调用 - * 返回清理函数,在界面关闭时调用 - */ - plugin_config_controller?: ( - ctx: NapCatPluginContext, - ui: PluginConfigUIController, - initialConfig: Record - ) => void | (() => void) | Promise void)>; - /** - * 响应式字段变化回调 - 当标记为 reactive 的字段值变化时调用 - */ - plugin_on_config_change?: ( - ctx: NapCatPluginContext, - ui: PluginConfigUIController, - key: string, - value: any, - currentConfig: Record - ) => void | Promise; -} - -export interface LoadedPlugin { - name: string; - fileId: string; // 文件系统目录名,用于路径解析 - version?: string; - pluginPath: string; - entryPath: string; - packageJson?: PluginPackageJson; - module: PluginModule; - context: NapCatPluginContext; // Store context -} - -export interface PluginStatusConfig { - [key: string]: boolean; // key: pluginName, value: enabled -} - -export class OB11PluginMangerAdapter extends IOB11NetworkAdapter { +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 class OB11PluginMangerAdapter extends IOB11NetworkAdapter implements IPluginManager { private readonly pluginPath: string; private readonly configPath: string; - /** 插件注册表: 包名(id) -> 插件数据 */ - private pluginRegistry: Map = new Map(); - /** 失败的插件: ID -> 错误信息 */ - private failedPlugins: Map = new Map(); + private readonly loader: PluginLoader; + + /** 插件注册表: ID -> 插件条目 */ + private plugins: Map = new Map(); + declare config: PluginConfig; public NapCatConfig = NapCatConfig; override get isActive (): boolean { - return this.isEnable && this.pluginRegistry.size > 0; + return this.isEnable && this.getLoadedPlugins().length > 0; } constructor ( @@ -191,218 +56,133 @@ export class OB11PluginMangerAdapter extends IOB11NetworkAdapter { 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); } - private loadPluginConfig (): PluginStatusConfig { - if (fs.existsSync(this.configPath)) { - try { - return JSON.parse(fs.readFileSync(this.configPath, 'utf-8')); - } catch (e) { - this.logger.logWarn('[Plugin Adapter] Error parsing plugins.json', e); + // ==================== 插件状态配置 ==================== + + public getPluginConfig (): PluginStatusConfig { + return this.loader.loadPluginStatusConfig(); + } + + private savePluginConfig (config: PluginStatusConfig): void { + this.loader.savePluginStatusConfig(config); + } + + // ==================== 插件扫描与加载 ==================== + + /** + * 扫描并加载所有插件 + */ + private async scanAndLoadPlugins (): Promise { + // 扫描所有插件目录 + 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); } } - return {}; - } - private savePluginConfig (config: PluginStatusConfig) { - try { - fs.writeFileSync(this.configPath, JSON.stringify(config, null, 2), 'utf-8'); - } catch (e) { - this.logger.logError('[Plugin Adapter] Error saving plugins.json', e); - } + const loadedCount = this.getLoadedPlugins().length; + this.logger.log(`[PluginManager] Loaded ${loadedCount} plugins`); } /** - * 扫描并加载插件 + * 加载单个插件 */ - private async loadPlugins (): Promise { - try { - // 确保插件目录存在 - if (!fs.existsSync(this.pluginPath)) { - this.logger.logWarn( - `[Plugin Adapter] Plugin directory does not exist: ${this.pluginPath}` - ); - fs.mkdirSync(this.pluginPath, { recursive: true }); - return; - } - - const items = fs.readdirSync(this.pluginPath, { withFileTypes: true }); - const pluginConfig = this.loadPluginConfig(); - - // 扫描文件和目录 (Only support directories as plugins now) - for (const item of items) { - if (!item.isDirectory()) { - continue; - } - - // 先读取 package.json 获取包名(id) - const packageJsonPath = path.join(this.pluginPath, item.name, 'package.json'); - let pluginId = item.name; // 默认使用目录名 - if (fs.existsSync(packageJsonPath)) { - try { - const pkg = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8')); - if (pkg.name) { - pluginId = pkg.name; - } - } catch (e) { } - } - - // Check if plugin is disabled in config (by id) - if (pluginConfig[pluginId] === false) { - this.logger.log(`[Plugin Adapter] Plugin ${pluginId} is disabled in config, skipping`); - continue; - } - - // 处理目录插件 - await this.loadDirectoryPlugin(item.name); - } - - this.logger.log( - `[Plugin Adapter] Loaded ${this.pluginRegistry.size} plugins` - ); - } catch (error) { - this.logger.logError('[Plugin Adapter] Error loading plugins:', error); + private async loadPlugin (entry: PluginEntry): Promise { + if (entry.loaded) { + return true; } - } - // loadFilePlugin removed + if (entry.runtime.status === 'error') { + return false; + } - /** - * 加载目录插件 - */ - public async loadDirectoryPlugin (dirname: string): Promise { - const pluginDir = path.join(this.pluginPath, dirname); + // 加载模块 + const module = await this.loader.loadPluginModule(entry); + if (!module) { + return false; + } + // 创建上下文 + const context = this.createPluginContext(entry); + + // 初始化插件 try { - // 尝试读取 package.json - let packageJson: PluginPackageJson | undefined; - const packageJsonPath = path.join(pluginDir, 'package.json'); + await module.plugin_init(context); - if (fs.existsSync(packageJsonPath)) { - try { - const packageContent = fs.readFileSync(packageJsonPath, 'utf-8'); - packageJson = JSON.parse(packageContent); - } catch (error) { - this.logger.logWarn( - `[Plugin Adapter] Invalid package.json in ${dirname}:`, - error - ); - } - } - - // 获取插件 id(包名) - const pluginId = packageJson?.name || dirname; - - // 检查插件是否被禁用 (by id) - const pluginConfig = this.loadPluginConfig(); - if (pluginConfig[pluginId] === false) { - this.logger.log(`[Plugin Adapter] Plugin ${pluginId} is disabled by user`); - return; - } - - // 确定入口文件 - const entryFile = this.findEntryFile(pluginDir, packageJson); - if (!entryFile) { - this.logger.logWarn( - `[Plugin Adapter] No valid entry file found for plugin directory: ${dirname}` - ); - return; - } - - const entryPath = path.join(pluginDir, entryFile); - const module = await this.importModule(entryPath); - - if (!this.isValidPluginModule(module)) { - this.logger.logWarn( - `[Plugin Adapter] Directory ${dirname} does not contain a valid plugin` - ); - return; - } - - const plugin: LoadedPlugin = { - name: pluginId, // 使用包名作为 id - fileId: dirname, // 保留目录名用于路径解析 - version: packageJson?.version, - pluginPath: pluginDir, - entryPath, - packageJson, + entry.loaded = true; + entry.runtime = { + status: 'loaded', module, - context: {} as NapCatPluginContext // Will be populated in registerPlugin + context, }; - await this.registerPlugin(plugin); - } catch (error) { - this.logger.logError( - `[Plugin Adapter] Error loading directory plugin ${dirname}:`, - error - ); + 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 findEntryFile ( - pluginDir: string, - packageJson?: PluginPackageJson - ): string | null { - // 优先级:package.json main > 默认文件名 - 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; - } - - - - /** - * 动态导入模块 - */ - private async importModule (filePath: string): Promise { - const fileUrl = `file://${filePath.replace(/\\/g, '/')}`; - // Add timestamp to force reload cache if supported or just import - // Note: dynamic import caching is tricky in ESM. Adding query param might help? - const fileUrlWithQuery = `${fileUrl}?t=${Date.now()}`; - return await import(fileUrlWithQuery); - } - - /** - * 检查模块是否为有效的插件模块 - */ - private isValidPluginModule (module: any): module is PluginModule { - return module && typeof module.plugin_init === 'function'; - } - - /** - * 注册插件 - */ - private async registerPlugin (plugin: LoadedPlugin): Promise { - // 检查名称冲突 - if (this.pluginRegistry.has(plugin.name)) { - this.logger.logWarn( - `[Plugin Adapter] Plugin name conflict: ${plugin.name}, skipping...` - ); + private async unloadPlugin (entry: PluginEntry): Promise { + if (!entry.loaded || entry.runtime.status !== 'loaded') { return; } - // Create Context - const dataPath = path.join(plugin.pluginPath, 'data'); + 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'); - // Create plugin-specific logger with prefix - const pluginPrefix = `[Plugin: ${plugin.name}]`; + // 创建插件专用日志器 + const pluginPrefix = `[Plugin: ${entry.id}]`; const coreLogger = this.logger; const pluginLogger: PluginLogger = { log: (...args: any[]) => coreLogger.log(pluginPrefix, ...args), @@ -412,255 +192,127 @@ export class OB11PluginMangerAdapter extends IOB11NetworkAdapter { error: (...args: any[]) => coreLogger.logError(pluginPrefix, ...args), }; - const context: NapCatPluginContext = { + return { core: this.core, oneBot: this.obContext, actions: this.actions, - pluginName: plugin.name, // Use package name for identification - pluginPath: plugin.pluginPath, - dataPath: dataPath, - configPath: configPath, - NapCatConfig: NapCatConfig, + pluginName: entry.id, + pluginPath: entry.pluginPath, + dataPath, + configPath, + NapCatConfig, adapterName: this.name, pluginManager: this, - logger: pluginLogger + logger: pluginLogger, }; - - plugin.context = context; // Store context on plugin object - - // 注册到映射表 - this.pluginRegistry.set(plugin.name, plugin); - - this.logger.log( - `[Plugin Adapter] Registered plugin: ${plugin.name}${plugin.version ? ` v${plugin.version}` : '' - }` - ); - - // 调用插件初始化方法(必须存在) - try { - await plugin.module.plugin_init(context); - this.logger.log(`[Plugin Adapter] Initialized plugin: ${plugin.name}`); - } 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.pluginRegistry.delete(plugin.name); - } } + // ==================== 公共 API ==================== + /** - * 卸载插件 + * 获取插件目录路径 */ - private async unloadPlugin (pluginName: string): Promise { - const plugin = this.pluginRegistry.get(pluginName); - if (!plugin) { - return; - } - - // 调用插件清理方法 - if (typeof plugin.module.plugin_cleanup === 'function') { - try { - await plugin.module.plugin_cleanup(plugin.context); - this.logger.log(`[Plugin Adapter] Cleaned up plugin: ${pluginName}`); - } catch (error) { - this.logger.logError( - `[Plugin Adapter] Error cleaning up plugin ${pluginName}:`, - error - ); - } - } - - // 从映射表中移除 - this.pluginRegistry.delete(pluginName); - this.logger.log(`[Plugin Adapter] Unloaded plugin: ${pluginName}`); - } - - public async unregisterPlugin (pluginName: string): Promise { - return this.unloadPlugin(pluginName); - } - public getPluginPath (): string { return this.pluginPath; } - public getPluginConfig (): PluginStatusConfig { - return this.loadPluginConfig(); - } - /** - * 设置插件状态(启用/禁用) - * @param pluginId 插件包名(id) - * @param enable 是否启用 + * 获取所有插件条目 */ - public setPluginStatus (pluginId: string, enable: boolean): void { - const config = this.loadPluginConfig(); - config[pluginId] = enable; - this.savePluginConfig(config); - - // 如果禁用且插件已加载,则卸载 - if (!enable) { - const plugin = this.pluginRegistry.get(pluginId); - if (plugin) { - this.unloadPlugin(pluginId).catch(e => this.logger.logError('Error unloading', e)); - } - } - } - - async onEvent (event: T) { - if (!this.isEnable) { - return; - } - - try { - await Promise.allSettled( - Array.from(this.pluginRegistry.values()).map((plugin) => - this.callPluginEventHandler(plugin, event) - ) - ); - } catch (error) { - this.logger.logError('[Plugin Adapter] Error handling event:', error); - } - } - - /** - * 调用插件的事件处理方法 - */ - private async callPluginEventHandler ( - plugin: LoadedPlugin, - event: OB11EmitEventContent - ): Promise { - try { - // 优先使用 plugin_onevent 方法 - if (typeof plugin.module.plugin_onevent === 'function') { - await plugin.module.plugin_onevent( - plugin.context, - event - ); - } - - // 如果是消息事件并且插件有 plugin_onmessage 方法,也调用 - if ( - (event as any).message_type && - typeof plugin.module.plugin_onmessage === 'function' - ) { - await plugin.module.plugin_onmessage( - plugin.context, - event as OB11Message - ); - } - } catch (error) { - this.logger.logError( - `[Plugin Adapter] Error calling plugin ${plugin.name} event handler:`, - error - ); - } - } - - async open () { - if (this.isEnable) { - return; - } - - this.logger.log('[Plugin Adapter] Opening plugin adapter...'); - this.isEnable = true; - - // 加载所有插件 - await this.loadPlugins(); - - this.logger.log( - `[Plugin Adapter] Plugin adapter opened with ${this.pluginRegistry.size} plugins loaded` - ); - } - - async close () { - if (!this.isEnable) { - return; - } - - this.logger.log('[Plugin Adapter] Closing plugin adapter...'); - this.isEnable = false; - - // 卸载所有插件 - const pluginNames = Array.from(this.pluginRegistry.keys()); - for (const pluginName of pluginNames) { - await this.unloadPlugin(pluginName); - } - - this.logger.log('[Plugin Adapter] Plugin adapter closed'); - } - - async reload () { - this.logger.log('[Plugin Adapter] Reloading plugin adapter...'); - - // 先关闭然后重新打开 - await this.close(); - await this.open(); - - this.logger.log('[Plugin Adapter] Plugin adapter reloaded'); - return OB11NetworkReloadType.Normal; + public getAllPlugins (): PluginEntry[] { + return Array.from(this.plugins.values()); } /** * 获取已加载的插件列表 */ - public getLoadedPlugins (): LoadedPlugin[] { - return Array.from(this.pluginRegistry.values()); + public getLoadedPlugins (): PluginEntry[] { + return Array.from(this.plugins.values()).filter(p => p.loaded); } /** - * 通过包名(id)获取插件信息 + * 通过 ID 获取插件信息 */ - public getPluginInfo (pluginId: string): LoadedPlugin | undefined { - return this.pluginRegistry.get(pluginId); + public getPluginInfo (pluginId: string): PluginEntry | undefined { + return this.plugins.get(pluginId); } /** - * 通过 id 加载插件 + * 设置插件状态(启用/禁用) */ - public async loadPluginById (id: string): Promise { - // 扫描文件系统查找 fileId - if (!fs.existsSync(this.pluginPath)) { - this.logger.logWarn(`[Plugin Adapter] Plugin ${id} not found in filesystem`); - return false; - } + public async setPluginStatus (pluginId: string, enable: boolean): Promise { + const config = this.getPluginConfig(); + config[pluginId] = enable; + this.savePluginConfig(config); - const items = fs.readdirSync(this.pluginPath, { withFileTypes: true }); - for (const item of items) { - if (!item.isDirectory()) continue; + const entry = this.plugins.get(pluginId); + if (entry) { + entry.enable = enable; - 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 === id) { - await this.loadDirectoryPlugin(item.name); - return this.pluginRegistry.has(id); - } - } catch (e) { } + if (enable && !entry.loaded) { + // 启用插件 + await this.loadPlugin(entry); + } else if (!enable && entry.loaded) { + // 禁用插件 + await this.unloadPlugin(entry); } } + } - this.logger.logWarn(`[Plugin Adapter] Plugin ${id} not found in filesystem`); - return false; + /** + * 通过 ID 加载插件 + */ + public async loadPluginById (pluginId: string): Promise { + 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 { + const entry = this.plugins.get(pluginId); + if (entry) { + await this.unloadPlugin(entry); + } } /** * 卸载并删除插件 */ - public async uninstallPlugin (id: string, cleanData: boolean = false): Promise { - const plugin = this.pluginRegistry.get(id); - if (!plugin) { - throw new Error(`Plugin ${id} not found or not loaded`); + public async uninstallPlugin (pluginId: string, cleanData: boolean = false): Promise { + const entry = this.plugins.get(pluginId); + if (!entry) { + throw new Error(`Plugin ${pluginId} not found`); } - const pluginPath = plugin.context.pluginPath; - const dataPath = plugin.context.dataPath; + const pluginPath = entry.pluginPath; + const dataPath = path.join(pluginPath, 'data'); - // 先卸载插件 - await this.unloadPlugin(id); + if (entry.loaded) { + await this.unloadPlugin(entry); + } + + // 从注册表移除 + this.plugins.delete(pluginId); // 删除插件目录 if (fs.existsSync(pluginPath)) { @@ -677,43 +329,69 @@ export class OB11PluginMangerAdapter extends IOB11NetworkAdapter { * 重载指定插件 */ public async reloadPlugin (pluginId: string): Promise { - const plugin = this.pluginRegistry.get(pluginId); - if (!plugin) { - this.logger.logWarn(`[Plugin Adapter] Plugin ${pluginId} not found`); + const entry = this.plugins.get(pluginId); + if (!entry) { + this.logger.logWarn(`[PluginManager] Plugin ${pluginId} not found`); return false; } - const dirname = path.basename(plugin.pluginPath); - try { // 卸载插件 - await this.unloadPlugin(pluginId); + await this.unloadPlugin(entry); - // 重新加载插件 - await this.loadDirectoryPlugin(dirname); + // 重新扫描 + const newEntry = this.loader.rescanPlugin(entry.fileId); + if (!newEntry) { + return false; + } - this.logger.log( - `[Plugin Adapter] Plugin ${pluginId} reloaded successfully` - ); + // 更新注册表 + 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( - `[Plugin Adapter] Error reloading plugin ${pluginId}:`, - error - ); + this.logger.logError(`[PluginManager] Error reloading plugin ${pluginId}:`, error); return false; } } + /** + * 加载目录插件(用于新安装的插件) + */ + public async loadDirectoryPlugin (dirname: string): Promise { + 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 plugin = this.pluginRegistry.get(pluginId); - if (!plugin) { + const entry = this.plugins.get(pluginId); + if (!entry) { throw new Error(`Plugin ${pluginId} not found`); } - return plugin.context.dataPath; + return path.join(entry.pluginPath, 'data'); } /** @@ -722,4 +400,98 @@ export class OB11PluginMangerAdapter extends IOB11NetworkAdapter { public getPluginConfigPath (pluginId: string): string { return path.join(this.getPluginDataPath(pluginId), 'config.json'); } + + // ==================== 事件处理 ==================== + + async onEvent (event: T): Promise { + 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 { + 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 { + 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 { + 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 { + this.logger.log('[PluginManager] Reloading plugin manager...'); + + // 先关闭然后重新打开 + await this.close(); + await this.open(); + + this.logger.log('[PluginManager] Plugin manager reloaded'); + return OB11NetworkReloadType.Normal; + } } diff --git a/packages/napcat-onebot/network/plugin/config.ts b/packages/napcat-onebot/network/plugin/config.ts new file mode 100644 index 00000000..aafc395f --- /dev/null +++ b/packages/napcat-onebot/network/plugin/config.ts @@ -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; + } +} diff --git a/packages/napcat-onebot/network/plugin/index.ts b/packages/napcat-onebot/network/plugin/index.ts new file mode 100644 index 00000000..595fc6fa --- /dev/null +++ b/packages/napcat-onebot/network/plugin/index.ts @@ -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'; diff --git a/packages/napcat-onebot/network/plugin/loader.ts b/packages/napcat-onebot/network/plugin/loader.ts new file mode 100644 index 00000000..7c4b23c5 --- /dev/null +++ b/packages/napcat-onebot/network/plugin/loader.ts @@ -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 { + 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 { + const fileUrl = `file://${filePath.replace(/\\/g, '/')}`; + const fileUrlWithQuery = `${fileUrl}?t=${Date.now()}`; + return await import(fileUrlWithQuery); + } + + /** + * 加载插件模块 + */ + async loadPluginModule (entry: PluginEntry): Promise { + 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; + } +} diff --git a/packages/napcat-onebot/network/plugin/manager.ts b/packages/napcat-onebot/network/plugin/manager.ts new file mode 100644 index 00000000..a3efe0bb --- /dev/null +++ b/packages/napcat-onebot/network/plugin/manager.ts @@ -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 implements IPluginManager { + private readonly pluginPath: string; + private readonly configPath: string; + private readonly loader: PluginLoader; + + /** 插件注册表: ID -> 插件条目 */ + private plugins: Map = 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 { + // 扫描所有插件目录 + 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 { + 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 { + 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 { + 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 { + 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 { + const entry = this.plugins.get(pluginId); + if (entry) { + await this.unloadPlugin(entry); + } + } + + /** + * 卸载并删除插件 + */ + public async uninstallPlugin (pluginId: string, cleanData: boolean = false): Promise { + 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 { + 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 { + 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 (event: T): Promise { + 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 { + 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 { + 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 { + 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 { + this.logger.log('[PluginManager] Reloading plugin manager...'); + + // 先关闭然后重新打开 + await this.close(); + await this.open(); + + this.logger.log('[PluginManager] Plugin manager reloaded'); + return OB11NetworkReloadType.Normal; + } +} diff --git a/packages/napcat-onebot/network/plugin/types.ts b/packages/napcat-onebot/network/plugin/types.ts new file mode 100644 index 00000000..abc333db --- /dev/null +++ b/packages/napcat-onebot/network/plugin/types.ts @@ -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; + loadPluginById (pluginId: string): Promise; + unregisterPlugin (pluginId: string): Promise; + uninstallPlugin (pluginId: string, cleanData?: boolean): Promise; + reloadPlugin (pluginId: string): Promise; + loadDirectoryPlugin (dirname: string): Promise; + getPluginDataPath (pluginId: string): string; + getPluginConfigPath (pluginId: string): string; +} + +// ==================== 插件配置 UI 控制器 ==================== + +/** 插件配置 UI 控制器 - 用于动态控制配置界面 */ +export interface PluginConfigUIController { + /** 更新整个 schema */ + updateSchema: (schema: PluginConfigSchema) => void; + /** 更新单个字段 */ + updateField: (key: string, field: Partial) => void; + /** 移除字段 */ + removeField: (key: string) => void; + /** 添加字段 */ + addField: (field: PluginConfigItem, afterKey?: string) => void; + /** 显示字段 */ + showField: (key: string) => void; + /** 隐藏字段 */ + hideField: (key: string) => void; + /** 获取当前配置值 */ + getCurrentConfig: () => Record; +} + +// ==================== 插件日志接口 ==================== + +/** + * 插件日志接口 - 简化的日志 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 { + plugin_init: (ctx: NapCatPluginContext) => void | Promise; + plugin_onmessage?: ( + ctx: NapCatPluginContext, + event: OB11Message, + ) => void | Promise; + plugin_onevent?: ( + ctx: NapCatPluginContext, + event: T, + ) => void | Promise; + plugin_cleanup?: ( + ctx: NapCatPluginContext + ) => void | Promise; + plugin_config_schema?: PluginConfigSchema; + plugin_config_ui?: PluginConfigSchema; + plugin_get_config?: (ctx: NapCatPluginContext) => unknown | Promise; + plugin_set_config?: (ctx: NapCatPluginContext, config: unknown) => void | Promise; + /** + * 配置界面控制器 - 当配置界面打开时调用 + * 返回清理函数,在界面关闭时调用 + */ + plugin_config_controller?: ( + ctx: NapCatPluginContext, + ui: PluginConfigUIController, + initialConfig: Record + ) => void | (() => void) | Promise void)>; + /** + * 响应式字段变化回调 - 当标记为 reactive 的字段值变化时调用 + */ + plugin_on_config_change?: ( + ctx: NapCatPluginContext, + ui: PluginConfigUIController, + key: string, + value: unknown, + currentConfig: Record + ) => void | Promise; +} + +// ==================== 插件运行时状态 ==================== + +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 +} diff --git a/packages/napcat-onebot/network/plugin.ts b/packages/napcat-onebot/network/plugin_develop.ts similarity index 100% rename from packages/napcat-onebot/network/plugin.ts rename to packages/napcat-onebot/network/plugin_develop.ts diff --git a/packages/napcat-webui-backend/src/api/Plugin.ts b/packages/napcat-webui-backend/src/api/Plugin.ts index fe8dfe27..484c0777 100644 --- a/packages/napcat-webui-backend/src/api/Plugin.ts +++ b/packages/napcat-webui-backend/src/api/Plugin.ts @@ -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(); // 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); + } +}; diff --git a/packages/napcat-webui-frontend/src/config/site.tsx b/packages/napcat-webui-frontend/src/config/site.tsx index 3332a92c..54aabae0 100644 --- a/packages/napcat-webui-frontend/src/config/site.tsx +++ b/packages/napcat-webui-frontend/src/config/site.tsx @@ -36,11 +36,6 @@ export const siteConfig = { icon: , href: '/network', }, - { - label: '其他配置', - icon: , - href: '/config', - }, { label: '猫猫日志', icon: , @@ -76,6 +71,11 @@ export const siteConfig = { icon: , href: '/terminal', }, + { + label: '系统配置', + icon: , + href: '/config', + }, { label: '关于我们', icon: ,