121 lines
9.1 KiB
TypeScript
121 lines
9.1 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 { randomUUID } from "node:crypto";
|
|
import { compileHwpodNodeOpsPlan } from "../../tools/src/hwpod-harness-lib.ts";
|
|
import { HWPOD_NODE_OPS, HWPOD_NODE_OPS_CONTRACT_VERSION } from "../../tools/src/hwpod-node-ops-contract.ts";
|
|
import { validateHwpodOperationInput, validateHwpodWebOperationRequest } from "./contracts.ts";
|
|
import { buildHwpodTopologyReadModel } from "../cloud/hwpod-topology-read-model.ts";
|
|
|
|
export function createHwpodHttpApp(options: { nodeOpsApiUrl: string; temporal: any; specRegistry: any; nodeRegistry: 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", nodeOpsApiUrl: options.nodeOpsApiUrl, 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), options.nodeRegistry.describe()));
|
|
}
|
|
if (url.pathname === "/v1/hwpod-node-ops" && request.method === "GET") {
|
|
const planId = url.searchParams.get("planId")?.trim() ?? "";
|
|
if (planId) {
|
|
const operation = await options.nodeRegistry.lookup(planId);
|
|
return json(operation ? 200 : 404, operation
|
|
? { ok: true, status: operation.result ? "completed" : "running", contractVersion: HWPOD_NODE_OPS_CONTRACT_VERSION, ...operation, valuesPrinted: false }
|
|
: { ok: false, status: "not-found", contractVersion: HWPOD_NODE_OPS_CONTRACT_VERSION, planId, error: { code: "hwpod_operation_not_found", message: `hwpod operation ${planId} was not found` }, valuesPrinted: false });
|
|
}
|
|
return json(200, { ok: true, status: "ready", contractVersion: HWPOD_NODE_OPS_CONTRACT_VERSION, supportedOps: [...HWPOD_NODE_OPS], websocket: options.nodeRegistry.describe(), valuesPrinted: false });
|
|
}
|
|
if (url.pathname === "/v1/hwpod-node-ops" && request.method === "POST") {
|
|
const plan = validateNodeOpsPlan(await request.json());
|
|
const result = await options.nodeRegistry.dispatch(plan, { requestId: `req_hwpod_native_${randomUUID()}`, operationKind: "user-operation", source: "hwpod-native-api" }, { timeoutMs: 900_000 });
|
|
return json(result?.ok === false ? 409 : 200, { ...result, contractVersion: HWPOD_NODE_OPS_CONTRACT_VERSION, planId: plan.planId, hwpodId: plan.hwpodId, nodeId: plan.nodeId, valuesPrinted: false });
|
|
}
|
|
if (url.pathname === "/v1/hwpod/operations" && request.method === "POST") {
|
|
const body = await request.json();
|
|
const input = await operationInput(body, options);
|
|
const started = await options.temporal.start(input);
|
|
return json(202, { ok: true, status: "accepted", operationId: input.operationId, planId: input.plan.planId, hwpodId: input.plan.hwpodId, ...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> = {}, nodeSnapshot: any = { mode: "hwpod-native-api-no-node-registry", nodes: [] }) {
|
|
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, nodeSnapshot, query);
|
|
}
|
|
|
|
async function readiness(options: { nodeOpsApiUrl: string; temporal: any; specRegistry: any; nodeRegistry: any }) {
|
|
try {
|
|
const specs = await options.specRegistry.list();
|
|
const nodes = options.nodeRegistry.describe();
|
|
return json(200, { ok: true, serviceId: "hwlab-hwpod-api", temporal: "configured", specRegistry: "ready", specCount: specs.count, nodeRegistry: "ready", connectedNodeCount: nodes.connectedCount, valuesPrinted: false });
|
|
} catch (error: any) {
|
|
return json(503, { ok: false, serviceId: "hwlab-hwpod-api", temporal: "configured", specRegistry: "blocked", nodeRegistry: "configured", error: { code: error?.code ?? "hwpod_spec_registry_unavailable", message: error?.message ?? String(error) }, valuesPrinted: false });
|
|
}
|
|
}
|
|
async function operationInput(body: any, options: { nodeOpsApiUrl: string; specRegistry: any }) {
|
|
if (body?.plan) return validateHwpodOperationInput({ ...body, runtimeApiUrl: options.nodeOpsApiUrl, authorization: "" });
|
|
const request = validateHwpodWebOperationRequest(body);
|
|
const spec = await options.specRegistry.get(request.hwpodId);
|
|
const operationId = `hwpod_plan_${randomUUID()}`;
|
|
const plan = compileHwpodNodeOpsPlan({ document: spec.document, specPath: `hwpod://${request.hwpodId}`, specAuthority: spec.authority, intent: request.intent, args: request.args, operationId });
|
|
return validateHwpodOperationInput({ operationId, plan, runtimeApiUrl: options.nodeOpsApiUrl });
|
|
}
|
|
function validateNodeOpsPlan(value: any) {
|
|
if (!value || typeof value !== "object" || Array.isArray(value)) throw Object.assign(new Error("hwpod-node-ops plan must be an object"), { code: "invalid_hwpod_node_ops_plan" });
|
|
if (value.contractVersion !== HWPOD_NODE_OPS_CONTRACT_VERSION || !String(value.planId ?? "").trim() || !String(value.nodeId ?? "").trim() || !Array.isArray(value.ops) || value.ops.length === 0) throw Object.assign(new Error("hwpod-node-ops plan identity and ops are required"), { code: "invalid_hwpod_node_ops_plan" });
|
|
for (const op of value.ops) if (!HWPOD_NODE_OPS.has(String(op?.op ?? ""))) throw Object.assign(new Error(`unsupported hwpod-node op: ${String(op?.op ?? "")}`), { code: "unsupported_hwpod_node_op" });
|
|
return value;
|
|
}
|
|
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" } }); }
|