106 lines
3.6 KiB
TypeScript
106 lines
3.6 KiB
TypeScript
/**
|
||
* 算法插件注册表
|
||
* ------------------------------------------------------------
|
||
* 启动时扫描 `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();
|
||
}
|