Files
pikasTech-HWLAB/internal/hwpod/http.ts
T
2026-07-21 08:46:18 +02:00

93 lines
6.4 KiB
TypeScript

/*
* SPEC: PJ2026-010103 HWPOD 服务;
* PJ2026-01010305 71FREQ 预装 draft-2026-06-26-71freq-v03-hwpod-preinstall。
* 实现引用: draft-2026-07-20-hwpod-temporal-split。
* 责任: 独立 HWPOD API HTTP 合同。
*/
import { validateHwpodOperationInput } from "./contracts.ts";
import { buildHwpodTopologyReadModel } from "../cloud/hwpod-topology-read-model.ts";
export function createHwpodHttpApp(options: { runtimeApiUrl: string; runtimeApiAuthorization: string; temporal: any; specRegistry: any }) {
return {
async fetch(request: Request) {
const url = new URL(request.url);
try {
if (url.pathname === "/health/live") return json(200, { ok: true, serviceId: "hwlab-hwpod-api", status: "live", valuesPrinted: false });
if (url.pathname === "/health/ready") return readiness(options);
if (url.pathname === "/v1/hwpod" && request.method === "GET") return json(200, { ok: true, serviceId: "hwlab-hwpod-api", executionAuthority: "temporal", runtimeApiUrl: options.runtimeApiUrl, valuesPrinted: false });
if (url.pathname === "/v1/hwpod/specs" && request.method === "GET") return json(200, await options.specRegistry.list());
if (url.pathname === "/v1/hwpod/specs" && request.method === "POST") {
const body = await request.json();
return json(201, await options.specRegistry.create(specDocument(body)));
}
const specMatch = /^\/v1\/hwpod\/specs\/([^/]+)$/u.exec(url.pathname);
if (specMatch) {
const hwpodId = decodeURIComponent(specMatch[1]);
if (request.method === "GET") return json(200, await options.specRegistry.get(hwpodId));
if (request.method === "PUT") return json(200, await options.specRegistry.update(hwpodId, specDocument(await request.json())));
if (request.method === "DELETE") return json(200, await options.specRegistry.delete(hwpodId));
return json(405, { ok: false, error: { code: "method_not_allowed", message: "GET, PUT, or DELETE required" }, valuesPrinted: false });
}
if (url.pathname === "/v1/hwpod/topology" && request.method === "GET") {
return json(200, await buildHwpodTopologyFromRegistry(options.specRegistry, Object.fromEntries(url.searchParams)));
}
if (url.pathname === "/v1/hwpod-node-ops" && request.method === "GET") return proxyGet(options.runtimeApiUrl, `/v1/hwpod-node-ops${url.search}`, options.runtimeApiAuthorization);
if (url.pathname === "/v1/hwpod/operations" && request.method === "POST") {
const body = await request.json();
const input = validateHwpodOperationInput({ ...(body as any), runtimeApiUrl: options.runtimeApiUrl, authorization: options.runtimeApiAuthorization });
const started = await options.temporal.start(input);
return json(202, { ok: true, status: "accepted", operationId: input.operationId, ...started, authority: "hwpod-temporal", valuesPrinted: false });
}
const match = /^\/v1\/hwpod\/operations\/([^/]+)$/u.exec(url.pathname);
if (match && request.method === "GET") {
const operationId = decodeURIComponent(match[1]);
const workflowId = `hwpod-operation-${operationId}`;
const description = await options.temporal.describe(workflowId);
const status = String((description as any)?.status?.name ?? (description as any)?.status ?? "RUNNING").toLowerCase();
const result = /completed|failed|canceled/u.test(status) ? await options.temporal.result(workflowId).catch((error: any) => ({ ok: false, error: { code: error?.code ?? "hwpod_workflow_result_failed", message: error?.message ?? String(error) } })) : null;
return json(200, { ok: true, operationId, workflowId, workflowRunId: (description as any)?.runId ?? null, status, result, valuesPrinted: false });
}
return json(404, { ok: false, error: { code: "hwpod_route_not_found", message: "HWPOD API route was not found" } });
} catch (error: any) {
const code = error?.code ?? "hwpod_http_error";
const status = code.includes("not_found") ? 404 : code === "hwpod_spec_frozen" || code === "hwpod_spec_exists" || code === "hwpod_spec_authority_conflict" ? 409 : 400;
return json(status, { ok: false, status: "failed", error: { code, message: error?.message ?? String(error), ...(error?.details && typeof error.details === "object" ? error.details : {}) }, valuesPrinted: false });
}
},
close: async () => {
await options.specRegistry.close?.();
await options.temporal.close();
}
};
}
export async function buildHwpodTopologyFromRegistry(specRegistry: any, query: Record<string, string> = {}) {
const listed = await specRegistry.list();
const specs = await Promise.all((listed.specs ?? []).map(async (summary: any) => {
const entity = await specRegistry.get(summary.hwpodId);
return {
...summary,
document: entity.document,
authority: entity.authority,
nodeId: entity.nodeId,
};
}));
return buildHwpodTopologyReadModel(specs, {
mode: "hwpod-native-api-no-node-registry",
nodes: [],
}, query);
}
async function readiness(options: { runtimeApiUrl: string; runtimeApiAuthorization: string; temporal: any; specRegistry: any }) {
try {
const specs = await options.specRegistry.list();
return json(200, { ok: true, serviceId: "hwlab-hwpod-api", temporal: "configured", specRegistry: "ready", specCount: specs.count, runtimeApi: "configured", valuesPrinted: false });
} catch (error: any) {
return json(503, { ok: false, serviceId: "hwlab-hwpod-api", temporal: "configured", specRegistry: "blocked", runtimeApi: "configured", error: { code: error?.code ?? "hwpod_spec_registry_unavailable", message: error?.message ?? String(error) }, valuesPrinted: false });
}
}
async function proxyGet(base: string, route: string, authorization: string) { const response = await fetch(`${base}${route}`, { headers: { authorization }, signal: AbortSignal.timeout(10_000) }); return new Response(await response.text(), { status: response.status, headers: { "content-type": response.headers.get("content-type") ?? "application/json", "cache-control": "no-store" } }); }
function specDocument(body: any) { return body && typeof body === "object" && !Array.isArray(body) && body.document !== undefined ? body.document : body; }
function json(status: number, body: unknown) { return Response.json(body, { status, headers: { "cache-control": "no-store" } }); }