2026-09-23 19:29:57 +08:00
|
|
|
|
/**
|
2026-09-24 15:09:37 +08:00
|
|
|
|
* 平台服务入口(架构设计第 2 节)。
|
2026-09-23 19:29:57 +08:00
|
|
|
|
*
|
2026-09-24 15:09:37 +08:00
|
|
|
|
* 组装:应用服务 + HTTP / SSE 入口。控制面(core)与引擎适配(agent)在后续阶段接入。
|
2026-09-23 19:29:57 +08:00
|
|
|
|
*/
|
2026-09-24 15:50:07 +08:00
|
|
|
|
import { randomUUID } from 'node:crypto';
|
2026-09-24 15:09:37 +08:00
|
|
|
|
import type { Server } from 'node:http';
|
|
|
|
|
|
import type { AddressInfo } from 'node:net';
|
|
|
|
|
|
|
2026-09-28 15:38:42 +08:00
|
|
|
|
import type { AgentRuntime } from './agent/runtime.js';
|
2026-09-28 16:01:04 +08:00
|
|
|
|
import { loadConfig } from './config.js';
|
2026-09-28 22:17:25 +08:00
|
|
|
|
import type { CapabilityVisibility } from './core/registry/visibility.js';
|
2026-09-28 16:03:12 +08:00
|
|
|
|
import { openDatabase, type Database } from './core/storage/db.js';
|
|
|
|
|
|
import { SessionRepository } from './core/storage/sessions.js';
|
2026-09-24 15:09:37 +08:00
|
|
|
|
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;
|
2026-09-28 15:38:42 +08:00
|
|
|
|
/** 引擎实现:默认假引擎;A3.3 起可换成 Pi 子进程,测试也可注入脚本化假引擎 */
|
|
|
|
|
|
runtime?: AgentRuntime;
|
2026-09-28 16:03:12 +08:00
|
|
|
|
/** 数据库路径:默认取配置;测试传 ':memory:' 以隔离 */
|
|
|
|
|
|
databasePath?: string;
|
2026-09-28 20:30:40 +08:00
|
|
|
|
/** 引擎覆盖:测试必须传 'fake',避免读到本机的真实模型配置 */
|
|
|
|
|
|
engine?: 'fake' | 'pi';
|
2026-09-28 22:17:25 +08:00
|
|
|
|
/** 能力可见性覆盖(A3.5):不传则用配置里的规则 */
|
|
|
|
|
|
capabilityVisibility?: CapabilityVisibility;
|
2026-09-24 15:09:37 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export interface RunningServer {
|
|
|
|
|
|
url: string;
|
|
|
|
|
|
port: number;
|
|
|
|
|
|
service: ApplicationService;
|
2026-09-24 15:50:07 +08:00
|
|
|
|
/** 引擎工具回调的内部令牌:由平台生成,启动引擎子进程时注入(A3.11) */
|
|
|
|
|
|
internalToken: string;
|
2026-09-24 15:09:37 +08:00
|
|
|
|
close(): Promise<void>;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export async function startServer(options: StartOptions = {}): Promise<RunningServer> {
|
2026-09-28 16:01:04 +08:00
|
|
|
|
const config = loadConfig();
|
|
|
|
|
|
const host = options.host ?? config.server.host;
|
|
|
|
|
|
const port = options.port ?? config.server.port;
|
2026-09-28 22:03:44 +08:00
|
|
|
|
const internalToken = randomUUID();
|
|
|
|
|
|
// 端口要等 listen 之后才知道,所以桥信息用闭包延后取(A3.11 的能力桥需要它)
|
|
|
|
|
|
const bridgeRef: { info?: { baseUrl: string; internalToken: string } } = {};
|
|
|
|
|
|
const runtime = options.runtime ?? (await selectRuntime(config, options.engine, () => bridgeRef.info));
|
2026-09-28 16:03:12 +08:00
|
|
|
|
const database: Database = openDatabase(options.databasePath ?? config.database.path);
|
2026-09-28 15:38:42 +08:00
|
|
|
|
const service = new ApplicationService({
|
|
|
|
|
|
...(options.version === undefined ? {} : { version: options.version }),
|
2026-09-28 20:21:24 +08:00
|
|
|
|
runtime,
|
2026-09-28 16:03:12 +08:00
|
|
|
|
sessionRepository: new SessionRepository(database),
|
2026-09-28 22:17:25 +08:00
|
|
|
|
capabilityVisibility: options.capabilityVisibility ?? config.capabilities,
|
2026-09-28 15:38:42 +08:00
|
|
|
|
});
|
2026-09-24 15:48:45 +08:00
|
|
|
|
// 开发期可选:APS_DEV_PROVIDER=1 时注册假 Provider,用于本地联调与演示(默认不注册,能力清单为空)
|
|
|
|
|
|
if (process.env.APS_DEV_PROVIDER === '1') {
|
|
|
|
|
|
const { createFakeProvider } = await import('@aps/provider-sdk/testing');
|
|
|
|
|
|
await service.registerProvider(createFakeProvider());
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-24 15:50:07 +08:00
|
|
|
|
const server: Server = createHttpServer(service, internalToken);
|
2026-09-24 15:09:37 +08:00
|
|
|
|
|
|
|
|
|
|
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;
|
2026-09-28 22:03:44 +08:00
|
|
|
|
// 监听完成后才拿得到真实端口,这时桥信息才可用
|
|
|
|
|
|
bridgeRef.info = { baseUrl: `http://${host}:${actualPort}`, internalToken };
|
2026-09-24 15:09:37 +08:00
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
url: `http://${host}:${actualPort}`,
|
|
|
|
|
|
port: actualPort,
|
|
|
|
|
|
service,
|
2026-09-24 15:50:07 +08:00
|
|
|
|
internalToken,
|
2026-09-24 15:09:37 +08:00
|
|
|
|
close: () =>
|
|
|
|
|
|
new Promise<void>((resolve, reject) => {
|
|
|
|
|
|
server.close((error) => (error ? reject(error) : resolve()));
|
2026-09-28 15:38:42 +08:00
|
|
|
|
// 事件流是长连接,关停时必须主动断开,否则 close 会一直等它们
|
|
|
|
|
|
// 客户端断线后按 Last-Event-ID 重连,不会丢事件(协议文档第 5 节)
|
|
|
|
|
|
server.closeAllConnections();
|
2026-09-28 16:03:12 +08:00
|
|
|
|
database.close();
|
2026-09-24 15:09:37 +08:00
|
|
|
|
}),
|
|
|
|
|
|
};
|
2026-09-28 20:21:24 +08:00
|
|
|
|
}
|
|
|
|
|
|
/** 引擎选择:显式注入 > 配置里的 engine.kind;选 pi 但没密钥时明确回退到假引擎 */
|
2026-09-28 22:03:44 +08:00
|
|
|
|
async function selectRuntime(
|
|
|
|
|
|
config: ReturnType<typeof loadConfig>,
|
|
|
|
|
|
override?: 'fake' | 'pi',
|
|
|
|
|
|
bridge?: () => { baseUrl: string; internalToken: string } | undefined,
|
|
|
|
|
|
): Promise<AgentRuntime> {
|
2026-09-28 20:30:40 +08:00
|
|
|
|
const kind = override ?? config.engine.kind;
|
|
|
|
|
|
if (kind !== 'pi') {
|
2026-09-28 20:21:24 +08:00
|
|
|
|
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');
|
2026-09-28 22:03:44 +08:00
|
|
|
|
return createPiRuntime({
|
|
|
|
|
|
provider: config.llm.provider,
|
|
|
|
|
|
model: config.llm.model,
|
|
|
|
|
|
apiKey: config.llm.apiKey,
|
|
|
|
|
|
...(bridge === undefined ? {} : { bridge }),
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|