feat: initial commit - massive-cloud point cloud processing system
This commit is contained in:
105
server/plugins/registry.ts
Normal file
105
server/plugins/registry.ts
Normal file
@@ -0,0 +1,105 @@
|
||||
/**
|
||||
* 算法插件注册表
|
||||
* ------------------------------------------------------------
|
||||
* 启动时扫描 `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();
|
||||
}
|
||||
Reference in New Issue
Block a user