baa-conductor

git clone 

commit
67c5482
parent
00c7dbf
author
im_wower
date
2026-03-22 00:36:55 +0800 CST
Merge remote-tracking branch 'origin/feat/T-017-status-runtime' into integration/third-wave-20260322

# Conflicts:
#	apps/status-api/package.json
7 files changed,  +170, -41
M apps/status-api/package.json
+4, -0
 1@@ -3,6 +3,10 @@
 2   "private": true,
 3   "type": "module",
 4   "main": "dist/index.js",
 5+  "exports": {
 6+    ".": "./dist/index.js",
 7+    "./runtime": "./dist/apps/status-api/src/runtime.js"
 8+  },
 9   "scripts": {
10     "build": "pnpm exec tsc -p tsconfig.json && BAA_DIST_DIR=apps/status-api/dist BAA_DIST_ENTRY=apps/status-api/src/index.js BAA_FIX_RELATIVE_EXTENSIONS=true pnpm -C ../.. run build:runtime-postprocess",
11     "typecheck": "pnpm exec tsc --noEmit -p tsconfig.json"
M apps/status-api/src/contracts.ts
+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 {
M apps/status-api/src/index.ts
+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";
A apps/status-api/src/runtime.ts
+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+}
M apps/status-api/src/service.ts
+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+}
M apps/status-api/tsconfig.json
+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   },
M coordination/tasks/T-017-status-runtime.md
+23, -9
 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。