2025-06-30 10:30:37 +08:00
|
|
|
import { logger } from "@/src/utils/logger";
|
|
|
|
import { IPlugin, PluginInfo, PluginManifest } from "@/src/types/pluginTypes";
|
|
|
|
import fs from "fs";
|
|
|
|
import path from "path";
|
|
|
|
import { pathToFileURL } from "url";
|
|
|
|
|
2025-06-30 10:44:54 +08:00
|
|
|
interface PluginRegistry {
|
|
|
|
plugins: string[];
|
|
|
|
}
|
|
|
|
|
|
|
|
interface PluginModule {
|
|
|
|
default?:
|
|
|
|
| (new () => IPlugin)
|
|
|
|
| {
|
|
|
|
default?: new () => IPlugin;
|
|
|
|
};
|
|
|
|
[key: string]: unknown;
|
|
|
|
}
|
|
|
|
|
|
|
|
interface PluginConfig {
|
|
|
|
enabled?: boolean;
|
|
|
|
[key: string]: unknown;
|
|
|
|
}
|
|
|
|
|
2025-06-30 10:30:37 +08:00
|
|
|
export class PluginManager {
|
|
|
|
private plugins: Map<string, IPlugin> = new Map();
|
|
|
|
private pluginInfos: Map<string, PluginInfo> = new Map();
|
|
|
|
private pluginsDir: string;
|
|
|
|
|
|
|
|
constructor(pluginsDir: string = "plugins") {
|
|
|
|
this.pluginsDir = path.resolve(pluginsDir);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Load all plugins from the plugins directory
|
|
|
|
*/
|
|
|
|
async loadPlugins(): Promise<void> {
|
|
|
|
try {
|
|
|
|
// Load plugin registry generated at build time
|
|
|
|
const pluginRegistryPath = path.resolve("build/plugin-registry.json");
|
|
|
|
if (!fs.existsSync(pluginRegistryPath)) {
|
|
|
|
logger.info("No plugin registry found, skipping plugin loading");
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2025-06-30 10:44:54 +08:00
|
|
|
const pluginRegistry = JSON.parse(fs.readFileSync(pluginRegistryPath, "utf-8")) as PluginRegistry;
|
2025-06-30 10:30:37 +08:00
|
|
|
logger.info(`Found ${pluginRegistry.plugins.length} plugins in registry`);
|
|
|
|
|
|
|
|
for (const pluginPath of pluginRegistry.plugins) {
|
|
|
|
await this.loadPlugin(pluginPath);
|
|
|
|
}
|
|
|
|
|
|
|
|
logger.info(`Successfully loaded ${this.plugins.size} plugins`);
|
|
|
|
} catch (error) {
|
|
|
|
logger.error("Failed to load plugins:", error);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Load a single plugin
|
|
|
|
*/
|
|
|
|
private async loadPlugin(pluginPath: string): Promise<void> {
|
|
|
|
try {
|
|
|
|
const absolutePath = path.resolve(pluginPath);
|
|
|
|
const manifestPath = path.join(absolutePath, "plugin.json");
|
2025-06-30 10:44:54 +08:00
|
|
|
|
2025-06-30 10:30:37 +08:00
|
|
|
if (!fs.existsSync(manifestPath)) {
|
|
|
|
logger.warn(`Plugin manifest not found: ${manifestPath}`);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2025-06-30 10:44:54 +08:00
|
|
|
const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf-8")) as PluginManifest;
|
|
|
|
|
2025-06-30 10:30:37 +08:00
|
|
|
// Check if plugin is enabled from config
|
2025-06-30 10:44:54 +08:00
|
|
|
const pluginConfig = manifest.config as PluginConfig | undefined;
|
2025-06-30 10:30:37 +08:00
|
|
|
const pluginInfo: PluginInfo = {
|
|
|
|
manifest,
|
|
|
|
path: absolutePath,
|
2025-06-30 10:44:54 +08:00
|
|
|
enabled: pluginConfig?.enabled ?? true
|
2025-06-30 10:30:37 +08:00
|
|
|
};
|
|
|
|
|
|
|
|
if (!pluginInfo.enabled) {
|
|
|
|
logger.info(`Plugin ${manifest.name} is disabled, skipping`);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Load the plugin module from build directory
|
|
|
|
const pluginName = path.basename(absolutePath);
|
|
|
|
const buildPluginPath = path.resolve("build/plugins", pluginName);
|
|
|
|
const mainFile = path.join(buildPluginPath, manifest.main || "index.js");
|
|
|
|
if (!fs.existsSync(mainFile)) {
|
|
|
|
logger.error(`Plugin main file not found: ${mainFile}`);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2025-06-30 10:44:54 +08:00
|
|
|
const pluginModule = (await import(pathToFileURL(mainFile).href)) as PluginModule;
|
|
|
|
|
|
|
|
let PluginClass: (new () => IPlugin) | undefined;
|
|
|
|
|
|
|
|
if (typeof pluginModule.default === "function") {
|
|
|
|
PluginClass = pluginModule.default;
|
|
|
|
} else if (typeof pluginModule.default === "object" && pluginModule.default.default) {
|
|
|
|
PluginClass = pluginModule.default.default;
|
|
|
|
} else if (pluginModule[manifest.name]) {
|
|
|
|
PluginClass = pluginModule[manifest.name] as new () => IPlugin;
|
|
|
|
}
|
|
|
|
|
2025-06-30 10:30:37 +08:00
|
|
|
if (!PluginClass) {
|
|
|
|
logger.error(`Plugin class not found in ${mainFile}`);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2025-06-30 10:44:54 +08:00
|
|
|
const plugin = new PluginClass();
|
|
|
|
|
2025-06-30 10:30:37 +08:00
|
|
|
// Validate plugin interface
|
|
|
|
if (!this.validatePlugin(plugin)) {
|
|
|
|
logger.error(`Plugin ${manifest.name} does not implement required interface`);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Initialize the plugin
|
2025-06-30 10:44:54 +08:00
|
|
|
await Promise.resolve(plugin.initialize());
|
|
|
|
|
2025-06-30 10:30:37 +08:00
|
|
|
this.plugins.set(manifest.name, plugin);
|
|
|
|
this.pluginInfos.set(manifest.name, pluginInfo);
|
2025-06-30 10:44:54 +08:00
|
|
|
|
2025-06-30 10:30:37 +08:00
|
|
|
logger.info(`Loaded plugin: ${manifest.name} v${manifest.version}`);
|
|
|
|
} catch (error) {
|
|
|
|
logger.error(`Failed to load plugin from ${pluginPath}:`, error);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Validate that a plugin implements the required interface
|
|
|
|
*/
|
2025-06-30 10:44:54 +08:00
|
|
|
private validatePlugin(plugin: unknown): plugin is IPlugin {
|
2025-06-30 10:30:37 +08:00
|
|
|
return (
|
2025-06-30 10:44:54 +08:00
|
|
|
plugin != null &&
|
|
|
|
typeof plugin === "object" &&
|
|
|
|
"name" in plugin &&
|
|
|
|
"version" in plugin &&
|
|
|
|
"initialize" in plugin &&
|
|
|
|
typeof (plugin as Record<string, unknown>).name === "string" &&
|
|
|
|
typeof (plugin as Record<string, unknown>).version === "string" &&
|
|
|
|
typeof (plugin as Record<string, unknown>).initialize === "function"
|
2025-06-30 10:30:37 +08:00
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get a plugin by name
|
|
|
|
*/
|
|
|
|
getPlugin(name: string): IPlugin | undefined {
|
|
|
|
return this.plugins.get(name);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get all loaded plugins
|
|
|
|
*/
|
|
|
|
getAllPlugins(): Map<string, IPlugin> {
|
|
|
|
return new Map(this.plugins);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get plugin info
|
|
|
|
*/
|
|
|
|
getPluginInfo(name: string): PluginInfo | undefined {
|
|
|
|
return this.pluginInfos.get(name);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Cleanup all plugins
|
|
|
|
*/
|
|
|
|
async cleanup(): Promise<void> {
|
|
|
|
logger.info("Cleaning up plugins...");
|
2025-06-30 10:44:54 +08:00
|
|
|
|
2025-06-30 10:30:37 +08:00
|
|
|
for (const [name, plugin] of this.plugins) {
|
|
|
|
try {
|
|
|
|
if (plugin.cleanup) {
|
2025-06-30 10:44:54 +08:00
|
|
|
await Promise.resolve(plugin.cleanup());
|
2025-06-30 10:30:37 +08:00
|
|
|
}
|
|
|
|
logger.info(`Cleaned up plugin: ${name}`);
|
|
|
|
} catch (error) {
|
|
|
|
logger.error(`Failed to cleanup plugin ${name}:`, error);
|
|
|
|
}
|
|
|
|
}
|
2025-06-30 10:44:54 +08:00
|
|
|
|
2025-06-30 10:30:37 +08:00
|
|
|
this.plugins.clear();
|
|
|
|
this.pluginInfos.clear();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Global plugin manager instance
|
|
|
|
export const pluginManager = new PluginManager();
|