aps-agent/server/index.ts

136 lines
6.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 平台服务入口(架构设计第 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 { RuntimeSupervisor } from './agent/process-manager.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<void>;
}
export async function startServer(options: StartOptions = {}): Promise<RunningServer> {
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);
// 引擎进程管理(A3.10):空闲回收与并发上限可按环境变量调;日志走 stderr
const supervisor = new RuntimeSupervisor({
maxConcurrent: Number.parseInt(process.env.APS_MAX_CONCURRENT_ENGINES ?? '4', 10),
idleTimeoutMs: Number.parseInt(process.env.APS_ENGINE_IDLE_MS ?? String(15 * 60_000), 10),
onLog: (message) => console.error(message),
});
const service = new ApplicationService({
...(options.version === undefined ? {} : { version: options.version }),
runtime,
sessionRepository: new SessionRepository(database),
capabilityVisibility: options.capabilityVisibility ?? config.capabilities,
contextRepository: new ContextRepository(database),
supervisor,
});
// 开发期可选:APS_DEV_PROVIDER=1 时注册假 Provider,用于本地联调与演示(默认不注册,能力清单为空)
if (process.env.APS_DEV_PROVIDER === '1') {
const { createFakeProvider } = await import('@aps/provider-sdk/testing');
await service.registerProvider(createFakeProvider());
}
supervisor.startSweeper();
const server: Server = createHttpServer(service, internalToken);
await new Promise<void>((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: async () => {
await new Promise<void>((resolve, reject) => {
server.close((error) => (error ? reject(error) : resolve()));
// 事件流是长连接,关停时必须主动断开,否则 close 会一直等它们
// 客户端断线后按 Last-Event-ID 重连,不会丢事件(协议文档第 5 节)
server.closeAllConnections();
});
// 等引擎子进程回收完成再返回:关停后不该有残留进程(A3.10)
await supervisor.disposeAll();
database.close();
},
};
}
/** 引擎选择:显式注入 > 配置里的 engine.kind;选 pi 但没密钥时明确回退到假引擎 */
async function selectRuntime(
config: ReturnType<typeof loadConfig>,
override?: 'fake' | 'pi',
bridge?: () => { baseUrl: string; internalToken: string } | undefined,
): Promise<AgentRuntime> {
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 }),
});
}