UNPKG

mira-app-server

Version:

Mira Server - standalone server application using mira-app-core

540 lines 24.7 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); Object.defineProperty(exports, "__esModule", { value: true }); exports.ServerPluginManager = void 0; const sdk_1 = require("mira-app-core/shared/sdk"); const fs = __importStar(require("fs")); const path = __importStar(require("path")); class ServerPluginManager { constructor({ server, dbService, pluginsDir }) { this.loadedPlugins = new Map(); this.httpHooks = []; this.fileFormatHandlers = new Map(); this.fields = []; this.pluginsDir = path.join(pluginsDir ?? __dirname, 'plugins'); console.log({ pluginsDir: this.pluginsDir }); this.server = server; this.dbService = dbService; this.pluginsConfigPath = path.join(this.pluginsDir, 'plugins.json'); // 创建 MiraClient 实例用于插件 const httpPort = this.server.backend.config.httpPort || 8081; const baseURL = `http://localhost:${httpPort}`; this.miraClient = new sdk_1.MiraClient(baseURL); console.log(`🔗 Created MiraClient for plugins with baseURL: ${baseURL}`); // Ensure plugins directory exists if (!fs.existsSync(this.pluginsDir)) { fs.mkdirSync(this.pluginsDir, { recursive: true }); } // Initialize plugins.json if it doesn't exist if (!fs.existsSync(this.pluginsConfigPath)) { fs.writeFileSync(this.pluginsConfigPath, JSON.stringify([], null, 2)); } } // getPluginDir getPluginDir(pluginName) { const pluginConfig = this.getPluginConfig(pluginName); return path.join(this.pluginsDir, pluginConfig?.path ?? pluginName); } getPluginDistDir(pluginName) { return path.join(this.getPluginDir(pluginName), 'dist'); } getPluginWebDir(pluginName) { return path.join(this.getPluginDir(pluginName), 'web'); } getLoadedWebPlugins() { const plugins = []; for (const pluginName of this.loadedPlugins.keys()) { const webDir = this.getPluginWebDir(pluginName); const manifestPath = path.join(webDir, 'plugin.json'); if (!fs.existsSync(manifestPath)) continue; try { const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf-8')); const index = typeof manifest.index === 'string' ? manifest.index : 'index.js'; const resolvedIndex = path.resolve(webDir, index); const relativeIndex = path.relative(path.resolve(webDir), resolvedIndex); if (!manifest.pluginId || !manifest.pluginName || relativeIndex.startsWith('..') || path.isAbsolute(relativeIndex) || !fs.existsSync(resolvedIndex)) { console.warn(`Skipping invalid Web plugin manifest: ${manifestPath}`); continue; } // 从插件 package.json 的 mira.icon 提取图标 (emoji / material 名), 供前端展示 let pkgIcon; try { const pkgPath = path.join(this.getPluginDir(pluginName), 'package.json'); if (fs.existsSync(pkgPath)) { const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8')); pkgIcon = (pkg.mira && pkg.mira.icon) || pkg.icon; } } catch (e) { console.warn(`Error reading package.json icon for ${pluginName}:`, e); } plugins.push({ ...manifest, index, version: manifest.version || '1.0.0', serverPluginName: pluginName, icon: manifest.icon || pkgIcon, }); } catch (error) { console.error(`Error reading Web plugin manifest for ${pluginName}:`, error); } } return plugins; } getPluginConfig(pluginName) { if (!fs.existsSync(this.pluginsConfigPath)) return undefined; try { const config = JSON.parse(fs.readFileSync(this.pluginsConfigPath, 'utf-8')); return config.find(plugin => plugin.name === pluginName); } catch (error) { console.error(`Error reading plugin config for ${pluginName}:`, error); return undefined; } } async loadPlugins(reload = false) { const config = JSON.parse(fs.readFileSync(this.pluginsConfigPath, 'utf-8')); for (const pluginConfig of config) { if (pluginConfig.enabled) { await this.loadPlugin(pluginConfig, reload); } } } async loadPlugin(pluginConfig, reload = false) { try { // 检查插件是否已经加载过 if (!reload && this.loadedPlugins.has(pluginConfig.name)) { console.log(`Plugin ${pluginConfig.name} already loaded, skipping...`); return; } const pluginPath = path.join(this.pluginsDir, pluginConfig.path); // 如果是重新加载,清除require缓存 if (reload || this.loadedPlugins.has(pluginConfig.name)) { delete require.cache[require.resolve(pluginPath)]; } const pluginModule = require(pluginPath); if (typeof pluginModule.init === 'function') { const obj = await pluginModule.init({ pluginManager: this, server: this.server, dbService: this.dbService, miraClient: this.miraClient, }); this.loadedPlugins.set(pluginConfig.name, obj); console.log(`${reload ? 'Reloaded' : 'Loaded'} plugin: ${pluginConfig.name}`); } else { console.warn(`Plugin ${pluginConfig.name} does not have an init function, skipping...`); } } catch (err) { console.error(`Failed to load plugin ${pluginConfig.name}:`, err); // 如果加载失败,从已加载插件中移除 this.loadedPlugins.delete(pluginConfig.name); } } registerFields(fields) { for (const field of fields) { this.registerField(field); } } registerField(field) { let { action, type, field: fieldName } = field; if (!fieldName || !action || !type) { throw new Error('Field registration error: action, type, and field are required'); } this.fields.push(field); } registerHttpHook(hook) { if (!hook.path || typeof hook.handler !== 'function') { throw new Error('HTTP hook registration error: path and handler are required'); } this.httpHooks.push({ ...hook, method: hook.method?.toUpperCase(), }); } registerFileFormat(pluginName, handler) { if (!handler.id || (!handler.extensions?.length && !handler.mimeTypes?.length)) { throw new Error('File format registration error: id and extensions or mimeTypes are required'); } const key = `${pluginName}:${handler.id}`; const previous = this.fileFormatHandlers.get(key); if (previous?.handler.thumbnail) { this.server.backend.thumbnailService.unregisterGenerator(key); } if (previous?.handler.metadata) { this.server.backend.metadataService.unregisterRule(key); } this.fileFormatHandlers.set(key, { pluginName, handler }); if (handler.thumbnail) { const generator = { name: key, supportedExtensions: (handler.thumbnailExtensions || handler.extensions || []).map(ext => ext.replace(/^\./, '').toLowerCase()), generate: handler.thumbnail, }; this.server.backend.thumbnailService.registerGenerator(generator); } if (handler.metadata) { this.server.backend.metadataService.registerRule({ name: key, supportedExtensions: handler.extensions || [], parse: handler.metadata, }); } return () => this.unregisterFileFormat(pluginName, handler.id); } unregisterFileFormat(pluginName, id) { const key = `${pluginName}:${id}`; const entry = this.fileFormatHandlers.get(key); if (!entry) return false; if (entry.handler.thumbnail) this.server.backend.thumbnailService.unregisterGenerator(key); if (entry.handler.metadata) this.server.backend.metadataService.unregisterRule(key); return this.fileFormatHandlers.delete(key); } getFileFormatHandlers() { return Array.from(this.fileFormatHandlers.values()).map(({ handler }) => ({ ...handler })); } async getPreviewViewers(context) { const extension = String(context.file.extension || path.extname(context.file.name || context.filePath)).replace(/^\./, '').toLowerCase(); const mimeType = String(context.file.mimeType || context.file.mime_type || '').toLowerCase(); const manifests = new Map(this.getLoadedWebPlugins().map(manifest => [manifest.serverPluginName, manifest])); const resolved = []; for (const { pluginName, handler } of this.fileFormatHandlers.values()) { const handlerMatches = handler.extensions?.some(ext => ext.replace(/^\./, '').toLowerCase() === extension) || handler.mimeTypes?.some(mime => mime.toLowerCase() === mimeType); if (!handlerMatches || !handler.viewers?.length) continue; const manifest = manifests.get(pluginName); if (!manifest) continue; for (const viewer of handler.viewers) { const viewerMatches = (!viewer.extensions?.length && !viewer.mimeTypes?.length) || viewer.extensions?.some(ext => ext.replace(/^\./, '').toLowerCase() === extension) || viewer.mimeTypes?.some(mime => mime.toLowerCase() === mimeType); if (!viewerMatches) continue; const webDir = path.resolve(this.getPluginWebDir(pluginName)); const entryPath = path.resolve(webDir, viewer.entry); const relativeEntry = path.relative(webDir, entryPath); if (relativeEntry.startsWith('..') || path.isAbsolute(relativeEntry) || !fs.existsSync(entryPath)) { console.warn(`Skipping invalid preview Viewer entry: ${pluginName}/${viewer.entry}`); continue; } try { const query = await viewer.getQuery?.(context) || {}; const search = new URLSearchParams(); for (const [key, value] of Object.entries(query)) { if (value !== undefined && value !== null && value !== '') search.set(key, String(value)); } const encodedEntry = relativeEntry.split(path.sep).map(encodeURIComponent).join('/'); const iframePath = `/server-plugins/${encodeURIComponent(context.libraryId)}/${encodeURIComponent(pluginName)}/${encodedEntry}`; const searchString = search.toString(); resolved.push({ viewerId: viewer.viewerId, pluginId: manifest.pluginId, pluginName: manifest.pluginName, serverPluginName: pluginName, title: viewer.title, iframeUrl: searchString ? `${iframePath}?${searchString}` : iframePath, priority: viewer.priority ?? (Number(manifest.priority) || 0), icon: viewer.icon, }); } catch (error) { console.warn(`Failed to resolve preview Viewer ${pluginName}:${viewer.viewerId}:`, error); } } } return resolved.sort((a, b) => b.priority - a.priority || a.title.localeCompare(b.title)); } async processFile(filePath, context = {}) { const extension = path.extname(filePath).toLowerCase().slice(1); const mimeType = String(context.mimeType || '').toLowerCase(); const entry = Array.from(this.fileFormatHandlers.values()).find(({ handler }) => handler.extensions?.some(ext => ext.replace(/^\./, '').toLowerCase() === extension) || handler.mimeTypes?.some(mime => mime.toLowerCase() === mimeType)); return entry?.handler.process ? entry.handler.process(filePath, context) : undefined; } async getExtraFileList(filePath, context = {}) { const handler = this.getFileFormatHandler(filePath); return handler?.getExtraFileList?.(filePath, context); } async getExtraFile(filePath, fileName, context = {}) { const handler = this.getFileFormatHandler(filePath); return handler?.getExtraFile?.(filePath, fileName, context); } getFileFormatHandler(filePath) { const extension = path.extname(filePath).toLowerCase().slice(1); return Array.from(this.fileFormatHandlers.values()).find(({ handler }) => handler.extensions?.some(ext => ext.replace(/^\./, '').toLowerCase() === extension))?.handler; } async runHttpHooks(context) { for (const hook of this.httpHooks) { if (!this.matchesHttpHook(hook, context)) continue; const result = await hook.handler(context); if (result === false) return false; } return true; } matchesHttpHook(hook, context) { if (hook.method && hook.method !== context.method.toUpperCase()) return false; if (typeof hook.path === 'string') return hook.path === context.path; return hook.path.test(context.path); } getPlugin(name) { return this.loadedPlugins.get(name); } isPluginLoaded(name) { return this.loadedPlugins.has(name); } unloadPlugin(name) { if (this.loadedPlugins.has(name)) { // 尝试调用插件的清理函数(如果存在�? const plugin = this.loadedPlugins.get(name); if (plugin && typeof plugin.cleanup === 'function') { try { plugin.cleanup(); } catch (error) { console.error(`Error cleaning up plugin ${name}:`, error); } } for (const [key, entry] of this.fileFormatHandlers) { if (entry.pluginName === name) this.unregisterFileFormat(name, key.slice(name.length + 1)); } this.loadedPlugins.delete(name); console.log(`Unloaded plugin: ${name}`); return true; } return false; } async reloadPlugin(name) { const config = JSON.parse(fs.readFileSync(this.pluginsConfigPath, 'utf-8')); const pluginConfig = config.find(p => p.name === name); if (!pluginConfig) { console.error(`Plugin config not found for: ${name}`); return false; } if (!pluginConfig.enabled) { console.log(`Plugin ${name} is disabled, skipping reload`); return false; } // 先卸载插�? this.unloadPlugin(name); // 重新加载插件 await this.loadPlugin(pluginConfig, true); return this.isPluginLoaded(name); } /** * 从插件目录的 package.json + icon 文件提取展示 meta */ extractPluginMeta(pluginName, pluginDir) { let packageInfo = {}; try { const packageJsonPath = path.join(pluginDir, 'package.json'); if (fs.existsSync(packageJsonPath)) { packageInfo = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8')); } } catch (error) { console.error(`Error reading package.json for plugin ${pluginName}:`, error); } // icon 文件优先, 其次 mira.icon, 最后 package.json 顶层 icon let icon = null; const iconExtensions = ['.png', '.jpg', '.jpeg', '.svg', '.ico', '.gif', '.webp']; for (const ext of iconExtensions) { if (fs.existsSync(path.join(pluginDir, `icon${ext}`))) { icon = `/api/plugins/${pluginName}/icon${ext}`; break; } } const miraInfo = (packageInfo.mira || {}); // author 可能是字符串或 { name, email } 对象 const authorRaw = packageInfo.author; const author = typeof authorRaw === 'string' ? authorRaw : (authorRaw?.name || authorRaw); return { version: packageInfo.version, description: packageInfo.description, author, main: packageInfo.main, icon: icon || miraInfo.icon || packageInfo.icon || null, title: miraInfo.title || packageInfo.title || pluginName, category: miraInfo.category || packageInfo.category || 'general', tags: miraInfo.tags || packageInfo.tags || [], dependencies: Object.keys(packageInfo.dependencies || {}), }; } getPluginsList() { const config = JSON.parse(fs.readFileSync(this.pluginsConfigPath, 'utf-8')); return config.map(pluginConfig => { const pluginDir = this.getPluginDir(pluginConfig.name); const meta = this.extractPluginMeta(pluginConfig.name, pluginDir); // 持久化字段优先, 缺失时 fallback package.json (meta) return { name: pluginConfig.name, enabled: pluginConfig.enabled, path: pluginConfig.path, registry: pluginConfig.registry, installedAt: pluginConfig.installedAt, version: pluginConfig.version || meta.version || '1.0.0', description: pluginConfig.description ?? meta.description ?? '', author: pluginConfig.author ?? meta.author ?? 'Unknown', main: pluginConfig.main || meta.main || 'index.js', dependencies: meta.dependencies, status: pluginConfig.enabled ? 'active' : 'inactive', configurable: true, icon: pluginConfig.icon !== undefined ? pluginConfig.icon : meta.icon, title: pluginConfig.title || meta.title || pluginConfig.name, category: pluginConfig.category || meta.category || 'general', tags: pluginConfig.tags || meta.tags || [], }; }); } async addPlugin(config) { const currentConfig = JSON.parse(fs.readFileSync(this.pluginsConfigPath, 'utf-8')); // 从 package.json 自动提取展示 meta, 补全调用方未提供的字段并持久化 const pluginDir = path.join(this.pluginsDir, config.path); const meta = this.extractPluginMeta(config.name, pluginDir); const enriched = { ...config, version: config.version || meta.version, description: config.description ?? meta.description, author: config.author ?? meta.author, main: config.main || meta.main, icon: config.icon !== undefined ? config.icon : meta.icon, title: config.title || meta.title, category: config.category || meta.category, tags: config.tags || meta.tags, installedAt: config.installedAt || new Date().toISOString(), }; const existingIndex = currentConfig.findIndex(p => p.name === config.name); if (existingIndex >= 0) { currentConfig[existingIndex] = { ...currentConfig[existingIndex], ...enriched }; } else { currentConfig.push(enriched); } fs.writeFileSync(this.pluginsConfigPath, JSON.stringify(currentConfig, null, 2)); if (config.enabled) { await this.loadPlugin(config, true); // 使用 reload=true 确保新插件被加载 } } /** * 重新从 package.json 同步所有插件的展示 meta 到 plugins.json (供「检查更新」调用) */ syncPluginsMeta() { const config = JSON.parse(fs.readFileSync(this.pluginsConfigPath, 'utf-8')); for (let i = 0; i < config.length; i++) { const pluginDir = this.getPluginDir(config[i].name); const meta = this.extractPluginMeta(config[i].name, pluginDir); config[i] = { ...config[i], version: meta.version || config[i].version, description: meta.description ?? config[i].description, author: meta.author ?? config[i].author, main: meta.main || config[i].main, icon: meta.icon, title: meta.title || config[i].title, category: meta.category || config[i].category, tags: meta.tags || config[i].tags, }; } fs.writeFileSync(this.pluginsConfigPath, JSON.stringify(config, null, 2)); return config.length; } /** * 获取所有已加载插件的路由定�? */ getAllPluginRoutes() { const allRoutes = []; for (const [pluginName, plugin] of this.loadedPlugins) { try { // 检查插件是否有 getRoutes 方法 if (plugin && typeof plugin.getRoutes === 'function') { const routes = plugin.getRoutes(); if (Array.isArray(routes)) { // 为每个路由添加插件名称标识,但不修改路径 const routesWithPluginInfo = routes.map(route => ({ ...route, pluginName, // 添加插件名称,方便追�? })); allRoutes.push(...routesWithPluginInfo); } } } catch (error) { console.error(`Error getting routes from plugin ${pluginName}:`, error); } } return allRoutes; } /** * 获取指定插件的路由定�? */ getPluginRoutes(pluginName) { const plugin = this.loadedPlugins.get(pluginName); if (plugin && typeof plugin.getRoutes === 'function') { try { const routes = plugin.getRoutes(); return Array.isArray(routes) ? routes : []; } catch (error) { console.error(`Error getting routes from plugin ${pluginName}:`, error); return []; } } return []; } /** * 手动注册插件实例(用于测试或特殊用途) */ registerPluginInstance(pluginName, pluginInstance) { this.loadedPlugins.set(pluginName, pluginInstance); console.log(`�?Manually registered plugin: ${pluginName}`); } } exports.ServerPluginManager = ServerPluginManager; //# sourceMappingURL=ServerPluginManager.js.map