/** * 平台服务入口(架构设计第 2 节)。 * * 组装:应用服务 + HTTP / SSE 入口。控制面(core)与引擎适配(agent)在后续阶段接入。 */ import { randomUUID } from 'node:crypto'; import type { Server } from 'node:http'; import type { AddressInfo } from 'node:net'; import type { AgentRuntime } from './agent/runtime.js'; import { loadConfig } from './config.js'; import type { CapabilityVisibility } from './core/registry/visibility.js'; import { openDatabase, type Database } from './core/storage/db.js'; import { SessionRepository } from './core/storage/sessions.js'; import { ContextRepository } from './core/storage/context.js'; import { ApplicationService } from './app/service.js'; import { createHttpServer } from './entry/http.js'; export { ApplicationService } from './app/service.js'; export type { Subject } from './app/subject.js'; export { createHttpServer } from './entry/http.js'; export interface StartOptions { host?: string; port?: number; version?: string; /** 引擎实现:默认假引擎;A3.3 起可换成 Pi 子进程,测试也可注入脚本化假引擎 */ runtime?: AgentRuntime; /** 数据库路径:默认取配置;测试传 ':memory:' 以隔离 */ databasePath?: string; /** 引擎覆盖:测试必须传 'fake',避免读到本机的真实模型配置 */ engine?: 'fake' | 'pi'; /** 能力可见性覆盖(A3.5):不传则用配置里的规则 */ capabilityVisibility?: CapabilityVisibility; } export interface RunningServer { /** 本实例选定的引擎实现(A3.9 一致性对比、A3.10 进程管理要用) */ runtime: AgentRuntime; url: string; port: number; service: ApplicationService; /** 引擎工具回调的内部令牌:由平台生成,启动引擎子进程时注入(A3.11) */ internalToken: string; close(): Promise; } export async function startServer(options: StartOptions = {}): Promise { const config = loadConfig(); const host = options.host ?? config.server.host; const port = options.port ?? config.server.port; const internalToken = randomUUID(); // 端口要等 listen 之后才知道,所以桥信息用闭包延后取(A3.11 的能力桥需要它) const bridgeRef: { info?: { baseUrl: string; internalToken: string } } = {}; const runtime = options.runtime ?? (await selectRuntime(config, options.engine, () => bridgeRef.info)); const database: Database = openDatabase(options.databasePath ?? config.database.path); const service = new ApplicationService({ ...(options.version === undefined ? {} : { version: options.version }), runtime, sessionRepository: new SessionRepository(database), capabilityVisibility: options.capabilityVisibility ?? config.capabilities, contextRepository: new ContextRepository(database), }); // 开发期可选:APS_DEV_PROVIDER=1 时注册假 Provider,用于本地联调与演示(默认不注册,能力清单为空) if (process.env.APS_DEV_PROVIDER === '1') { const { createFakeProvider } = await import('@aps/provider-sdk/testing'); await service.registerProvider(createFakeProvider()); } const server: Server = createHttpServer(service, internalToken); await new Promise((resolve, reject) => { server.once('error', reject); server.listen(port, host, () => resolve()); }); const address = server.address() as AddressInfo | null; const actualPort = address ? address.port : port; // 监听完成后才拿得到真实端口,这时桥信息才可用 bridgeRef.info = { baseUrl: `http://${host}:${actualPort}`, internalToken }; return { url: `http://${host}:${actualPort}`, port: actualPort, service, runtime, internalToken, close: () => new Promise((resolve, reject) => { server.close((error) => (error ? reject(error) : resolve())); // 事件流是长连接,关停时必须主动断开,否则 close 会一直等它们 // 客户端断线后按 Last-Event-ID 重连,不会丢事件(协议文档第 5 节) server.closeAllConnections(); database.close(); }), }; } /** 引擎选择:显式注入 > 配置里的 engine.kind;选 pi 但没密钥时明确回退到假引擎 */ async function selectRuntime( config: ReturnType, override?: 'fake' | 'pi', bridge?: () => { baseUrl: string; internalToken: string } | undefined, ): Promise { const kind = override ?? config.engine.kind; if (kind !== 'pi') { const { createFakeRuntime } = await import('./agent/fake-runtime.js'); const { createRulePlanner, plannerToFakeTurn } = await import('./agent/rule-planner.js'); return createFakeRuntime({ name: 'fake', planner: plannerToFakeTurn(createRulePlanner()) }); } if (config.llm.apiKey.trim() === '') { console.error('警告:配置要求 Pi 引擎但没有模型密钥,已回退到假引擎(ADR-0011 允许未配置时降级)'); const { createFakeRuntime } = await import('./agent/fake-runtime.js'); const { createRulePlanner, plannerToFakeTurn } = await import('./agent/rule-planner.js'); return createFakeRuntime({ name: 'fake', planner: plannerToFakeTurn(createRulePlanner()) }); } const { createPiRuntime } = await import('./agent/pi/runtime.js'); return createPiRuntime({ provider: config.llm.provider, model: config.llm.model, apiKey: config.llm.apiKey, ...(bridge === undefined ? {} : { bridge }), }); }