NapCatQQ/packages/napcat-onebot/network/plugin.ts
手瓜一十雪 4360775eff
refactor: 整体重构 (#1381)
* feat: pnpm new

* Refactor build and release workflows, update dependencies

Switch build scripts and workflows from npm to pnpm, update build and artifact paths, and simplify release workflow by removing version detection and changelog steps. Add new dependencies (silk-wasm, express, ws, node-pty-prebuilt-multiarch), update exports in package.json files, and add vite config for napcat-framework. Also, rename manifest.json for framework package and fix static asset copying in shell build config.
2025-11-13 15:39:42 +08:00

374 lines
12 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { OB11EmitEventContent, OB11NetworkReloadType } from './index';
import { NapCatOneBot11Adapter, OB11Message } from '@/napcat-onebot/index';
import { NapCatCore } from 'napcat-core';
import { PluginConfig } from '../config/config';
import { ActionMap } from '../action';
import { IOB11NetworkAdapter } from '@/napcat-onebot/network/adapter';
import fs from 'fs';
import path from 'path';
export interface PluginPackageJson {
name?: string;
version?: string;
main?: string;
}
export interface PluginModule<T extends OB11EmitEventContent = OB11EmitEventContent> {
plugin_init: (core: NapCatCore, obContext: NapCatOneBot11Adapter, actions: ActionMap, instance: OB11PluginAdapter) => void | Promise<void>;
plugin_onmessage?: (adapter: string, core: NapCatCore, obCtx: NapCatOneBot11Adapter, event: OB11Message, actions: ActionMap, instance: OB11PluginAdapter) => void | Promise<void>;
plugin_onevent?: (adapter: string, core: NapCatCore, obCtx: NapCatOneBot11Adapter, event: T, actions: ActionMap, instance: OB11PluginAdapter) => void | Promise<void>;
plugin_cleanup?: (core: NapCatCore, obContext: NapCatOneBot11Adapter, actions: ActionMap, instance: OB11PluginAdapter) => void | Promise<void>;
}
export interface LoadedPlugin {
name: string;
version?: string;
pluginPath: string;
entryPath: string;
packageJson?: PluginPackageJson;
module: PluginModule;
}
export class OB11PluginAdapter extends IOB11NetworkAdapter<PluginConfig> {
private readonly pluginPath: string;
private loadedPlugins: Map<string, LoadedPlugin> = new Map();
declare config: PluginConfig;
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;
}
/**
* 扫描并加载插件
*/
private async loadPlugins (): Promise<void> {
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 });
// 扫描文件和目录
for (const item of items) {
if (item.isFile()) {
// 处理单文件插件
await this.loadFilePlugin(item.name);
} else if (item.isDirectory()) {
// 处理目录插件
await this.loadDirectoryPlugin(item.name);
}
}
this.logger.log(`[Plugin Adapter] Loaded ${this.loadedPlugins.size} plugins`);
} catch (error) {
this.logger.logError('[Plugin Adapter] Error loading plugins:', error);
}
}
/**
* 加载单文件插件 (.mjs, .js)
*/
private async loadFilePlugin (filename: string): Promise<void> {
// 只处理支持的文件类型
if (!this.isSupportedFile(filename)) {
return;
}
const filePath = path.join(this.pluginPath, filename);
const pluginName = path.parse(filename).name;
try {
const module = await this.importModule(filePath);
if (!this.isValidPluginModule(module)) {
this.logger.logWarn(`[Plugin Adapter] File ${filename} is not a valid plugin (missing plugin methods)`);
return;
}
const plugin: LoadedPlugin = {
name: pluginName,
pluginPath: this.pluginPath,
entryPath: filePath,
module,
};
await this.registerPlugin(plugin);
} catch (error) {
this.logger.logError(`[Plugin Adapter] Error loading file plugin ${filename}:`, error);
}
}
/**
* 加载目录插件
*/
private async loadDirectoryPlugin (dirname: string): Promise<void> {
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(`[Plugin Adapter] Invalid package.json in ${dirname}:`, error);
}
}
// 确定入口文件
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: packageJson?.name || dirname,
version: packageJson?.version,
pluginPath: pluginDir,
entryPath,
packageJson,
module,
};
await this.registerPlugin(plugin);
} catch (error) {
this.logger.logError(`[Plugin Adapter] Error loading directory plugin ${dirname}:`, error);
}
}
/**
* 查找插件目录的入口文件
*/
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 isSupportedFile (filename: string): boolean {
const ext = path.extname(filename).toLowerCase();
return ['.mjs', '.js'].includes(ext);
}
/**
* 动态导入模块
*/
private async importModule (filePath: string): Promise<any> {
const fileUrl = `file://${filePath.replace(/\\/g, '/')}`;
return await import(fileUrl);
}
/**
* 检查模块是否为有效的插件模块
*/
private isValidPluginModule (module: any): module is PluginModule {
return module && typeof module.plugin_init === 'function';
}
/**
* 注册插件
*/
private async registerPlugin (plugin: LoadedPlugin): Promise<void> {
// 检查名称冲突
if (this.loadedPlugins.has(plugin.name)) {
this.logger.logWarn(`[Plugin Adapter] Plugin name conflict: ${plugin.name}, skipping...`);
return;
}
this.loadedPlugins.set(plugin.name, plugin);
this.logger.log(`[Plugin Adapter] Registered plugin: ${plugin.name}${plugin.version ? ` v${plugin.version}` : ''}`);
// 调用插件初始化方法(必须存在)
try {
await plugin.module.plugin_init(this.core, this.obContext, this.actions, this);
this.logger.log(`[Plugin Adapter] Initialized plugin: ${plugin.name}`);
} catch (error) {
this.logger.logError(`[Plugin Adapter] Error initializing plugin ${plugin.name}:`, error);
}
}
/**
* 卸载插件
*/
private async unloadPlugin (pluginName: string): Promise<void> {
const plugin = this.loadedPlugins.get(pluginName);
if (!plugin) {
return;
}
// 调用插件清理方法
if (typeof plugin.module.plugin_cleanup === 'function') {
try {
await plugin.module.plugin_cleanup(this.core, this.obContext, this.actions, this);
this.logger.log(`[Plugin Adapter] Cleaned up plugin: ${pluginName}`);
} catch (error) {
this.logger.logError(`[Plugin Adapter] Error cleaning up plugin ${pluginName}:`, error);
}
}
this.loadedPlugins.delete(pluginName);
this.logger.log(`[Plugin Adapter] Unloaded plugin: ${pluginName}`);
}
async onEvent<T extends OB11EmitEventContent>(event: T) {
if (!this.isEnable) {
return;
}
// 遍历所有已加载的插件,调用它们的事件处理方法
for (const [, plugin] of this.loadedPlugins) {
this.callPluginEventHandler(plugin, event);
}
}
/**
* 调用插件的事件处理方法
*/
private async callPluginEventHandler (plugin: LoadedPlugin, event: OB11EmitEventContent): Promise<void> {
try {
// 优先使用 plugin_onevent 方法
if (typeof plugin.module.plugin_onevent === 'function') {
await plugin.module.plugin_onevent(this.name, this.core, this.obContext, event, this.actions, this);
}
// 如果是消息事件并且插件有 plugin_onmessage 方法,也调用
if ((event as any).message_type && typeof plugin.module.plugin_onmessage === 'function') {
await plugin.module.plugin_onmessage(this.name, this.core, this.obContext, event as OB11Message, this.actions, this);
}
} 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.loadedPlugins.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.loadedPlugins.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 getLoadedPlugins (): LoadedPlugin[] {
return Array.from(this.loadedPlugins.values());
}
/**
* 获取插件信息
*/
public getPluginInfo (pluginName: string): LoadedPlugin | undefined {
return this.loadedPlugins.get(pluginName);
}
/**
* 重载指定插件
*/
public async reloadPlugin (pluginName: string): Promise<boolean> {
const plugin = this.loadedPlugins.get(pluginName);
if (!plugin) {
this.logger.logWarn(`[Plugin Adapter] Plugin ${pluginName} not found`);
return false;
}
try {
// 卸载插件
await this.unloadPlugin(pluginName);
// 重新加载插件
const isDirectory = fs.statSync(plugin.pluginPath).isDirectory() &&
plugin.pluginPath !== this.pluginPath;
if (isDirectory) {
const dirname = path.basename(plugin.pluginPath);
await this.loadDirectoryPlugin(dirname);
} else {
const filename = path.basename(plugin.entryPath);
await this.loadFilePlugin(filename);
}
this.logger.log(`[Plugin Adapter] Plugin ${pluginName} reloaded successfully`);
return true;
} catch (error) {
this.logger.logError(`[Plugin Adapter] Error reloading plugin ${pluginName}:`, error);
return false;
}
}
}