/** * Provider 自动注册表 —— 扫描 `providers/` 目录,按文件名 `llm..js` 建表。 * * 「加厂商 = 加一个文件」:放进 `providers/llm..js` 并默认导出一个继承 LLMBase 的类, * 即被发现,无需改本文件或任何现有文件。注册键 = 文件名中 `llm.` 与 `.js` 之间的段。 * * 不在模块顶层执行扫描(边界纪律:无顶层副作用);`loadRegistry()` 被 `createLLM` 调用时才读目录。 * 目录内只有真实厂商适配器:假 provider 已删除,网关不提供任何「不发真实请求」的实现。 */ import { readdirSync } from 'node:fs' import path from 'node:path' import { fileURLToPath, pathToFileURL } from 'node:url' const here = path.dirname(fileURLToPath(import.meta.url)) const PROVIDERS_DIR = path.join(here, 'providers') /** 文件名形如 `llm.openai.js` / `llm.azure_foundry.js` → 捕获 provider key。 */ const FILE_RE = /^llm\.([a-z0-9_-]+)\.js$/ /** * 扫描 providers/ 目录,返回 `{ apiType: () => Promise }` 的惰性加载表。 * 惰性:只在真正 `createLLM({ provider })` 命中某 apiType 时才 dynamic import 该文件, * 避免加载未用到的子类(也避免任一子类的加载错误污染整张表)。 * @param {string} [dir] 覆盖扫描目录(仅供测试自定义 providers 目录)。 * @returns {Record Promise<{ default: new (cfg: any) => any }>>} */ export function loadRegistry(dir = PROVIDERS_DIR) { /** @type {Record Promise<{ default: new (cfg: any) => any }>>} */ const table = {} for (const name of readdirSync(dir)) { const m = FILE_RE.exec(name) if (!m) continue const apiType = m[1] const fileUrl = pathToFileURL(path.join(dir, name)).href table[apiType] = () => import(fileUrl) } return table } /** * 列出已注册的 apiType(用于未知 provider 的报错信息)。 * @param {string} [dir] * @returns {string[]} */ export function registeredProviders(dir = PROVIDERS_DIR) { return Object.keys(loadRegistry(dir)).sort() }