aps-agent/server/index.ts

181 lines
8.5 KiB
TypeScript
Raw Normal View History

/**
* 平台服务入口(架构设计第 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 { IdentityContext } from './core/identity/context.js';
import { CredentialBroker } from './core/credentials/broker.js';
import { PendingActionStore } from './core/approval/store.js';
import { AuditLog } from './core/audit/log.js';
import { ApplicationService } from './app/service.js';
import type { Provider } from '@aps/provider-sdk';
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;
/** 确认卡有效期覆盖(A4.4):测试用,默认配置里的 15 分钟 */
approvalTtlMs?: number;
}
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),
});
// A4 控制面五块:身份、凭据、待确认动作、审计落平台库;策略判定在 Core 内按同一批依赖执行
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,
identity: new IdentityContext(database),
credentials: new CredentialBroker({ db: database, keyFile: config.security.credentialKeyPath }),
approvals: new PendingActionStore(database),
audit: new AuditLog(database),
...(options.approvalTtlMs === undefined ? {} : { approvalTtlMs: options.approvalTtlMs }),
});
// 开发期可选:APS_DEV_PROVIDER=1 时注册假 Provider,用于本地联调与演示(默认不注册,能力清单为空)
if (process.env.APS_DEV_PROVIDER === '1') {
const { createFakeProvider } = await import('@aps/provider-sdk/testing');
await service.registerProvider(createFakeProvider());
}
// 已配置的真实 Provider(A5):按配置动态加载,注册不上不阻塞平台启动
await registerConfiguredProviders(service, config.providers);
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();
},
};
}
/**
* 按配置加载 Provider(A5/A7 的可插拔位)。
*
* 用配置里的模块名做动态 import:平台代码不静态依赖任何具体 Provider(依赖方向检查不允许),
* 换一个 Provider 只改配置。注册失败只记账——APS 没起来时平台照样能启动,只是能力清单里没有它。
*/
async function registerConfiguredProviders(
service: ApplicationService,
providers: Array<{ module: string; baseUrl: string; token?: string; timeoutMs?: number }>,
): Promise<void> {
for (const instance of providers) {
try {
const loaded = (await import(instance.module)) as { createProvider?: (options: Record<string, unknown>) => Provider };
if (typeof loaded.createProvider !== 'function') {
console.error(`Provider ${instance.module} 没导出 createProvider(options),已跳过`);
continue;
}
const provider = loaded.createProvider({
baseUrl: instance.baseUrl,
...(instance.token === undefined ? {} : { token: instance.token }),
...(instance.timeoutMs === undefined ? {} : { timeoutMs: instance.timeoutMs }),
});
const manifest = await service.registerProvider(provider);
console.log(`Provider 已注册:${manifest.id}(${manifest.capabilities.length} 个能力)← ${instance.baseUrl}`);
} catch (error) {
console.error(`Provider ${instance.module} 注册失败(${instance.baseUrl}):${error instanceof Error ? error.message : '未知错误'}`);
}
}
}
/** 引擎选择:显式注入 > 配置里的 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 }),
});
}