Files
pikasTech-HWLAB/internal/cloud/server.mjs
T
2026-05-23 21:15:05 +00:00

1287 lines
42 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { createServer } from "node:http";
import { randomUUID } from "node:crypto";
import { existsSync, lstatSync, mkdirSync, symlinkSync } from "node:fs";
import { readFile } from "node:fs/promises";
import path from "node:path";
import { fileURLToPath } from "node:url";
import {
buildMetadataFromEnv,
imageTagFromReference,
normalizeIsoTimestamp
} from "../build-metadata.mjs";
import { DEV_ENDPOINT, ENVIRONMENT_DEV, ERROR_CODES, SERVICE_IDS } from "../protocol/index.mjs";
import {
AUDIT_FIELD_NAMES,
CLOUD_API_SERVICE_ID,
createAuditRecord
} from "../audit/index.mjs";
import {
SUPPORTED_RPC_METHODS,
createErrorEnvelope,
handleJsonRpcRequest
} from "./json-rpc.mjs";
import {
createCodeAgentErrorPayload,
describeCodeAgentAvailability,
handleCodeAgentChat
} from "./code-agent-chat.mjs";
import { buildGateDiagnosticsRows } from "./gate-diagnostics.mjs";
import {
applyRuntimeDbReadinessLayers,
buildDbRuntimeReadiness
} from "./db-contract.mjs";
import { buildCloudApiReadiness } from "./health-contract.mjs";
import {
M3_IO_CONTROL_ROUTE,
M3_STATUS_ROUTE,
describeM3IoControl,
describeM3IoControlLive,
describeM3StatusLive,
handleM3IoControl
} from "./m3-io-control.mjs";
import { createConfiguredCloudRuntimeStore } from "../db/runtime-store.mjs";
import { createGatewayDemoRegistry } from "./gateway-demo-registry.mjs";
const DEFAULT_BODY_LIMIT_BYTES = 1024 * 1024;
const LIVE_BUILD_TIMEOUT_MS = 900;
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
const LIVE_BUILD_METADATA_PATHS = Object.freeze({
deployManifest: "deploy/deploy.json",
artifactCatalog: "deploy/artifact-catalog.dev.json",
artifactReport: "reports/dev-gate/dev-artifacts.json"
});
const LIVE_BUILD_SERVICE_DEFAULTS = Object.freeze({
"hwlab-cloud-api": Object.freeze({ urlEnv: null, defaultUrl: "http://127.0.0.1:6667" }),
"hwlab-cloud-web": Object.freeze({ urlEnv: "HWLAB_CLOUD_WEB_SERVICE_URL", defaultUrl: "http://hwlab-cloud-web.hwlab-dev.svc.cluster.local:8080" }),
"hwlab-agent-mgr": Object.freeze({ urlEnv: "HWLAB_AGENT_MGR_URL", defaultUrl: "http://hwlab-agent-mgr.hwlab-dev.svc.cluster.local:7410" }),
"hwlab-agent-worker": Object.freeze({ urlEnv: "HWLAB_AGENT_WORKER_URL", defaultUrl: "http://hwlab-agent-worker.hwlab-dev.svc.cluster.local:7411" }),
"hwlab-gateway": Object.freeze({ urlEnv: "HWLAB_GATEWAY_URL", defaultUrl: "http://hwlab-gateway.hwlab-dev.svc.cluster.local:7001" }),
"hwlab-gateway-simu": Object.freeze({ urlEnv: "HWLAB_GATEWAY_SIMU_URL", defaultUrl: "http://hwlab-gateway-simu.hwlab-dev.svc.cluster.local:7101" }),
"hwlab-box-simu": Object.freeze({ urlEnv: "HWLAB_BOX_SIMU_URL", defaultUrl: "http://hwlab-box-simu.hwlab-dev.svc.cluster.local:7201" }),
"hwlab-patch-panel": Object.freeze({ urlEnv: "HWLAB_PATCH_PANEL_URL", defaultUrl: "http://hwlab-patch-panel.hwlab-dev.svc.cluster.local:7301" }),
"hwlab-router": Object.freeze({ urlEnv: "HWLAB_ROUTER_URL", defaultUrl: "http://hwlab-router.hwlab-dev.svc.cluster.local:7401" }),
"hwlab-tunnel-client": Object.freeze({ urlEnv: "HWLAB_TUNNEL_CLIENT_URL", defaultUrl: "http://hwlab-tunnel-client.hwlab-dev.svc.cluster.local:7402" }),
"hwlab-edge-proxy": Object.freeze({ urlEnv: "HWLAB_EDGE_PROXY_URL", defaultUrl: "http://hwlab-edge-proxy.hwlab-dev.svc.cluster.local:6667", healthPath: "/health" }),
"hwlab-cli": Object.freeze({ urlEnv: "HWLAB_CLI_URL", defaultUrl: "http://hwlab-cli.hwlab-dev.svc.cluster.local:7501" }),
"hwlab-agent-skills": Object.freeze({ urlEnv: "HWLAB_AGENT_SKILLS_URL", defaultUrl: "http://hwlab-agent-skills.hwlab-dev.svc.cluster.local:7430" })
});
const LIVE_BUILD_EXTERNAL_COMPONENTS = Object.freeze([
Object.freeze({
serviceId: "hwlab-frpc",
serviceName: "hwlab-frpc",
kind: "external",
externalImage: true,
defaultImage: "127.0.0.1:5000/hwlab/frpc:v0.68.1",
externalReason: "外部镜像或非 HWLAB 构建产物"
})
]);
export function createCloudApiServer(options = {}) {
const env = options.env ?? process.env;
ensureCodeAgentRuntimeBase(env);
const runtimeStore = options.runtimeStore || createConfiguredCloudRuntimeStore({ ...options, env });
const gatewayRegistry = options.gatewayRegistry || createGatewayDemoRegistry({
staleMs: parsePositiveInteger(env.HWLAB_GATEWAY_DEMO_STALE_MS, 30000),
dispatchTimeoutMs: parsePositiveInteger(env.HWLAB_GATEWAY_DISPATCH_TIMEOUT_MS, 30000)
});
return createServer(async (request, response) => {
try {
await routeRequest(request, response, { ...options, env, runtimeStore, gatewayRegistry });
} catch (error) {
sendJson(response, 500, {
error: {
code: "internal_error",
message: "hwlab-cloud-api request handling failed",
reason: error.message
}
});
}
});
}
export function ensureCodeAgentRuntimeBase(env = process.env) {
const workspace = env.HWLAB_CODE_AGENT_CODEX_WORKSPACE || env.HWLAB_CODE_AGENT_WORKSPACE || "/workspace/hwlab";
const codexHome = env.CODEX_HOME || env.HWLAB_CODE_AGENT_CODEX_HOME || "/codex-home";
const appCodexCommand = "/app/node_modules/.bin/codex";
const localCodexCommand = path.join(process.cwd(), "node_modules", ".bin", "codex");
const codexCommand = existsSync(appCodexCommand)
? appCodexCommand
: existsSync(localCodexCommand)
? localCodexCommand
: appCodexCommand;
env.HWLAB_CODE_AGENT_WORKSPACE ||= workspace;
env.HWLAB_CODE_AGENT_CODEX_WORKSPACE ||= workspace;
env.HWLAB_CODE_AGENT_CODEX_SANDBOX ||= "workspace-write";
env.HWLAB_CODE_AGENT_CODEX_STDIO_ENABLED ||= "1";
env.HWLAB_CODE_AGENT_CODEX_STDIO_SUPERVISOR ||= "repo-owned";
env.CODEX_HOME ||= codexHome;
env.HWLAB_CODE_AGENT_CODEX_COMMAND ||= codexCommand;
env.HWLAB_CODE_AGENT_SKILLS_DIRS ||= "/app/skills:/root/.agents/skills:/home/ubuntu/.agents/skills";
for (const target of [path.dirname(workspace), codexHome]) {
try {
mkdirSync(target, { recursive: true });
} catch {
// Runtime health reports the concrete missing/writable blocker.
}
}
try {
if (!existsSync(workspace)) {
if (workspace === "/workspace/hwlab" && existsSync("/app")) {
symlinkSync("/app", workspace, "dir");
} else {
mkdirSync(workspace, { recursive: true });
}
}
lstatSync(workspace);
} catch {
// Runtime health reports the concrete missing/writable blocker.
}
}
export async function buildHealthPayload(options = {}) {
const serviceId = CLOUD_API_SERVICE_ID;
const env = options.env ?? process.env;
ensureCodeAgentRuntimeBase(env);
const metadata = buildMetadataFromEnv(env, {
serviceId,
fallbackImageRepository: "ghcr.io/pikastech/hwlab-cloud-api"
});
const dbProbe = await buildDbRuntimeReadiness(env, options.dbProbe);
const codeAgent = await describeCodeAgentAvailability(env, options);
const runtime = await runtimeReadiness(options.runtimeStore ?? createConfiguredCloudRuntimeStore({ ...options, env }));
const db = applyRuntimeDbReadinessLayers(dbProbe, runtime);
const readiness = buildCloudApiReadiness({ db, codeAgent, runtime });
return {
serviceId,
environment: ENVIRONMENT_DEV,
status: readiness.status,
ready: readiness.ready,
revision: metadata.revision,
readiness,
blockers: readiness.blockers,
blockerCodes: readiness.blockerCodes,
service: {
id: serviceId,
role: "cloud-api",
healthPath: "/health",
livePath: "/health/live"
},
commit: metadata.commit,
image: metadata.image,
build: metadata.build,
endpoint: env.HWLAB_PUBLIC_ENDPOINT || DEV_ENDPOINT,
observedAt: new Date().toISOString(),
db,
codeAgent,
runtime
};
}
async function routeRequest(request, response, options) {
const url = new URL(request.url || "/", "http://hwlab-cloud-api.local");
if (request.method === "GET" && (url.pathname === "/health" || url.pathname === "/health/live")) {
sendJson(response, 200, await buildHealthPayload(options));
return;
}
if (request.method === "GET" && url.pathname === "/live") {
sendJson(response, 200, {
serviceId: CLOUD_API_SERVICE_ID,
status: "live"
});
return;
}
if (request.method === "POST" && (url.pathname === "/rpc" || url.pathname === "/json-rpc")) {
await handleRpcHttpRequest(request, response, options);
return;
}
if (url.pathname === "/v1" || url.pathname.startsWith("/v1/")) {
await handleRestAdapter(request, response, url, options);
return;
}
sendRestError(request, response, 404, "not_found", "Route is not part of the L1 cloud-api runtime", {
operation: `${request.method || "GET"} ${url.pathname}`,
target: {
type: "route",
id: url.pathname
}
});
}
async function handleRpcHttpRequest(request, response, options) {
const body = await readBody(request, options.bodyLimitBytes);
let envelope;
try {
envelope = body ? JSON.parse(body) : {};
} catch (error) {
sendJson(
response,
400,
createErrorEnvelope({
id: "req_unassigned",
code: ERROR_CODES.parseError,
message: "Invalid JSON body",
data: {
reason: error.message
},
context: {
operation: "json_rpc.parse",
target: {
type: "http_route",
id: request.url || "/rpc"
},
result: "rejected"
}
})
);
return;
}
const rpcResponse = await handleJsonRpcRequest(envelope, {
adapter: "json-rpc",
runtimeStore: options.runtimeStore,
env: options.env,
dbProbe: options.dbProbe,
m3IoRequestJson: options.m3IoRequestJson,
gatewayRegistry: options.gatewayRegistry
});
sendJson(response, statusForRpcResponse(rpcResponse), rpcResponse);
}
async function handleRestAdapter(request, response, url, options) {
if (request.method === "GET" && url.pathname === "/v1") {
const dbProbe = await buildDbRuntimeReadiness(options.env ?? process.env, options.dbProbe);
const codeAgent = await describeCodeAgentAvailability(options.env ?? process.env, options);
const runtime = await runtimeReadiness(options.runtimeStore);
const db = applyRuntimeDbReadinessLayers(dbProbe, runtime);
const readiness = buildCloudApiReadiness({ db, codeAgent, runtime });
sendJson(response, 200, {
serviceId: CLOUD_API_SERVICE_ID,
adapter: "rest",
status: readiness.status,
ready: readiness.ready,
rpcBridge: "POST /v1/rpc/{method}",
codeAgent,
methods: SUPPORTED_RPC_METHODS,
auditFields: AUDIT_FIELD_NAMES,
db,
runtime,
readiness,
blockers: readiness.blockers,
blockerCodes: readiness.blockerCodes,
m3IoControl: describeM3IoControl(options)
});
return;
}
if (request.method === "GET" && url.pathname === M3_IO_CONTROL_ROUTE) {
sendJson(response, 200, await describeM3IoControlLive(options));
return;
}
if (request.method === "GET" && url.pathname === M3_STATUS_ROUTE) {
sendJson(response, 200, await describeM3StatusLive(options));
return;
}
if (request.method === "GET" && url.pathname === "/v1/diagnostics/gate") {
sendJson(response, 200, await buildGateDiagnosticsRows(options));
return;
}
if (request.method === "GET" && url.pathname === "/v1/live-builds") {
sendJson(response, 200, await buildLiveBuildsPayload(options));
return;
}
if (request.method === "GET" && url.pathname === "/v1/gateway/sessions") {
sendJson(response, 200, options.gatewayRegistry.describe());
return;
}
if (request.method === "POST" && url.pathname === "/v1/gateway/poll") {
await handleGatewayPollHttp(request, response, options);
return;
}
if (request.method === "POST" && url.pathname === "/v1/gateway/result") {
await handleGatewayResultHttp(request, response, options);
return;
}
if (request.method === "POST" && url.pathname === M3_IO_CONTROL_ROUTE) {
await handleM3IoControlHttp(request, response, options);
return;
}
if (request.method === "POST" && url.pathname === "/v1/agent/chat") {
await handleCodeAgentChatHttp(request, response, options);
return;
}
if (request.method !== "POST" || !url.pathname.startsWith("/v1/rpc/")) {
sendRestError(request, response, 404, "not_found", "REST route is not implemented in the L1 cloud-api runtime", {
operation: `${request.method || "GET"} ${url.pathname}`,
target: {
type: "route",
id: url.pathname
}
});
return;
}
const method = decodeURIComponent(url.pathname.slice("/v1/rpc/".length));
const body = await readBody(request, options.bodyLimitBytes);
let params = {};
try {
params = body ? JSON.parse(body) : {};
} catch (error) {
sendRestError(request, response, 400, "parse_error", "Invalid JSON body", {
operation: method,
target: {
type: "rpc_method",
id: method
},
result: "rejected",
reason: error.message
});
return;
}
if (!params || typeof params !== "object" || Array.isArray(params)) {
sendRestError(request, response, 400, "invalid_params", "REST RPC bridge body must be a JSON object", {
operation: method,
target: {
type: "rpc_method",
id: method
},
result: "rejected"
});
return;
}
const rpcResponse = await handleJsonRpcRequest(
{
jsonrpc: "2.0",
id: getHeader(request, "x-request-id") || `req_${randomUUID()}`,
method,
params,
meta: {
traceId: getHeader(request, "x-trace-id") || `trc_${randomUUID()}`,
actorId: getHeader(request, "x-actor-id"),
serviceId: getHeader(request, "x-source-service-id") || CLOUD_API_SERVICE_ID,
environment: ENVIRONMENT_DEV
}
},
{
adapter: "rest",
runtimeStore: options.runtimeStore,
env: options.env,
dbProbe: options.dbProbe,
m3IoRequestJson: options.m3IoRequestJson,
gatewayRegistry: options.gatewayRegistry
}
);
sendJson(response, statusForRpcResponse(rpcResponse), rpcResponse);
}
async function buildLiveBuildsPayload(options = {}) {
const env = options.env ?? process.env;
const metadata = await loadLiveBuildMetadata(options);
const inventory = liveBuildServiceInventory(metadata);
const services = await Promise.all(
inventory.map((service) => observeLiveBuildService(service, { ...options, env }))
);
const hwlabServices = services.filter((service) => service.kind === "hwlab");
const latest = [...hwlabServices]
.filter((service) => service.build.createdAt)
.sort((left, right) => Date.parse(right.build.createdAt) - Date.parse(left.build.createdAt))[0] ?? null;
return {
serviceId: CLOUD_API_SERVICE_ID,
environment: ENVIRONMENT_DEV,
status: "ok",
contractVersion: "live-builds-v1",
observedAt: new Date().toISOString(),
source: {
kind: "live-health+artifact-catalog",
route: "/v1/live-builds",
healthPath: "/health/live",
metadataPaths: LIVE_BUILD_METADATA_PATHS,
desiredStateSource: metadata.deployManifest.status === "ok" ? LIVE_BUILD_METADATA_PATHS.deployManifest : "unavailable",
artifactCatalogSource: metadata.artifactCatalog.status === "ok" ? LIVE_BUILD_METADATA_PATHS.artifactCatalog : "unavailable",
artifactReportSource: metadata.artifactReport.status === "ok" ? LIVE_BUILD_METADATA_PATHS.artifactReport : "unavailable",
note: "Each row prefers current live service health build metadata and falls back only to repo-owned deploy/artifact metadata; observedAt is not used as build time."
},
latest,
counts: {
total: services.length,
hwlab: hwlabServices.length,
withBuildTime: hwlabServices.filter((service) => Boolean(service.build.createdAt)).length,
unavailable: hwlabServices.filter((service) => service.build.createdAt === null).length,
external: services.filter((service) => service.kind === "external").length
},
services
};
}
async function observeLiveBuildService(service, options) {
if (service.externalImage) {
return externalLiveBuildRecord(service, {
healthUrl: null,
httpStatus: null,
imageReference: service.defaultImage ?? "unknown",
reason: "外部镜像或非 HWLAB 构建产物"
});
}
const metadataRecord = liveBuildRecordFromMetadata(service);
if (service.serviceId === CLOUD_API_SERVICE_ID) {
const health = await buildHealthPayload(options);
return liveBuildRecordFromHealth(service, {
ok: true,
status: 200,
url: "/health/live",
payload: health
}, metadataRecord);
}
const baseUrl = String(options.env?.[service.urlEnv] || service.defaultUrl || "").replace(/\/+$/u, "");
if (!baseUrl) {
return metadataRecord ?? unavailableLiveBuildRecord(service, {
reasonCode: "service_url_unavailable",
reason: `${service.serviceId} 没有配置 live health URL`
});
}
const healthPath = service.healthPath ?? "/health/live";
return liveBuildRecordFromHealth(
service,
await fetchServiceHealth(`${baseUrl}${healthPath}`, options.fetchImpl ?? fetch),
metadataRecord
);
}
async function fetchServiceHealth(url, fetchImpl) {
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), LIVE_BUILD_TIMEOUT_MS);
try {
const response = await fetchImpl(url, {
headers: { Accept: "application/json" },
signal: controller.signal
});
const text = await response.text();
let payload = null;
try {
payload = text ? JSON.parse(text) : null;
} catch {
return {
ok: false,
status: response.status,
url,
reasonCode: "invalid_json",
reason: `${url} returned non-JSON health payload`
};
}
return {
ok: response.ok,
status: response.status,
url,
payload,
reasonCode: response.ok ? null : "http_error",
reason: response.ok ? null : `${url} returned HTTP ${response.status}`
};
} catch (error) {
const timedOut = error?.name === "AbortError";
return {
ok: false,
status: null,
url,
reasonCode: timedOut ? "timeout" : "request_failed",
reason: timedOut ? `${url} timed out after ${LIVE_BUILD_TIMEOUT_MS}ms` : error.message
};
} finally {
clearTimeout(timeout);
}
}
function liveBuildRecordFromHealth(service, result, metadataRecord = null) {
if (!result.ok) {
const reason = healthUnavailableReason(service, result.reason ?? `${service.serviceId} health unavailable`);
return metadataRecord
? metadataRecordWithHealthGap(metadataRecord, {
healthUrl: result.url,
httpStatus: result.status,
reason
})
: unavailableLiveBuildRecord(service, {
healthUrl: result.url,
httpStatus: result.status,
reasonCode: result.reasonCode ?? "health_unavailable",
reason
});
}
const payload = result.payload ?? {};
const payloadServiceId = payload.serviceId ?? service.serviceId;
if (!String(payloadServiceId).startsWith("hwlab-")) {
return externalLiveBuildRecord(service, {
healthUrl: result.url,
httpStatus: result.status,
imageReference: payload.image?.reference ?? payload.image ?? "unknown",
imageTag: payload.image?.tag ?? null,
imageDigest: payload.image?.digest ?? "unknown",
commitId: payload.commit?.id ?? payload.revision ?? payload.version ?? "unknown",
commitSource: payload.commit?.source ?? "external-health",
revision: payload.revision ?? payload.commit?.id ?? payload.version ?? "unknown",
reason: "外部镜像或非 HWLAB 构建产物"
});
}
const imageReference = payload.image?.reference ?? payload.image ?? "unknown";
const commitId = payload.commit?.id ?? payload.revision ?? "unknown";
const createdAt = normalizeHealthBuildTime(payload);
if (!createdAt && metadataRecord?.build?.createdAt) {
return mergeLiveHealthOntoMetadataRecord(metadataRecord, {
service,
result,
payload,
imageReference,
commitId
});
}
return {
serviceId: service.serviceId,
name: service.serviceName,
kind: "hwlab",
status: createdAt ? "ok" : "build_time_unavailable",
healthUrl: result.url,
httpStatus: result.status,
desiredState: desiredStateSummary(service),
build: {
createdAt,
source: payload.build?.source ?? payload.commit?.source ?? "health-payload",
metadataSource: payload.build?.metadataSource ?? (createdAt ? "health:build.createdAt" : "unavailable"),
unavailableReason: createdAt ? null : "构建时间不可用:health payload 缺少 build.createdAt / image.createdAt / buildCreatedAt"
},
image: {
reference: imageReference,
tag: payload.image?.tag ?? imageTagFromReference(imageReference) ?? "unknown",
digest: payload.image?.digest ?? "unknown"
},
commit: {
id: commitId,
source: payload.commit?.source ?? "health-payload"
},
revision: payload.revision ?? commitId
};
}
function metadataRecordWithHealthGap(metadataRecord, { healthUrl, httpStatus, reason }) {
return {
...metadataRecord,
healthUrl,
httpStatus,
build: {
...metadataRecord.build,
liveHealthMissingReason: `live health 不可用,已使用 repo-owned artifact metadata${reason}`
}
};
}
function mergeLiveHealthOntoMetadataRecord(metadataRecord, { service, result, payload, imageReference, commitId }) {
return {
...metadataRecord,
serviceId: service.serviceId,
name: service.serviceName,
kind: "hwlab",
status: "ok",
healthUrl: result.url,
httpStatus: result.status,
build: {
...metadataRecord.build,
metadataSource: metadataRecord.build.metadataSource,
unavailableReason: null,
liveHealthMissingReason: "health payload 缺少 build.createdAt / image.createdAt / buildCreatedAt,已使用 repo-owned artifact metadata"
},
image: {
reference: imageReference !== "unknown" ? imageReference : metadataRecord.image.reference,
tag: payload.image?.tag ?? imageTagFromReference(imageReference) ?? metadataRecord.image.tag,
digest: payload.image?.digest ?? metadataRecord.image.digest
},
commit: {
id: commitId !== "unknown" ? commitId : metadataRecord.commit.id,
source: payload.commit?.source ?? metadataRecord.commit.source
},
revision: payload.revision ?? (commitId !== "unknown" ? commitId : metadataRecord.revision)
};
}
function liveBuildRecordFromMetadata(service) {
const createdAt = normalizeIsoTimestamp(
service.artifact?.buildCreatedAt ??
service.report?.buildCreatedAt ??
service.deploy?.env?.HWLAB_BUILD_CREATED_AT ??
null
);
if (!createdAt) return null;
const imageReference = service.artifact?.image ?? service.deploy?.image ?? "unknown";
const commitId = service.artifact?.commitId ?? service.report?.sourceCommitId ?? service.deploy?.env?.HWLAB_COMMIT_ID ?? imageTagFromReference(imageReference) ?? "unknown";
const digest = service.artifact?.digest ?? service.report?.digest ?? service.deploy?.env?.HWLAB_IMAGE_DIGEST ?? "unknown";
const source = service.artifact?.buildSource ?? service.report?.buildSource ?? service.deploy?.env?.HWLAB_BUILD_SOURCE ?? "repo-owned-artifact-metadata";
return {
serviceId: service.serviceId,
name: service.serviceName,
kind: "hwlab",
status: "ok",
healthUrl: null,
httpStatus: null,
desiredState: desiredStateSummary(service),
build: {
createdAt,
source,
metadataSource: artifactMetadataSource(service),
unavailableReason: null
},
image: {
reference: imageReference,
tag: service.artifact?.imageTag ?? service.deploy?.env?.HWLAB_IMAGE_TAG ?? imageTagFromReference(imageReference) ?? "unknown",
digest
},
commit: {
id: commitId,
source: service.artifact ? "artifact-catalog" : service.report ? "artifact-report" : "deploy-desired-state"
},
revision: service.deploy?.env?.HWLAB_REVISION ?? commitId
};
}
function desiredStateSummary(service) {
return {
namespace: service.deploy?.namespace ?? service.artifact?.namespace ?? "hwlab-dev",
profile: service.deploy?.profile ?? service.artifact?.profile ?? ENVIRONMENT_DEV,
replicas: Number.isFinite(Number(service.deploy?.replicas)) ? Number(service.deploy.replicas) : null,
healthPath: service.healthPath ?? service.deploy?.healthPath ?? service.artifact?.healthPath ?? "/health/live",
source: service.deploy ? LIVE_BUILD_METADATA_PATHS.deployManifest : "service-defaults"
};
}
function artifactMetadataSource(service) {
if (service.artifact?.buildCreatedAt) return "artifact-catalog:buildCreatedAt";
if (service.report?.buildCreatedAt) return "artifact-report:buildCreatedAt";
return "deploy-env:HWLAB_BUILD_CREATED_AT";
}
function unavailableLiveBuildRecord(service, { healthUrl = null, httpStatus = null, reasonCode, reason }) {
return {
serviceId: service.serviceId,
name: service.serviceName,
kind: "hwlab",
status: "unavailable",
healthUrl,
httpStatus,
desiredState: desiredStateSummary(service),
build: {
createdAt: null,
metadataSource: "unavailable",
unavailableReason: `构建时间不可用:${reason}`
},
image: {
reference: "unknown",
tag: "unknown",
digest: "unknown"
},
commit: {
id: "unknown",
source: "unavailable"
},
revision: "unknown",
error: {
code: reasonCode,
message: reason
}
};
}
function externalLiveBuildRecord(service, {
healthUrl = null,
httpStatus = null,
imageReference = "unknown",
imageTag = null,
imageDigest = "unknown",
commitId = "unknown",
commitSource = "external-image",
revision = commitId,
reason
}) {
return {
serviceId: service.serviceId,
name: service.serviceName,
kind: "external",
status: "external",
healthUrl,
httpStatus,
build: {
createdAt: null,
metadataSource: "external-image",
unavailableReason: reason
},
image: {
reference: imageReference,
tag: imageTag ?? imageTagFromReference(imageReference) ?? "unknown",
digest: imageDigest
},
commit: {
id: commitId,
source: commitSource
},
revision
};
}
function healthUnavailableReason(service, reason) {
if (Number(service.deploy?.replicas) === 0) {
return `${service.serviceId} 当前 hwlab-dev desired replicas=0,无可读取的常驻 /health/live${reason}`;
}
if (service.activation === "manual") {
return `${service.serviceId} 是手动激活服务,当前 live health 不可用:${reason}`;
}
return reason;
}
function normalizeHealthBuildTime(payload) {
return normalizeIsoTimestamp(
payload?.build?.createdAt ??
payload?.image?.createdAt ??
payload?.buildCreatedAt ??
payload?.metadata?.buildCreatedAt ??
null
);
}
async function loadLiveBuildMetadata(options = {}) {
if (options.liveBuildMetadata) return normalizeLiveBuildMetadata(options.liveBuildMetadata);
const root = options.repoRoot ?? process.env.HWLAB_REPO_ROOT ?? repoRoot;
const [deployManifest, artifactCatalog, artifactReport] = await Promise.all([
readJsonMetadata(root, LIVE_BUILD_METADATA_PATHS.deployManifest),
readJsonMetadata(root, LIVE_BUILD_METADATA_PATHS.artifactCatalog),
readJsonMetadata(root, LIVE_BUILD_METADATA_PATHS.artifactReport)
]);
return normalizeLiveBuildMetadata({ deployManifest, artifactCatalog, artifactReport });
}
function normalizeLiveBuildMetadata(metadata) {
return {
deployManifest: normalizeMetadataFile(metadata.deployManifest),
artifactCatalog: normalizeMetadataFile(metadata.artifactCatalog),
artifactReport: normalizeMetadataFile(metadata.artifactReport)
};
}
async function readJsonMetadata(root, relativePath) {
try {
const payload = JSON.parse(await readFile(path.join(root, relativePath), "utf8"));
return { status: "ok", path: relativePath, payload };
} catch (error) {
return {
status: "unavailable",
path: relativePath,
error: error.message,
payload: null
};
}
}
function normalizeMetadataFile(file) {
if (file?.status === "ok" || file?.status === "unavailable") {
return {
status: file.status,
path: file.path ?? null,
error: file.error ?? null,
payload: file.payload ?? null
};
}
return {
status: file ? "ok" : "unavailable",
path: null,
error: null,
payload: file ?? null
};
}
function liveBuildServiceInventory(metadata) {
const deployServices = Array.isArray(metadata.deployManifest.payload?.services)
? metadata.deployManifest.payload.services
: [];
const deployByServiceId = new Map(deployServices.map((service) => [service.serviceId, service]));
const artifactByServiceId = new Map(
(Array.isArray(metadata.artifactCatalog.payload?.services) ? metadata.artifactCatalog.payload.services : [])
.map((service) => [service.serviceId, service])
);
const reportByServiceId = new Map(
(Array.isArray(metadata.artifactReport.payload?.artifactPublish?.services) ? metadata.artifactReport.payload.artifactPublish.services : [])
.map((service) => [service.serviceId, service])
);
const serviceIds = uniqueStrings([
...SERVICE_IDS,
...deployServices.map((service) => service.serviceId),
...artifactByServiceId.keys()
]).filter((serviceId) => String(serviceId).startsWith("hwlab-"));
return [
...serviceIds.map((serviceId) => {
const deploy = deployByServiceId.get(serviceId) ?? null;
const defaults = LIVE_BUILD_SERVICE_DEFAULTS[serviceId] ?? {};
const healthPath = deploy?.healthPath ?? defaults.healthPath ?? "/health/live";
return Object.freeze({
serviceId,
serviceName: deploy?.name ?? serviceId,
urlEnv: defaults.urlEnv ?? serviceUrlEnvName(serviceId),
defaultUrl: defaults.defaultUrl ?? serviceDefaultUrl(serviceId),
healthPath,
activation: deploy?.replicas === 0 ? "zero-replica" : defaults.activation,
deploy,
artifact: artifactByServiceId.get(serviceId) ?? null,
report: reportByServiceId.get(serviceId) ?? null
});
}),
...LIVE_BUILD_EXTERNAL_COMPONENTS
];
}
function uniqueStrings(values) {
return [...new Set(values.map((value) => String(value ?? "").trim()).filter(Boolean))];
}
function serviceUrlEnvName(serviceId) {
return `${serviceId.replace(/^hwlab-/u, "HWLAB_").replace(/-/gu, "_").toUpperCase()}_URL`;
}
function serviceDefaultUrl(serviceId) {
const port = {
"hwlab-cloud-api": 6667,
"hwlab-cloud-web": 8080,
"hwlab-agent-mgr": 7410,
"hwlab-agent-worker": 7411,
"hwlab-gateway": 7001,
"hwlab-gateway-simu": 7101,
"hwlab-box-simu": 7201,
"hwlab-patch-panel": 7301,
"hwlab-router": 7401,
"hwlab-tunnel-client": 7402,
"hwlab-edge-proxy": 6667,
"hwlab-cli": 7501,
"hwlab-agent-skills": 7430
}[serviceId] ?? 8080;
return `http://${serviceId}.hwlab-dev.svc.cluster.local:${port}`;
}
async function handleGatewayPollHttp(request, response, options) {
const body = await readBody(request, options.bodyLimitBytes);
let params = {};
try {
params = body ? JSON.parse(body) : {};
} catch (error) {
sendJson(response, 400, {
accepted: false,
error: {
code: "parse_error",
message: "Invalid JSON body",
reason: error.message
}
});
return;
}
if (!params || typeof params !== "object" || Array.isArray(params)) {
sendJson(response, 400, {
accepted: false,
error: {
code: "invalid_params",
message: "gateway poll body must be a JSON object"
}
});
return;
}
const session = options.gatewayRegistry.updateSession(params);
await persistGatewayDemoSession({
runtimeStore: options.runtimeStore,
session,
request,
env: options.env
});
const outbound = options.gatewayRegistry.nextRequest(session.gatewaySessionId);
sendJson(response, 200, {
accepted: true,
serviceId: CLOUD_API_SERVICE_ID,
mode: "gateway-outbound-poll-demo",
gatewayId: session.gatewayId,
gatewaySessionId: session.gatewaySessionId,
queueDepth: session.queue.length,
request: outbound,
type: outbound ? "request" : "noop",
observedAt: new Date().toISOString()
});
}
async function handleGatewayResultHttp(request, response, options) {
const body = await readBody(request, options.bodyLimitBytes);
let params = {};
try {
params = body ? JSON.parse(body) : {};
} catch (error) {
sendJson(response, 400, {
accepted: false,
error: {
code: "parse_error",
message: "Invalid JSON body",
reason: error.message
}
});
return;
}
const result = options.gatewayRegistry.complete(params);
sendJson(response, result.accepted ? 200 : 404, {
...result,
serviceId: CLOUD_API_SERVICE_ID,
mode: "gateway-outbound-poll-demo",
observedAt: new Date().toISOString()
});
}
async function persistGatewayDemoSession({ runtimeStore, session, request, env = process.env }) {
if (!runtimeStore) return;
const refreshMs = parsePositiveInteger(env.HWLAB_GATEWAY_REGISTRATION_REFRESH_MS, 60000);
if (session.runtimeRegisteredEpochMs && Date.now() - session.runtimeRegisteredEpochMs < refreshMs) {
return;
}
const meta = {
traceId: getHeader(request, "x-trace-id") || `trc_gateway_poll_${randomUUID()}`,
actorId: `svc_${session.serviceId}`,
serviceId: CLOUD_API_SERVICE_ID,
environment: ENVIRONMENT_DEV
};
try {
await runtimeStore.registerGatewaySession?.({
projectId: session.projectId,
gatewaySessionId: session.gatewaySessionId,
serviceId: "hwlab-gateway",
gatewayId: session.gatewayId,
endpoint: session.endpoint,
status: "connected",
labels: {
demoOutboundPoll: "true",
os: String(session.system?.platform ?? "unknown")
}
}, meta);
await runtimeStore.registerBoxResource?.({
projectId: session.projectId,
gatewaySessionId: session.gatewaySessionId,
resourceId: session.resourceId,
boxId: session.boxId,
resourceType: "simulator_endpoint",
state: "available",
metadata: {
serviceId: "hwlab-gateway",
demoShellHost: true
}
}, meta);
if (session.capabilities.length > 0) {
await runtimeStore.reportBoxCapabilities?.({
capabilities: session.capabilities.map((capability) => ({
...capability,
resourceId: capability.resourceId ?? session.resourceId,
projectId: capability.projectId ?? session.projectId
}))
}, meta);
}
session.runtimeRegisteredAt = new Date().toISOString();
session.runtimeRegisteredEpochMs = Date.now();
} catch {
// Demo polling must keep the gateway online even when durable runtime writes are not ready.
}
}
async function handleCodeAgentChatHttp(request, response, options) {
const body = await readBody(request, options.bodyLimitBytes);
let params = {};
try {
params = body ? JSON.parse(body) : {};
} catch (error) {
sendJson(response, 400, {
...createCodeAgentErrorPayload({
code: "parse_error",
message: "Invalid JSON body",
reason: error.message,
traceId: getHeader(request, "x-trace-id") || "trc_unassigned",
model: options.env?.HWLAB_CODE_AGENT_MODEL || options.env?.OPENAI_MODEL || process.env.HWLAB_CODE_AGENT_MODEL || process.env.OPENAI_MODEL || "unknown",
layer: "api",
retryable: true
})
});
return;
}
if (!params || typeof params !== "object" || Array.isArray(params)) {
sendJson(response, 400, {
...createCodeAgentErrorPayload({
code: "invalid_params",
message: "Code Agent chat body must be a JSON object",
traceId: getHeader(request, "x-trace-id") || "trc_unassigned",
model: options.env?.HWLAB_CODE_AGENT_MODEL || options.env?.OPENAI_MODEL || process.env.HWLAB_CODE_AGENT_MODEL || process.env.OPENAI_MODEL || "unknown",
layer: "api",
retryable: true
})
});
return;
}
const payload = await handleCodeAgentChat(
{
...params,
traceId: getHeader(request, "x-trace-id") || params.traceId
},
{
callProvider: options.callCodeAgentProvider,
timeoutMs: options.codeAgentTimeoutMs,
env: options.env,
workspace: options.workspace,
skillsDirs: options.skillsDirs,
skillsDirsExact: options.skillsDirsExact,
skillsDiscoveryDelayMs: options.skillsDiscoveryDelayMs,
sessionRegistry: options.sessionRegistry,
m3IoSkillRequestJson: options.m3IoSkillRequestJson,
codexStdioManager: options.codexStdioManager
}
);
sendJson(response, payload.status === "failed" && payload.error?.code === "invalid_params" ? 400 : 200, payload);
}
async function handleM3IoControlHttp(request, response, options) {
const body = await readBody(request, options.bodyLimitBytes);
let params = {};
try {
params = body ? JSON.parse(body) : {};
} catch (error) {
sendJson(response, 400, {
serviceId: CLOUD_API_SERVICE_ID,
contractVersion: "m3-io-control-v1",
route: M3_IO_CONTROL_ROUTE,
method: "POST",
status: "blocked",
accepted: false,
traceId: getHeader(request, "x-trace-id") || "trc_unassigned",
operationId: null,
auditId: null,
evidenceId: null,
audit: {
auditId: null,
status: "not_written"
},
evidence: {
evidenceId: null,
status: "blocked",
sourceKind: "BLOCKED"
},
readback: null,
error: {
code: "parse_error",
message: "Invalid JSON body",
reason: error.message
}
});
return;
}
if (!params || typeof params !== "object" || Array.isArray(params)) {
sendJson(response, 400, {
serviceId: CLOUD_API_SERVICE_ID,
contractVersion: "m3-io-control-v1",
route: M3_IO_CONTROL_ROUTE,
method: "POST",
status: "blocked",
accepted: false,
traceId: getHeader(request, "x-trace-id") || "trc_unassigned",
operationId: null,
auditId: null,
evidenceId: null,
audit: {
auditId: null,
status: "not_written"
},
evidence: {
evidenceId: null,
status: "blocked",
sourceKind: "BLOCKED"
},
readback: null,
error: {
code: "invalid_params",
message: "M3 IO control body must be a JSON object"
}
});
return;
}
try {
const payload = await handleM3IoControl(
{
...params,
traceId: getHeader(request, "x-trace-id") || params.traceId,
requestId: getHeader(request, "x-request-id") || params.requestId,
actorId: getHeader(request, "x-actor-id") || params.actorId
},
{
runtimeStore: options.runtimeStore,
env: options.env,
requestJson: options.m3IoRequestJson,
traceId: getHeader(request, "x-trace-id"),
requestId: getHeader(request, "x-request-id"),
actorId: getHeader(request, "x-actor-id")
}
);
sendJson(response, payload.httpStatus ?? 200, payload);
} catch (error) {
const protocolCode = Number.isInteger(error?.code) ? error.code : ERROR_CODES.internalError;
sendJson(response, protocolCode === ERROR_CODES.invalidParams ? 400 : 200, {
serviceId: CLOUD_API_SERVICE_ID,
contractVersion: "m3-io-control-v1",
route: M3_IO_CONTROL_ROUTE,
method: "POST",
status: "blocked",
accepted: false,
traceId: getHeader(request, "x-trace-id") || params.traceId || "trc_unassigned",
operationId: null,
auditId: null,
evidenceId: null,
audit: {
auditId: null,
status: "not_written"
},
evidence: {
evidenceId: null,
status: "blocked",
sourceKind: "BLOCKED"
},
readback: null,
error: {
code: protocolCode,
message: error.message,
data: error.data ?? {}
}
});
}
}
function sendRestError(request, response, statusCode, code, message, context = {}) {
const requestId = getHeader(request, "x-request-id") || "req_unassigned";
sendJson(response, statusCode, {
error: {
code,
message,
reason: context.reason,
audit: createAuditRecord({
requestId,
actor: {
type: "user",
id: getHeader(request, "x-actor-id") || `system_${CLOUD_API_SERVICE_ID}`
},
source: {
serviceId: CLOUD_API_SERVICE_ID,
environment: ENVIRONMENT_DEV,
traceId: getHeader(request, "x-trace-id"),
adapter: "rest"
},
operation: context.operation || `${request.method || "GET"} ${request.url || "/"}`,
target: context.target || {
type: "route",
id: request.url || "/"
},
result: context.result || "failed"
})
}
});
}
function statusForRpcResponse(rpcResponse) {
if (!rpcResponse.error) {
return 200;
}
if (rpcResponse.error.code === ERROR_CODES.invalidRequest || rpcResponse.error.code === ERROR_CODES.parseError) {
return 400;
}
if (rpcResponse.error.code === ERROR_CODES.methodNotFound) {
return 404;
}
return 200;
}
function readBody(request, limitBytes = DEFAULT_BODY_LIMIT_BYTES) {
const limit = Number.isInteger(limitBytes) && limitBytes > 0 ? limitBytes : DEFAULT_BODY_LIMIT_BYTES;
return new Promise((resolve, reject) => {
let body = "";
request.setEncoding("utf8");
request.on("data", (chunk) => {
body += chunk;
if (Buffer.byteLength(body, "utf8") > limit) {
request.destroy(new Error(`request body exceeds ${limit} bytes`));
}
});
request.on("end", () => resolve(body));
request.on("error", reject);
});
}
function sendJson(response, statusCode, body) {
response.writeHead(statusCode, {
"content-type": "application/json; charset=utf-8",
"cache-control": "no-store"
});
response.end(`${JSON.stringify(body)}\n`);
}
function getHeader(request, name) {
const value = request.headers[name.toLowerCase()];
if (Array.isArray(value)) {
return value[0];
}
return value;
}
function parsePositiveInteger(value, fallback) {
const parsed = Number.parseInt(value ?? "", 10);
return Number.isInteger(parsed) && parsed > 0 ? parsed : fallback;
}
async function runtimeReadiness(runtimeStore) {
if (typeof runtimeStore?.readiness === "function") {
return runtimeStore.readiness();
}
return runtimeStore.summary();
}