mirror of
https://github.com/NapNeko/NapCatQQ.git
synced 2026-02-04 06:31:13 +00:00
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:
parent
65bae6b57a
commit
40409a3841
File diff suppressed because it is too large
Load Diff
39
packages/napcat-onebot/network/plugin/config.ts
Normal file
39
packages/napcat-onebot/network/plugin/config.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
23
packages/napcat-onebot/network/plugin/index.ts
Normal file
23
packages/napcat-onebot/network/plugin/index.ts
Normal 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';
|
||||
298
packages/napcat-onebot/network/plugin/loader.ts
Normal file
298
packages/napcat-onebot/network/plugin/loader.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
487
packages/napcat-onebot/network/plugin/manager.ts
Normal file
487
packages/napcat-onebot/network/plugin/manager.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
222
packages/napcat-onebot/network/plugin/types.ts
Normal file
222
packages/napcat-onebot/network/plugin/types.ts
Normal 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
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
};
|
||||
|
||||
@ -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' />,
|
||||
|
||||
Loading…
Reference in New Issue
Block a user