This commit is contained in:
ny 2025-06-30 10:44:54 +08:00
parent f99f9a945c
commit 64f0d81eb5
7 changed files with 121 additions and 62 deletions

22
.eslintignore Normal file
View File

@ -0,0 +1,22 @@
# Build output
build/
# Node modules
node_modules/
# Logs
logs/
# Environment files
.env
.env.local
.env.*.local
# OS generated files
.DS_Store
.DS_Store?
._*
.Spotlight-V100
.Trashes
ehthumbs.db
Thumbs.db

View File

@ -9,22 +9,24 @@ export default class ExamplePlugin implements IPlugin {
async initialize(): Promise<void> { async initialize(): Promise<void> {
logger.info(`[${this.name}] Plugin initialized successfully!`); logger.info(`[${this.name}] Plugin initialized successfully!`);
// Add your plugin initialization logic here // Add your plugin initialization logic here
// For example: // For example:
// - Register new routes // - Register new routes
// - Add new API endpoints // - Add new API endpoints
// - Set up event listeners // - Set up event listeners
// - Connect to external services // - Connect to external services
await Promise.resolve(); // Simulate async operation if needed
} }
async cleanup(): Promise<void> { async cleanup(): Promise<void> {
logger.info(`[${this.name}] Plugin cleanup completed`); logger.info(`[${this.name}] Plugin cleanup completed`);
// Add your cleanup logic here // Add your cleanup logic here
// For example: // For example:
// - Close database connections // - Close database connections
// - Clear timers/intervals // - Clear timers/intervals
// - Remove event listeners // - Remove event listeners
await Promise.resolve(); // Simulate async operation if needed
} }
} }

View File

@ -1,11 +1,11 @@
{ {
"name": "ExamplePlugin", "name": "ExamplePlugin",
"version": "1.0.0", "version": "1.0.0",
"description": "Example plugin for the server", "description": "Example plugin for the server",
"author": "Your Name", "author": "Your Name",
"main": "index.js", "main": "index.js",
"dependencies": [], "dependencies": [],
"config": { "config": {
"enabled": false "enabled": false
} }
} }

View File

@ -3,11 +3,11 @@
* This script runs during build time to discover all plugins and generate a registry * This script runs during build time to discover all plugins and generate a registry
*/ */
const fs = require('fs'); const fs = require("fs");
const path = require('path'); const path = require("path");
class PluginDiscovery { class PluginDiscovery {
constructor(pluginsDir = 'plugins', outputPath = 'build/plugin-registry.json') { constructor(pluginsDir = "plugins", outputPath = "build/plugin-registry.json") {
this.pluginsDir = path.resolve(pluginsDir); this.pluginsDir = path.resolve(pluginsDir);
this.outputPath = path.resolve(outputPath); this.outputPath = path.resolve(outputPath);
} }
@ -25,11 +25,12 @@ class PluginDiscovery {
console.log(`🔍 Discovering plugins in: ${this.pluginsDir}`); console.log(`🔍 Discovering plugins in: ${this.pluginsDir}`);
if (!fs.existsSync(this.pluginsDir)) { if (!fs.existsSync(this.pluginsDir)) {
console.log('⚠️ Plugins directory not found, creating empty registry'); console.log("⚠️ Plugins directory not found, creating empty registry");
return registry; return registry;
} }
const pluginDirs = fs.readdirSync(this.pluginsDir, { withFileTypes: true }) const pluginDirs = fs
.readdirSync(this.pluginsDir, { withFileTypes: true })
.filter(dirent => dirent.isDirectory()) .filter(dirent => dirent.isDirectory())
.map(dirent => dirent.name); .map(dirent => dirent.name);
@ -55,13 +56,13 @@ class PluginDiscovery {
*/ */
loadPluginManifest(pluginPath) { loadPluginManifest(pluginPath) {
try { try {
const manifestPath = path.join(pluginPath, 'plugin.json'); const manifestPath = path.join(pluginPath, "plugin.json");
if (!fs.existsSync(manifestPath)) { if (!fs.existsSync(manifestPath)) {
return null; return null;
} }
const manifestContent = fs.readFileSync(manifestPath, 'utf-8'); const manifestContent = fs.readFileSync(manifestPath, "utf-8");
const manifest = JSON.parse(manifestContent); const manifest = JSON.parse(manifestContent);
// Validate required fields // Validate required fields
@ -72,8 +73,8 @@ class PluginDiscovery {
// Check if main file exists // Check if main file exists
const mainFile = path.join(pluginPath, manifest.main); const mainFile = path.join(pluginPath, manifest.main);
const mainTsFile = mainFile.replace(/\.js$/, '.ts'); const mainTsFile = mainFile.replace(/\.js$/, ".ts");
if (!fs.existsSync(mainFile) && !fs.existsSync(mainTsFile)) { if (!fs.existsSync(mainFile) && !fs.existsSync(mainTsFile)) {
console.log(`⚠️ Main file not found: ${mainFile} or ${mainTsFile}`); console.log(`⚠️ Main file not found: ${mainFile} or ${mainTsFile}`);
return null; return null;
@ -109,12 +110,12 @@ class PluginDiscovery {
* Main execution function * Main execution function
*/ */
run() { run() {
console.log('🚀 Starting plugin discovery...'); console.log("🚀 Starting plugin discovery...");
const registry = this.discoverPlugins(); const registry = this.discoverPlugins();
this.saveRegistry(registry); this.saveRegistry(registry);
console.log('✨ Plugin discovery completed!'); console.log("✨ Plugin discovery completed!");
} }
} }

View File

@ -31,16 +31,18 @@ JSON.stringify = JSONStringify;
validateConfig(); validateConfig();
// Handle graceful shutdown // Handle graceful shutdown
process.on('SIGINT', async () => { process.on("SIGINT", () => {
logger.info('Received SIGINT, starting graceful shutdown...'); logger.info("Received SIGINT, starting graceful shutdown...");
await pluginManager.cleanup(); void pluginManager.cleanup().then(() => {
process.exit(0); process.exit(0);
});
}); });
process.on('SIGTERM', async () => { process.on("SIGTERM", () => {
logger.info('Received SIGTERM, starting graceful shutdown...'); logger.info("Received SIGTERM, starting graceful shutdown...");
await pluginManager.cleanup(); void pluginManager.cleanup().then(() => {
process.exit(0); process.exit(0);
});
}); });
mongoose mongoose

View File

@ -4,6 +4,24 @@ import fs from "fs";
import path from "path"; import path from "path";
import { pathToFileURL } from "url"; import { pathToFileURL } from "url";
interface PluginRegistry {
plugins: string[];
}
interface PluginModule {
default?:
| (new () => IPlugin)
| {
default?: new () => IPlugin;
};
[key: string]: unknown;
}
interface PluginConfig {
enabled?: boolean;
[key: string]: unknown;
}
export class PluginManager { export class PluginManager {
private plugins: Map<string, IPlugin> = new Map(); private plugins: Map<string, IPlugin> = new Map();
private pluginInfos: Map<string, PluginInfo> = new Map(); private pluginInfos: Map<string, PluginInfo> = new Map();
@ -25,7 +43,7 @@ export class PluginManager {
return; return;
} }
const pluginRegistry = JSON.parse(fs.readFileSync(pluginRegistryPath, "utf-8")); const pluginRegistry = JSON.parse(fs.readFileSync(pluginRegistryPath, "utf-8")) as PluginRegistry;
logger.info(`Found ${pluginRegistry.plugins.length} plugins in registry`); logger.info(`Found ${pluginRegistry.plugins.length} plugins in registry`);
for (const pluginPath of pluginRegistry.plugins) { for (const pluginPath of pluginRegistry.plugins) {
@ -45,19 +63,20 @@ export class PluginManager {
try { try {
const absolutePath = path.resolve(pluginPath); const absolutePath = path.resolve(pluginPath);
const manifestPath = path.join(absolutePath, "plugin.json"); const manifestPath = path.join(absolutePath, "plugin.json");
if (!fs.existsSync(manifestPath)) { if (!fs.existsSync(manifestPath)) {
logger.warn(`Plugin manifest not found: ${manifestPath}`); logger.warn(`Plugin manifest not found: ${manifestPath}`);
return; return;
} }
const manifest: PluginManifest = JSON.parse(fs.readFileSync(manifestPath, "utf-8")); const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf-8")) as PluginManifest;
// Check if plugin is enabled from config // Check if plugin is enabled from config
const pluginConfig = manifest.config as PluginConfig | undefined;
const pluginInfo: PluginInfo = { const pluginInfo: PluginInfo = {
manifest, manifest,
path: absolutePath, path: absolutePath,
enabled: manifest.config?.enabled ?? true enabled: pluginConfig?.enabled ?? true
}; };
if (!pluginInfo.enabled) { if (!pluginInfo.enabled) {
@ -74,16 +93,25 @@ export class PluginManager {
return; return;
} }
const pluginModule = await import(pathToFileURL(mainFile).href); const pluginModule = (await import(pathToFileURL(mainFile).href)) as PluginModule;
const PluginClass = pluginModule.default?.default || pluginModule.default || pluginModule[manifest.name];
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;
}
if (!PluginClass) { if (!PluginClass) {
logger.error(`Plugin class not found in ${mainFile}`); logger.error(`Plugin class not found in ${mainFile}`);
return; return;
} }
const plugin: IPlugin = new PluginClass(); const plugin = new PluginClass();
// Validate plugin interface // Validate plugin interface
if (!this.validatePlugin(plugin)) { if (!this.validatePlugin(plugin)) {
logger.error(`Plugin ${manifest.name} does not implement required interface`); logger.error(`Plugin ${manifest.name} does not implement required interface`);
@ -91,11 +119,11 @@ export class PluginManager {
} }
// Initialize the plugin // Initialize the plugin
await plugin.initialize(); await Promise.resolve(plugin.initialize());
this.plugins.set(manifest.name, plugin); this.plugins.set(manifest.name, plugin);
this.pluginInfos.set(manifest.name, pluginInfo); this.pluginInfos.set(manifest.name, pluginInfo);
logger.info(`Loaded plugin: ${manifest.name} v${manifest.version}`); logger.info(`Loaded plugin: ${manifest.name} v${manifest.version}`);
} catch (error) { } catch (error) {
logger.error(`Failed to load plugin from ${pluginPath}:`, error); logger.error(`Failed to load plugin from ${pluginPath}:`, error);
@ -105,12 +133,16 @@ export class PluginManager {
/** /**
* Validate that a plugin implements the required interface * Validate that a plugin implements the required interface
*/ */
private validatePlugin(plugin: any): plugin is IPlugin { private validatePlugin(plugin: unknown): plugin is IPlugin {
return ( return (
plugin && plugin != null &&
typeof plugin.name === "string" && typeof plugin === "object" &&
typeof plugin.version === "string" && "name" in plugin &&
typeof plugin.initialize === "function" "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"
); );
} }
@ -140,18 +172,18 @@ export class PluginManager {
*/ */
async cleanup(): Promise<void> { async cleanup(): Promise<void> {
logger.info("Cleaning up plugins..."); logger.info("Cleaning up plugins...");
for (const [name, plugin] of this.plugins) { for (const [name, plugin] of this.plugins) {
try { try {
if (plugin.cleanup) { if (plugin.cleanup) {
await plugin.cleanup(); await Promise.resolve(plugin.cleanup());
} }
logger.info(`Cleaned up plugin: ${name}`); logger.info(`Cleaned up plugin: ${name}`);
} catch (error) { } catch (error) {
logger.error(`Failed to cleanup plugin ${name}:`, error); logger.error(`Failed to cleanup plugin ${name}:`, error);
} }
} }
this.plugins.clear(); this.plugins.clear();
this.pluginInfos.clear(); this.pluginInfos.clear();
} }

View File

@ -3,36 +3,36 @@ export interface IPlugin {
* Plugin name * Plugin name
*/ */
name: string; name: string;
/** /**
* Plugin version * Plugin version
*/ */
version: string; version: string;
/** /**
* Plugin description * Plugin description
*/ */
description?: string; description?: string;
/** /**
* Plugin author * Plugin author
*/ */
author?: string; author?: string;
/** /**
* Initialize the plugin * Initialize the plugin
*/ */
initialize(): Promise<void> | void; initialize(): Promise<void> | void;
/** /**
* Cleanup plugin resources * Cleanup plugin resources
*/ */
cleanup?(): Promise<void> | void; cleanup?(): Promise<void> | void;
/** /**
* Plugin configuration * Plugin configuration
*/ */
config?: any; config?: Record<string, unknown>;
} }
export interface PluginManifest { export interface PluginManifest {
@ -42,7 +42,7 @@ export interface PluginManifest {
author?: string; author?: string;
main: string; main: string;
dependencies?: string[]; dependencies?: string[];
config?: any; config?: Record<string, unknown>;
} }
export interface PluginInfo { export interface PluginInfo {