import { getVersionManagerTool } from "@/lib/db/versionManager"; import { markAllUnavailable } from "@/lib/db/serviceModels"; import { registerSupervisor, getSupervisor } from "./registry"; import { ServiceSupervisor } from "./ServiceSupervisor"; import { resolveSpawnArgs as nineRouterSpawnArgs } from "./installers/ninerouter"; import { resolveSpawnArgs as cliproxySpawnArgs, CLIPROXY_DEFAULT_PORT, } from "./installers/cliproxy"; import { getOrCreateApiKey } from "./apiKey"; import { scheduleServiceModelSync, stopServiceModelSync } from "./modelSync"; import type { ServiceStatus } from "./types"; const NINEROUTER_PORT = parseInt(process.env.NINEROUTER_PORT ?? "20130", 10); const CLIPROXY_PORT = parseInt(process.env.CLIPROXYAPI_PORT ?? String(CLIPROXY_DEFAULT_PORT), 10); type ServiceEntry = { tool: string; port: number; healthPath: string; healthIntervalMs: number; stopTimeoutMs: number; logsBufferBytes: number; needsApiKey: boolean; }; const SERVICES: ServiceEntry[] = [ { tool: "9router", port: NINEROUTER_PORT, healthPath: "/api/health", healthIntervalMs: 2_000, stopTimeoutMs: 15_000, logsBufferBytes: 5_242_880, needsApiKey: true, }, { tool: "cliproxy", port: CLIPROXY_PORT, healthPath: "/v1/models", healthIntervalMs: 5_000, stopTimeoutMs: 15_000, logsBufferBytes: 5_242_880, needsApiKey: false, }, ]; function buildSpawnArgsFactory( cfg: ServiceEntry, apiKey: string ): () => ReturnType { if (cfg.tool === "9router") { return () => nineRouterSpawnArgs(apiKey, cfg.port); } return () => cliproxySpawnArgs(cfg.port); } export async function bootstrapEmbeddedServices(): Promise { for (const cfg of SERVICES) { if (getSupervisor(cfg.tool)) continue; const row = await getVersionManagerTool(cfg.tool); if (!row || row.status === "not_installed") continue; const apiKey = cfg.needsApiKey ? await getOrCreateApiKey(cfg.tool).catch(() => "placeholder") : ""; const supervisor = new ServiceSupervisor({ tool: cfg.tool, port: cfg.port, spawnArgs: buildSpawnArgsFactory(cfg, apiKey), healthUrl: () => `http://127.0.0.1:${cfg.port}${cfg.healthPath}`, healthIntervalMs: cfg.healthIntervalMs, stopTimeoutMs: cfg.stopTimeoutMs, logsBufferBytes: cfg.logsBufferBytes, }); registerSupervisor(supervisor); const baseUrl = `http://127.0.0.1:${cfg.port}`; supervisor.on("stateChange", (status: ServiceStatus) => { if (status.state === "running") { scheduleServiceModelSync(cfg.tool, baseUrl, apiKey); } else if (status.state === "stopped" || status.state === "error") { stopServiceModelSync(cfg.tool); markAllUnavailable(cfg.tool); } }); if (row.autoStart) { supervisor.start().catch((err: unknown) => { const msg = err instanceof Error ? err.message : String(err); console.warn(`[Services] Auto-start failed for ${cfg.tool}: ${msg}`); }); } } }