- commit
- 8035a92
- parent
- c5e007b
- author
- im_wower
- date
- 2026-03-22 00:09:18 +0800 CST
feat: add status api runtime entry
7 files changed,
+172,
-42
+6,
-1
1@@ -2,8 +2,13 @@
2 "name": "@baa-conductor/status-api",
3 "private": true,
4 "type": "module",
5+ "main": "./dist/apps/status-api/src/index.js",
6+ "exports": {
7+ ".": "./dist/apps/status-api/src/index.js",
8+ "./runtime": "./dist/apps/status-api/src/runtime.js"
9+ },
10 "scripts": {
11- "build": "pnpm exec tsc --noEmit -p tsconfig.json",
12+ "build": "pnpm exec tsc -p tsconfig.json",
13 "typecheck": "pnpm exec tsc --noEmit -p tsconfig.json"
14 }
15 }
+1,
-0
1@@ -26,6 +26,7 @@ export interface StatusApiRoute {
2 path: string;
3 summary: string;
4 contentType: "application/json" | "text/html" | "text/plain";
5+ aliases?: string[];
6 }
7
8 export interface StatusApiRequest {
+1,
-0
1@@ -1,4 +1,5 @@
2 export * from "./contracts.js";
3 export * from "./data-source.js";
4 export * from "./render.js";
5+export * from "./runtime.js";
6 export * from "./service.js";
+51,
-0
1@@ -0,0 +1,51 @@
2+import { StaticStatusSnapshotLoader } from "./data-source.js";
3+import type { StatusApiHandler, StatusApiRequest, StatusApiResponse, StatusApiRoute, StatusSnapshotLoader } from "./contracts.js";
4+import { createStatusApiHandler } from "./service.js";
5+
6+export interface StatusApiRuntime extends StatusApiHandler {
7+ fetch(request: Request): Promise<Response>;
8+}
9+
10+export interface StatusApiRuntimeOptions {
11+ snapshotLoader?: StatusSnapshotLoader;
12+}
13+
14+export function createStatusApiRuntime(options: StatusApiRuntimeOptions = {}): StatusApiRuntime {
15+ const handler = createStatusApiHandler(options.snapshotLoader ?? new StaticStatusSnapshotLoader());
16+
17+ return {
18+ routes: handler.routes,
19+ handle: handler.handle,
20+ fetch: async (request) => toFetchResponse(await handler.handle(toStatusApiRequest(request)))
21+ };
22+}
23+
24+export function createStatusApiFetchHandler(snapshotLoader: StatusSnapshotLoader): (request: Request) => Promise<Response> {
25+ const handler = createStatusApiHandler(snapshotLoader);
26+
27+ return async (request) => toFetchResponse(await handler.handle(toStatusApiRequest(request)));
28+}
29+
30+export function toStatusApiRequest(request: Pick<Request, "method" | "url">): StatusApiRequest {
31+ return {
32+ method: request.method,
33+ path: request.url
34+ };
35+}
36+
37+export function toFetchResponse(response: StatusApiResponse): Response {
38+ return new Response(response.body, {
39+ status: response.status,
40+ headers: new Headers(response.headers)
41+ });
42+}
43+
44+export function describeStatusApiRuntimeSurface(runtime: Pick<StatusApiRuntime, "routes">): string[] {
45+ return runtime.routes.map(describeStatusApiRoute);
46+}
47+
48+function describeStatusApiRoute(route: StatusApiRoute): string {
49+ const paths = [route.path, ...(route.aliases ?? [])].join(", ");
50+
51+ return `${route.method} ${paths} (${route.contentType})`;
52+}
+89,
-32
1@@ -22,15 +22,53 @@ const TEXT_HEADERS = {
2 "cache-control": "no-store"
3 } as const;
4
5-export const STATUS_API_ROUTES: StatusApiRoute[] = [
6- { method: "GET", path: "/healthz", summary: "状态服务健康检查", contentType: "text/plain" },
7- { method: "GET", path: "/v1/status", summary: "读取全局自动化状态快照", contentType: "application/json" },
8- { method: "GET", path: "/v1/status/ui", summary: "读取最小 HTML 状态面板", contentType: "text/html" },
9- { method: "GET", path: "/", summary: "最小状态面板首页", contentType: "text/html" }
10+type StatusApiRouteId = "healthz" | "status" | "ui";
11+
12+type StatusApiRouteDefinition = StatusApiRoute & {
13+ id: StatusApiRouteId;
14+};
15+
16+const STATUS_API_ROUTE_DEFINITIONS: ReadonlyArray<StatusApiRouteDefinition> = [
17+ {
18+ id: "healthz",
19+ method: "GET",
20+ path: "/healthz",
21+ summary: "状态服务健康检查",
22+ contentType: "text/plain"
23+ },
24+ {
25+ id: "status",
26+ method: "GET",
27+ path: "/v1/status",
28+ summary: "读取全局自动化状态快照",
29+ contentType: "application/json"
30+ },
31+ {
32+ id: "ui",
33+ method: "GET",
34+ path: "/v1/status/ui",
35+ aliases: ["/", "/ui"],
36+ summary: "读取最小 HTML 状态面板",
37+ contentType: "text/html"
38+ }
39 ];
40
41+const STATUS_API_ROUTE_LOOKUP = createStatusApiRouteLookup();
42+
43+export const STATUS_API_ROUTES: StatusApiRoute[] = STATUS_API_ROUTE_DEFINITIONS.map((route) => ({
44+ method: route.method,
45+ path: route.path,
46+ summary: route.summary,
47+ contentType: route.contentType,
48+ ...(route.aliases == null ? {} : { aliases: [...route.aliases] })
49+}));
50+
51 export function describeStatusApiSurface(): string[] {
52- return STATUS_API_ROUTES.map((route) => `${route.method} ${route.path} - ${route.summary}`);
53+ return STATUS_API_ROUTE_DEFINITIONS.map((route) => {
54+ const paths = [route.path, ...(route.aliases ?? [])].join(", ");
55+
56+ return `${route.method} ${paths} - ${route.summary}`;
57+ });
58 }
59
60 export function createStatusApiHandler(snapshotLoader: StatusSnapshotLoader): StatusApiHandler {
61@@ -59,36 +97,37 @@ export async function handleStatusApiRequest(
62 );
63 }
64
65- if (path === "/healthz") {
66- return {
67- status: 200,
68- headers: { ...TEXT_HEADERS },
69- body: "ok"
70- };
71- }
72-
73- const snapshot = await snapshotLoader.loadSnapshot();
74+ const route = resolveStatusApiRoute(path);
75
76- if (path === "/" || path === "/ui" || path === "/v1/status/ui") {
77- return {
78- status: 200,
79- headers: { ...HTML_HEADERS },
80- body: renderStatusPage(snapshot)
81- };
82- }
83-
84- if (path === "/v1/status") {
85- return jsonResponse(200, {
86- ok: true,
87- data: snapshot
88+ if (route == null) {
89+ return jsonResponse(404, {
90+ ok: false,
91+ error: "not_found",
92+ message: `No status route matches "${path}".`
93 });
94 }
95
96- return jsonResponse(404, {
97- ok: false,
98- error: "not_found",
99- message: `No status route matches "${path}".`
100- });
101+ switch (route.id) {
102+ case "healthz":
103+ return {
104+ status: 200,
105+ headers: { ...TEXT_HEADERS },
106+ body: "ok"
107+ };
108+
109+ case "status":
110+ return jsonResponse(200, {
111+ ok: true,
112+ data: await snapshotLoader.loadSnapshot()
113+ });
114+
115+ case "ui":
116+ return {
117+ status: 200,
118+ headers: { ...HTML_HEADERS },
119+ body: renderStatusPage(await snapshotLoader.loadSnapshot())
120+ };
121+ }
122 }
123
124 function jsonResponse(
125@@ -113,3 +152,21 @@ function normalizePath(value: string): string {
126
127 return normalized === "" ? "/" : normalized;
128 }
129+
130+function createStatusApiRouteLookup(): Map<string, StatusApiRouteDefinition> {
131+ const lookup = new Map<string, StatusApiRouteDefinition>();
132+
133+ for (const route of STATUS_API_ROUTE_DEFINITIONS) {
134+ lookup.set(route.path, route);
135+
136+ for (const alias of route.aliases ?? []) {
137+ lookup.set(alias, route);
138+ }
139+ }
140+
141+ return lookup;
142+}
143+
144+function resolveStatusApiRoute(path: string): StatusApiRouteDefinition | null {
145+ return STATUS_API_ROUTE_LOOKUP.get(path) ?? null;
146+}
+1,
-0
1@@ -1,6 +1,7 @@
2 {
3 "extends": "../../tsconfig.base.json",
4 "compilerOptions": {
5+ "lib": ["ES2022", "DOM"],
6 "rootDir": "../..",
7 "outDir": "dist"
8 },
1@@ -1,15 +1,15 @@
2 ---
3 task_id: T-017
4 title: Status API 运行时入口
5-status: todo
6+status: review
7 branch: feat/T-017-status-runtime
8 repo: /Users/george/code/baa-conductor
9-base_ref: main
10+base_ref: main@c5e007b
11 depends_on:
12 - T-010
13 write_scope:
14 - apps/status-api/**
15-updated_at: 2026-03-21
16+updated_at: 2026-03-22
17 ---
18
19 # T-017 Status API 运行时入口
20@@ -55,25 +55,39 @@ updated_at: 2026-03-21
21
22 ## files_changed
23
24-- 待填写
25+- `apps/status-api/package.json`
26+- `apps/status-api/tsconfig.json`
27+- `apps/status-api/src/contracts.ts`
28+- `apps/status-api/src/index.ts`
29+- `apps/status-api/src/runtime.ts`
30+- `apps/status-api/src/service.ts`
31+- `coordination/tasks/T-017-status-runtime.md`
32
33 ## commands_run
34
35-- 待填写
36+- `npx --yes pnpm install`
37+- `npx --yes pnpm --filter @baa-conductor/status-api typecheck`
38+- `npx --yes pnpm --filter @baa-conductor/status-api build`
39+- `node --input-type=module -e "import { createStatusApiRuntime } from './apps/status-api/dist/apps/status-api/src/index.js'; ..."`
40
41 ## result
42
43-- 待填写
44+- 已将 status-api 从包内 handler 扩展为可挂载的 fetch 运行时入口,新增 `createStatusApiRuntime()` 与 `createStatusApiFetchHandler()`,可直接对接标准 `Request`/`Response`。
45+- 已整理对外路由面,明确以 `GET /healthz`、`GET /v1/status`、`GET /v1/status/ui` 为 canonical surface,并把 `/`、`/ui` 保留为 UI 别名。
46+- 已将 `build` 改为真实 `tsc` 发射,确认生成 `apps/status-api/dist/**` 产物,并用构建后的运行时代码冒烟验证三条 GET 路由。
47
48 ## risks
49
50-- 待填写
51+- 默认运行时仍使用 `StaticStatusSnapshotLoader`;真正接入 D1 或本地控制平面数据库仍需由后续整合步骤注入 `D1StatusSnapshotLoader`。
52+- 当前 dist 入口路径受 `rootDir: ../..` 影响为 `dist/apps/status-api/src/*.js`;如果后续统一构建任务收敛到平铺的 `dist/index.js`,需要同步调整 `package.json` 的 `main`/`exports`。
53+- fetch 运行时假设宿主环境提供标准 Fetch API;若后续必须在更旧的 Node 版本运行,需要额外补 polyfill 或改为 node server adapter。
54
55 ## next_handoff
56
57-- 待填写
58+- 在 status-api 的宿主进程中注入真实 `D1StatusSnapshotLoader`,把 `createStatusApiRuntime()` 挂到本地 HTTP server、launchd 进程或上层 router。
59+- 若仓库后续统一 dist 布局,顺手把 `@baa-conductor/status-api` 的导出路径更新到新的 build 产物位置。
60
61 ## notes
62
63 - `2026-03-21`: 创建第三波任务卡
64-
65+- `2026-03-22`: 从 `main@c5e007b` 建立独立 worktree,补齐 status-api fetch 运行时入口与 canonical route surface,完成验证并进入 review。