Files
massive-cloud/server/plugins/registry.ts

106 lines
3.6 KiB
TypeScript
Raw 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.
/**
* 算法插件注册表
* ------------------------------------------------------------
* 启动时扫描 `server/plugins/manifests/*.json`,把每个 JSON 解析为
* `PluginManifest`。注册表是内存只读单例;增减算法只需添加 / 删除
* manifest 文件并重启进程(无需改 TS)。
*
* 路径解析:
* - 开发态:本文件位于 `server/plugins/registry.ts`,manifests 直接在同级
* `manifests/` 目录。
* - 生产态:esbuild 把 `server/_core/index.ts` 打到 `dist/index.js`,本文件
* 被打包到 dist/index.js 里。为了让 fs 能找到 manifests,Dockerfile 在
* runner 阶段额外把 `server/plugins/` 拷贝到 `dist/plugins/`,并通过
* 环境变量 `PLUGIN_DIR` 覆盖。
*/
import * as fs from 'fs';
import * as path from 'path';
import { fileURLToPath } from 'node:url';
import type { PluginManifest } from './types.js';
let _cache: Map<string, PluginManifest> | null = null;
/** 返回 manifests / adapters / fixtures 的根目录。 */
export function pluginDir(): string {
if (process.env.PLUGIN_DIR) return process.env.PLUGIN_DIR;
const here = path.dirname(fileURLToPath(import.meta.url));
// 生产态 here === /app/dist;开发态 here === server/plugins
const candidates = [
path.join(here), // server/plugins(开发)
path.join(here, 'plugins'), // dist/plugins(生产 — Dockerfile 拷贝)
path.join(here, '..', 'server', 'plugins'),
];
for (const c of candidates) {
if (fs.existsSync(path.join(c, 'manifests'))) return c;
}
// 兜底——开发态相对仓库根
return path.resolve(here, '..', '..', 'server', 'plugins');
}
export function manifestsDir(): string {
return path.join(pluginDir(), 'manifests');
}
export function adaptersDir(): string {
return path.join(pluginDir(), 'adapters');
}
export function fixturesDir(): string {
return path.join(pluginDir(), 'fixtures');
}
/** 从 `manifests/*.json` 重新加载注册表。 */
export function reloadPlugins(): Map<string, PluginManifest> {
const dir = manifestsDir();
const result = new Map<string, PluginManifest>();
if (!fs.existsSync(dir)) {
console.warn('[PluginRegistry] manifests dir not found:', dir);
_cache = result;
return result;
}
for (const file of fs.readdirSync(dir)) {
if (!file.endsWith('.json')) continue;
const full = path.join(dir, file);
try {
const m = JSON.parse(fs.readFileSync(full, 'utf-8')) as PluginManifest;
if (!m.id || !m.runtime?.kind) {
console.warn('[PluginRegistry] skip invalid manifest', file);
continue;
}
if (result.has(m.id)) {
console.warn('[PluginRegistry] duplicate plugin id:', m.id);
}
result.set(m.id, m);
} catch (e) {
console.error('[PluginRegistry] failed to parse', file, e);
}
}
_cache = result;
const ids: string[] = [];
result.forEach((_v, k) => ids.push(k));
console.log(`[PluginRegistry] loaded ${result.size} plugin(s): ${ids.join(', ')}`);
return result;
}
export function listPlugins(): PluginManifest[] {
if (!_cache) reloadPlugins();
const arr: PluginManifest[] = [];
_cache!.forEach((m) => arr.push(m));
return arr;
}
export function getPlugin(id: string): PluginManifest | undefined {
if (!_cache) reloadPlugins();
return _cache!.get(id);
}
/**
* Force a full reload from disk and return the new list.
* Called by the /api/plugins/reload endpoint so operators can hot-swap
* manifests without restarting the server.
*/
export function forceReload(): PluginManifest[] {
_cache = null;
return listPlugins();
}