Files
pikasTech-HWLAB/internal/cloud/m3-io-control.mjs
T
2026-05-23 04:47:07 +00:00

1278 lines
38 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 { createHash, randomUUID } from "node:crypto";
import {
ENVIRONMENT_DEV,
ERROR_CODES,
HwlabProtocolError,
assertProtocolRecord
} from "../protocol/index.mjs";
import {
CLOUD_API_SERVICE_ID,
createProtocolAuditEvent
} from "../audit/index.mjs";
export const M3_IO_CONTROL_CONTRACT_VERSION = "m3-io-control-v1";
export const M3_IO_CONTROL_ROUTE = "/v1/m3/io";
export const M3_IO_CHAIN = Object.freeze({
projectId: "prj_m3_hardware_loop",
sourceGatewayId: "gwsimu_1",
sourceGatewaySessionId: "gws_gwsimu_1",
sourceResourceId: "res_boxsimu_1",
sourceBoxId: "boxsimu_1",
sourcePort: "DO1",
targetGatewayId: "gwsimu_2",
targetGatewaySessionId: "gws_gwsimu_2",
targetResourceId: "res_boxsimu_2",
targetBoxId: "boxsimu_2",
targetPort: "DI1",
patchPanelServiceId: "hwlab-patch-panel"
});
export const M3_IO_BLOCKER_CODES = Object.freeze({
gatewayUnavailable: "m3_gateway_session_unavailable",
gatewayIdentityMismatch: "m3_gateway_identity_mismatch",
boxUnavailable: "m3_box_resource_unavailable",
portDirectionInvalid: "m3_port_direction_invalid",
wiringMissing: "m3_wiring_missing",
patchPanelUnavailable: "m3_patch_panel_unavailable",
dispatchFailed: "m3_gateway_dispatch_failed",
runtimeDurableBlocked: "runtime_durable_not_green"
});
const DEFAULT_TIMEOUT_MS = 2200;
const DEV_SERVICE_ENDPOINTS = Object.freeze({
gateway1: "http://hwlab-gateway-simu-1.hwlab-dev.svc.cluster.local:7101",
gateway2: "http://hwlab-gateway-simu-2.hwlab-dev.svc.cluster.local:7101",
patchPanel: "http://hwlab-patch-panel.hwlab-dev.svc.cluster.local:7301"
});
const ACTIONS = new Set(["do.write", "di.read"]);
export function describeM3IoControl(options = {}) {
const env = options.env ?? process.env;
const config = buildM3IoConfig(env);
return {
serviceId: CLOUD_API_SERVICE_ID,
contractVersion: M3_IO_CONTROL_CONTRACT_VERSION,
route: M3_IO_CONTROL_ROUTE,
status: config.enabled ? "available" : "blocked",
sourceKind: "SOURCE",
chain: M3_IO_CHAIN,
actions: [
{
action: "do.write",
label: "box-simu-1 DO1 write",
gatewayId: M3_IO_CHAIN.sourceGatewayId,
resourceId: M3_IO_CHAIN.sourceResourceId,
boxId: M3_IO_CHAIN.sourceBoxId,
port: M3_IO_CHAIN.sourcePort,
valueType: "boolean"
},
{
action: "di.read",
label: "box-simu-2 DI1 read",
gatewayId: M3_IO_CHAIN.targetGatewayId,
resourceId: M3_IO_CHAIN.targetResourceId,
boxId: M3_IO_CHAIN.targetBoxId,
port: M3_IO_CHAIN.targetPort,
valueType: "boolean"
}
],
boundaries: {
frontendCallsOnly: M3_IO_CONTROL_ROUTE,
cloudApiDispatchesToGateway: true,
patchPanelOwnsPropagation: true,
directFrontendGatewayOrBoxAccess: false,
genericHardwareRpcExposedToFrontend: false
},
endpointsConfigured: {
gateway1: Boolean(config.gateway1Url),
gateway2: Boolean(config.gateway2Url),
patchPanel: Boolean(config.patchPanelUrl)
},
blockedReason: config.enabled ? null : "M3 IO 控制未启用;前端保持 blocked,不直连 gateway/box-simu。"
};
}
export async function handleM3IoControl(params = {}, context = {}) {
const env = context.env ?? process.env;
const now = context.now?.() ?? new Date().toISOString();
const config = buildM3IoConfig(env);
const action = normalizeAction(params.action ?? params.operation);
const meta = buildRequestMeta(params, context);
const runtimeBefore = await safeRuntimeSummary(context.runtimeStore);
if (!config.enabled) {
return blockedResult({
action,
meta,
runtime: runtimeBefore,
code: "m3_control_disabled",
reason: "M3 IO 控制未启用;前端不能绕过 cloud-api 直连 gateway/box-simu。",
httpStatus: 503,
now
});
}
if (!ACTIONS.has(action)) {
throw new HwlabProtocolError("M3 IO action must be do.write or di.read", {
code: ERROR_CODES.invalidParams,
data: {
action,
allowedActions: [...ACTIONS]
}
});
}
const command = normalizeCommand(action, params);
const directionIssue = validateM3Command(command);
if (directionIssue) {
return blockedResult({
action,
meta,
runtime: runtimeBefore,
code: M3_IO_BLOCKER_CODES.portDirectionInvalid,
reason: directionIssue,
httpStatus: 400,
now
});
}
const gatewayUrl = action === "do.write" ? config.gateway1Url : config.gateway2Url;
const gatewayRole = action === "do.write" ? "source" : "target";
const gateway = await readGatewaySession(gatewayUrl, {
gatewayRole,
expectedGatewayId: command.gatewayId,
expectedGatewaySessionId: command.gatewaySessionId,
requestJson: context.requestJson,
timeoutMs: config.timeoutMs
});
if (!gateway.available) {
return blockedResult({
action,
command,
meta,
runtime: runtimeBefore,
code: gateway.code ?? M3_IO_BLOCKER_CODES.gatewayUnavailable,
reason: gateway.reason,
httpStatus: 200,
now,
details: {
gateway
}
});
}
const box = findGatewayBox(gateway.status, command);
if (!box) {
return blockedResult({
action,
command,
meta,
runtime: runtimeBefore,
code: M3_IO_BLOCKER_CODES.boxUnavailable,
reason: `${gatewayRole} gateway 已响应,但目标 box resource 未注册/不可用:${command.resourceId}`,
httpStatus: 200,
now,
details: {
gateway: publicGatewayStatus(gateway.status)
}
});
}
const capability = capabilityFor(command, box);
const operation = createOperationRecord({
action,
command,
capability,
gatewaySessionId: gateway.gatewaySessionId,
meta,
now
});
const registration = await persistRuntimeRegistrations({
runtimeStore: context.runtimeStore,
gatewayStatus: gateway.status,
box,
capability,
command,
meta
});
const operationWrite = await tryRuntimeWrite(
"hardware.operation.request",
() => context.runtimeStore?.requestHardwareOperation?.(operation, meta)
);
if (action === "di.read") {
return finalizeControlResult(await readDi({
config,
command,
capability,
gateway,
operation,
meta,
runtimeStore: context.runtimeStore,
runtimeBefore,
registration,
operationWrite,
requestJson: context.requestJson,
now
}));
}
return finalizeControlResult(await writeDo({
config,
command,
capability,
gateway,
operation,
meta,
runtimeStore: context.runtimeStore,
runtimeBefore,
registration,
operationWrite,
requestJson: context.requestJson,
now
}));
}
export function buildM3IoConfig(env = process.env) {
const enabledValue = String(env.HWLAB_M3_IO_CONTROL_ENABLED ?? env.HWLAB_M3_CONTROL_ENABLED ?? "true").toLowerCase();
return {
enabled: !["0", "false", "no", "disabled"].includes(enabledValue),
gateway1Url: trimUrl(env.HWLAB_M3_GATEWAY_SIMU_1_URL) ?? DEV_SERVICE_ENDPOINTS.gateway1,
gateway2Url: trimUrl(env.HWLAB_M3_GATEWAY_SIMU_2_URL) ?? DEV_SERVICE_ENDPOINTS.gateway2,
patchPanelUrl: trimUrl(env.HWLAB_M3_PATCH_PANEL_URL) ?? DEV_SERVICE_ENDPOINTS.patchPanel,
timeoutMs: parseTimeout(env.HWLAB_M3_CONTROL_TIMEOUT_MS)
};
}
async function writeDo({
config,
command,
capability,
gateway,
operation,
meta,
runtimeStore,
runtimeBefore,
registration,
operationWrite,
requestJson,
now
}) {
const dispatch = await invokeGateway(gateway.url, {
operation: "hardware.port.write",
projectId: M3_IO_CHAIN.projectId,
gatewaySessionId: gateway.gatewaySessionId,
resourceId: command.resourceId,
boxId: command.boxId,
capabilityId: capability.capabilityId,
port: command.port,
value: command.value,
operationId: operation.operationId,
traceId: meta.traceId,
requestId: meta.requestId,
actorType: "service",
actorId: `svc_${CLOUD_API_SERVICE_ID}`,
requestedBy: meta.actorId,
source: "cloud-api-m3-io-control"
}, { requestJson, timeoutMs: config.timeoutMs });
if (!dispatch.ok || dispatch.body?.accepted !== true) {
return blockedControlResult({
action: command.action,
command,
operation,
meta,
runtimeStore,
runtimeBefore,
registration,
operationWrite,
code: M3_IO_BLOCKER_CODES.dispatchFailed,
reason: `gateway-simu DO 写入失败:${dispatch.error ?? dispatch.body?.error?.message ?? "unknown"}`,
gateway,
dispatch,
now
});
}
const patchPanel = await tickPatchPanel(config.patchPanelUrl, {
signals: [
{
fromResourceId: M3_IO_CHAIN.sourceResourceId,
fromPort: M3_IO_CHAIN.sourcePort,
value: command.value,
operationId: operation.operationId,
traceId: meta.traceId
}
]
}, { requestJson, timeoutMs: config.timeoutMs });
const patchIssue = classifyPatchPanelResult(patchPanel);
if (patchIssue) {
return blockedControlResult({
action: command.action,
command,
operation,
meta,
runtimeStore,
runtimeBefore,
registration,
operationWrite,
code: patchIssue.code,
reason: patchIssue.reason,
gateway,
dispatch,
patchPanel,
now
});
}
const targetReadback = await readTargetDiAfterWrite({
config,
operation,
meta,
requestJson
});
if (targetReadback.status !== "succeeded" || targetReadback.value !== command.value) {
return blockedControlResult({
action: command.action,
command,
operation,
meta,
runtimeStore,
runtimeBefore,
registration,
operationWrite,
code: targetReadback.code ?? M3_IO_BLOCKER_CODES.wiringMissing,
reason: targetReadback.reason ?? `DI1 回读值 ${String(targetReadback.value)} 与 DO1 写入值 ${String(command.value)} 不一致;不能证明 M3 DO->DI 闭环。`,
gateway,
dispatch,
patchPanel,
targetReadback,
now
});
}
const tracePayload = {
action: command.action,
command,
gateway: gatewayResultSummary(gateway),
dispatch: dispatch.body,
patchPanel: patchPanel.body,
targetReadback
};
return successControlResult({
action: command.action,
command,
operation,
meta,
runtimeStore,
runtimeBefore,
registration,
operationWrite,
gateway,
dispatch,
patchPanel,
targetReadback,
tracePayload,
now
});
}
async function readDi({
config,
command,
capability,
gateway,
operation,
meta,
runtimeStore,
runtimeBefore,
registration,
operationWrite,
requestJson,
now
}) {
const dispatch = await invokeGateway(gateway.url, {
operation: "hardware.port.read",
projectId: M3_IO_CHAIN.projectId,
gatewaySessionId: gateway.gatewaySessionId,
resourceId: command.resourceId,
boxId: command.boxId,
capabilityId: capability.capabilityId,
port: command.port,
operationId: operation.operationId,
traceId: meta.traceId,
requestId: meta.requestId,
actorType: "service",
actorId: `svc_${CLOUD_API_SERVICE_ID}`,
requestedBy: meta.actorId,
source: "cloud-api-m3-io-control"
}, { requestJson, timeoutMs: config.timeoutMs });
if (!dispatch.ok || dispatch.body?.accepted !== true) {
return blockedControlResult({
action: command.action,
command,
operation,
meta,
runtimeStore,
runtimeBefore,
registration,
operationWrite,
code: M3_IO_BLOCKER_CODES.dispatchFailed,
reason: `gateway-simu DI 读取失败:${dispatch.error ?? dispatch.body?.error?.message ?? "unknown"}`,
gateway,
dispatch,
now
});
}
const tracePayload = {
action: command.action,
command,
gateway: gatewayResultSummary(gateway),
dispatch: dispatch.body,
value: readValueFromGatewayResult(dispatch.body)
};
return successControlResult({
action: command.action,
command,
operation,
meta,
runtimeStore,
runtimeBefore,
registration,
operationWrite,
gateway,
dispatch,
tracePayload,
now
});
}
async function readTargetDiAfterWrite({ config, operation, meta, requestJson }) {
const gateway = await readGatewaySession(config.gateway2Url, {
gatewayRole: "target-readback",
expectedGatewayId: M3_IO_CHAIN.targetGatewayId,
expectedGatewaySessionId: M3_IO_CHAIN.targetGatewaySessionId,
requestJson,
timeoutMs: config.timeoutMs
});
if (!gateway.available) {
return {
status: "blocked",
code: gateway.code ?? M3_IO_BLOCKER_CODES.gatewayUnavailable,
reason: gateway.reason
};
}
const command = normalizeCommand("di.read", {});
const box = findGatewayBox(gateway.status, command);
if (!box) {
return {
status: "blocked",
code: M3_IO_BLOCKER_CODES.boxUnavailable,
reason: `target gateway 已响应,但 ${M3_IO_CHAIN.targetResourceId} 未注册/不可用`
};
}
const capability = capabilityFor(command, box);
const dispatch = await invokeGateway(gateway.url, {
operation: "hardware.port.read",
projectId: M3_IO_CHAIN.projectId,
gatewaySessionId: gateway.gatewaySessionId,
resourceId: command.resourceId,
boxId: command.boxId,
capabilityId: capability.capabilityId,
port: command.port,
operationId: operation.operationId,
traceId: meta.traceId,
requestId: meta.requestId,
actorType: "service",
actorId: `svc_${CLOUD_API_SERVICE_ID}`,
requestedBy: meta.actorId,
source: "cloud-api-m3-io-control"
}, { requestJson, timeoutMs: config.timeoutMs });
return dispatch.ok && dispatch.body?.accepted === true
? {
status: "succeeded",
value: readValueFromGatewayResult(dispatch.body),
gatewaySessionId: gateway.gatewaySessionId,
resourceId: command.resourceId,
port: command.port,
result: dispatch.body
}
: {
status: "blocked",
code: M3_IO_BLOCKER_CODES.dispatchFailed,
reason: dispatch.error ?? dispatch.body?.error?.message ?? "target DI readback failed"
};
}
function normalizeAction(action) {
return String(action ?? "").trim().toLowerCase();
}
function normalizeCommand(action, params = {}) {
if (action === "do.write") {
return {
action,
gatewayId: params.gatewayId ?? params.sourceGatewayId ?? M3_IO_CHAIN.sourceGatewayId,
gatewaySessionId: params.gatewaySessionId ?? M3_IO_CHAIN.sourceGatewaySessionId,
resourceId: params.resourceId ?? M3_IO_CHAIN.sourceResourceId,
boxId: params.boxId ?? M3_IO_CHAIN.sourceBoxId,
port: normalizePort(params.port ?? M3_IO_CHAIN.sourcePort),
value: normalizeBoolean(params.value)
};
}
return {
action,
gatewayId: params.gatewayId ?? params.targetGatewayId ?? M3_IO_CHAIN.targetGatewayId,
gatewaySessionId: params.gatewaySessionId ?? M3_IO_CHAIN.targetGatewaySessionId,
resourceId: params.resourceId ?? M3_IO_CHAIN.targetResourceId,
boxId: params.boxId ?? M3_IO_CHAIN.targetBoxId,
port: normalizePort(params.port ?? M3_IO_CHAIN.targetPort)
};
}
function validateM3Command(command) {
if (command.action === "do.write") {
if (command.gatewayId !== M3_IO_CHAIN.sourceGatewayId || command.resourceId !== M3_IO_CHAIN.sourceResourceId || command.port !== M3_IO_CHAIN.sourcePort) {
return `M3 只允许 ${M3_IO_CHAIN.sourceGatewayId}/${M3_IO_CHAIN.sourceResourceId}/${M3_IO_CHAIN.sourcePort} 执行 DO write;当前请求会造成端口方向或资源越界。`;
}
if (typeof command.value !== "boolean") {
return "M3 DO1 写入值必须是 boolean。";
}
return null;
}
if (command.action === "di.read") {
if (command.gatewayId !== M3_IO_CHAIN.targetGatewayId || command.resourceId !== M3_IO_CHAIN.targetResourceId || command.port !== M3_IO_CHAIN.targetPort) {
return `M3 只允许 ${M3_IO_CHAIN.targetGatewayId}/${M3_IO_CHAIN.targetResourceId}/${M3_IO_CHAIN.targetPort} 执行 DI read;当前请求会造成端口方向或资源越界。`;
}
}
return null;
}
function normalizePort(port) {
return String(port ?? "").trim().toUpperCase();
}
function normalizeBoolean(value) {
if (typeof value === "boolean") return value;
if (value === "true") return true;
if (value === "false") return false;
return value;
}
function buildRequestMeta(params = {}, context = {}) {
return {
traceId: params.traceId || context.traceId || `trc_${randomUUID()}`,
requestId: params.requestId || context.requestId || `req_${randomUUID()}`,
actorType: "user",
actorId: params.actorId || context.actorId || "usr_hwlab_cloud_web",
serviceId: CLOUD_API_SERVICE_ID,
environment: ENVIRONMENT_DEV
};
}
function createOperationRecord({ action, command, capability, gatewaySessionId, meta, now }) {
const operationId = command.operationId || `op_m3_${action.replace(".", "_")}_${randomUUID()}`;
const operation = {
operationId,
projectId: M3_IO_CHAIN.projectId,
gatewaySessionId,
resourceId: command.resourceId,
capabilityId: capability.capabilityId,
requestedBy: meta.actorId,
input: {
action,
port: command.port,
...(Object.hasOwn(command, "value") ? { value: command.value } : {}),
route: {
from: `${M3_IO_CHAIN.sourceResourceId}:${M3_IO_CHAIN.sourcePort}`,
via: M3_IO_CHAIN.patchPanelServiceId,
to: `${M3_IO_CHAIN.targetResourceId}:${M3_IO_CHAIN.targetPort}`
},
frontendBypass: false
},
status: "accepted",
environment: ENVIRONMENT_DEV,
requestedAt: now,
updatedAt: now
};
assertProtocolRecord("hardwareOperation", operation);
return operation;
}
function capabilityFor(command, box = {}) {
const suffix = command.action === "do.write" ? "write" : "read";
const portPart = command.port.toLowerCase();
const boxId = box.boxId ?? command.boxId;
return {
capabilityId: `cap_${boxId}_${portPart}_${suffix}`,
resourceId: command.resourceId,
projectId: M3_IO_CHAIN.projectId,
name: command.action,
description: `M3 ${command.action} ${command.resourceId}:${command.port}`,
direction: command.action === "do.write" ? "output" : "input",
valueType: "boolean",
constraints: {
port: command.port,
portFamily: command.action === "do.write" ? "DO" : "DI",
patchPanelOnlyPropagation: true,
frontendBypassAllowed: false
},
mutatesState: command.action === "do.write",
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString()
};
}
async function readGatewaySession(url, { gatewayRole, expectedGatewayId, expectedGatewaySessionId, requestJson, timeoutMs }) {
if (!url) {
return {
available: false,
role: gatewayRole,
code: M3_IO_BLOCKER_CODES.gatewayUnavailable,
reason: "gateway 未注册/不可用:cloud-api 未配置 gateway-simu endpoint"
};
}
const response = await requestRuntimeJson(new URL("/status", ensureTrailingSlash(url)).toString(), {
method: "GET",
requestJson,
timeoutMs
});
if (!response.ok) {
return {
available: false,
role: gatewayRole,
url: redactUrl(url),
code: M3_IO_BLOCKER_CODES.gatewayUnavailable,
reason: `gateway 未注册/不可用:${response.error ?? `HTTP ${response.status}`}`
};
}
const status = response.body ?? {};
const gatewaySessionId = status.session?.gatewaySessionId ?? status.gatewaySessionId ?? status.registry?.gatewaySessionId;
const gatewayId = status.gatewayId ?? status.session?.gatewayId ?? status.registry?.gatewayId;
if (!gatewaySessionId || !gatewayId) {
return {
available: false,
role: gatewayRole,
url: redactUrl(url),
status,
code: M3_IO_BLOCKER_CODES.gatewayUnavailable,
reason: "gateway 未注册/不可用:/status 未返回 gatewaySessionId/gatewayId"
};
}
if (expectedGatewayId && gatewayId !== expectedGatewayId) {
return {
available: false,
role: gatewayRole,
url: redactUrl(url),
status,
code: M3_IO_BLOCKER_CODES.gatewayIdentityMismatch,
reason: `gateway-simu identity mismatchexpected gatewayId=${expectedGatewayId}actual=${gatewayId}indexed simulator identity drift must fail closed.`
};
}
if (expectedGatewaySessionId && gatewaySessionId !== expectedGatewaySessionId) {
return {
available: false,
role: gatewayRole,
url: redactUrl(url),
status,
code: M3_IO_BLOCKER_CODES.gatewayIdentityMismatch,
reason: `gateway-simu identity mismatchexpected gatewaySessionId=${expectedGatewaySessionId}actual=${gatewaySessionId}indexed simulator identity drift must fail closed.`
};
}
return {
available: true,
role: gatewayRole,
url,
gatewayId,
gatewaySessionId,
status
};
}
function findGatewayBox(gatewayStatus, command) {
const boxes = [
...(Array.isArray(gatewayStatus?.registry?.boxes) ? gatewayStatus.registry.boxes : []),
...(Array.isArray(gatewayStatus?.boxes)
? gatewayStatus.boxes.map((item) => typeof item === "string" ? { resourceId: item } : item)
: [])
];
return boxes.find((box) => box?.resourceId === command.resourceId || box?.boxId === command.boxId) ?? null;
}
async function invokeGateway(gatewayUrl, body, options) {
return requestRuntimeJson(new URL("/invoke", ensureTrailingSlash(gatewayUrl)).toString(), {
method: "POST",
body,
requestJson: options.requestJson,
timeoutMs: options.timeoutMs
});
}
async function tickPatchPanel(patchPanelUrl, body, options) {
if (!patchPanelUrl) {
return {
ok: false,
status: 0,
error: "patch-panel endpoint is not configured"
};
}
return requestRuntimeJson(new URL("/sync/tick", ensureTrailingSlash(patchPanelUrl)).toString(), {
method: "POST",
body,
requestJson: options.requestJson,
timeoutMs: options.timeoutMs
});
}
function classifyPatchPanelResult(patchPanel) {
if (!patchPanel.ok) {
return {
code: M3_IO_BLOCKER_CODES.patchPanelUnavailable,
reason: `hwlab-patch-panel 不可用:${patchPanel.error ?? `HTTP ${patchPanel.status}`}`
};
}
if (patchPanel.body?.accepted !== true) {
const diagnostic = firstDiagnostic(patchPanel.body);
return {
code: diagnostic?.code === "target_endpoint_missing" ? M3_IO_BLOCKER_CODES.boxUnavailable : M3_IO_BLOCKER_CODES.patchPanelUnavailable,
reason: diagnostic?.message ?? "hwlab-patch-panel 拒绝同步;请检查 wiring 和 endpointMap。"
};
}
if (!hasExpectedPatchDelivery(patchPanel.body)) {
return {
code: M3_IO_BLOCKER_CODES.wiringMissing,
reason: `hwlab-patch-panel 未路由 ${M3_IO_CHAIN.sourceResourceId}:${M3_IO_CHAIN.sourcePort} -> ${M3_IO_CHAIN.targetResourceId}:${M3_IO_CHAIN.targetPort}wiring 不存在或不是 active。`
};
}
return null;
}
function hasExpectedPatchDelivery(body) {
const routes = Array.isArray(body?.routes) ? body.routes : [];
return routes.some((route) =>
(route.deliveries ?? []).some((delivery) =>
delivery.sourceResourceId === M3_IO_CHAIN.sourceResourceId &&
delivery.sourcePort === M3_IO_CHAIN.sourcePort &&
delivery.resourceId === M3_IO_CHAIN.targetResourceId &&
delivery.port === M3_IO_CHAIN.targetPort &&
delivery.deliveryStatus !== "failed"
)
);
}
function firstDiagnostic(body) {
return [
...(Array.isArray(body?.diagnostics) ? body.diagnostics : []),
...(Array.isArray(body?.routes) ? body.routes.flatMap((route) => route.diagnostics ?? []) : [])
][0] ?? null;
}
async function requestRuntimeJson(url, { method = "GET", body, requestJson, timeoutMs = DEFAULT_TIMEOUT_MS }) {
if (requestJson) {
try {
const result = await requestJson(url, { method, body, timeoutMs });
return normalizeRuntimeJsonResponse(result);
} catch (error) {
return {
ok: false,
status: 0,
error: error instanceof Error ? error.message : String(error)
};
}
}
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), timeoutMs);
try {
const response = await fetch(url, {
method,
headers: body === undefined
? { accept: "application/json" }
: {
accept: "application/json",
"content-type": "application/json"
},
body: body === undefined ? undefined : JSON.stringify(body),
signal: controller.signal
});
const text = await response.text();
let parsed = null;
try {
parsed = text ? JSON.parse(text) : null;
} catch {
parsed = null;
}
return {
ok: response.ok,
status: response.status,
body: parsed,
error: response.ok ? null : parsed?.error?.message ?? parsed?.message ?? response.statusText
};
} catch (error) {
return {
ok: false,
status: 0,
error: error.name === "AbortError" ? `request timed out after ${timeoutMs}ms` : error.message
};
} finally {
clearTimeout(timer);
}
}
function normalizeRuntimeJsonResponse(result = {}) {
if (Object.hasOwn(result, "ok") || Object.hasOwn(result, "body")) {
return {
ok: result.ok ?? (Number(result.status ?? 200) >= 200 && Number(result.status ?? 200) < 300),
status: result.status ?? 200,
body: result.body ?? result.json ?? null,
error: result.error ?? result.body?.error?.message ?? null
};
}
if (Object.hasOwn(result, "status") || Object.hasOwn(result, "json")) {
const status = result.status ?? 200;
return {
ok: status >= 200 && status < 300,
status,
body: result.json ?? null,
error: result.error ?? null
};
}
return {
ok: true,
status: 200,
body: result
};
}
async function persistRuntimeRegistrations({ runtimeStore, gatewayStatus, box, capability, command, meta }) {
const gatewaySessionId = gatewayStatus.session?.gatewaySessionId ?? gatewayStatus.gatewaySessionId ?? gatewayStatus.registry?.gatewaySessionId ?? command.gatewaySessionId;
const gatewayId = gatewayStatus.gatewayId ?? gatewayStatus.session?.gatewayId ?? gatewayStatus.registry?.gatewayId ?? command.gatewayId;
const gatewaySession = {
projectId: M3_IO_CHAIN.projectId,
gatewaySessionId,
gatewayId,
serviceId: "hwlab-gateway-simu",
endpoint: gatewayStatus.registry?.endpoint ?? gatewayStatus.session?.endpoint,
status: "connected",
labels: {
m3ControlSurface: "true"
}
};
const resource = {
projectId: M3_IO_CHAIN.projectId,
gatewaySessionId,
resourceId: command.resourceId,
boxId: box.boxId ?? command.boxId,
resourceType: "simulator_endpoint",
state: "available",
metadata: {
serviceId: "hwlab-box-simu",
patchPanelOnlyPropagation: true,
frontendBypassAllowed: false
}
};
const writes = [];
writes.push(await tryRuntimeWrite("gateway.session.register", () => runtimeStore?.registerGatewaySession?.(gatewaySession, meta)));
writes.push(await tryRuntimeWrite("box.resource.register", () => runtimeStore?.registerBoxResource?.(resource, meta)));
writes.push(await tryRuntimeWrite("box.capability.report", () => runtimeStore?.reportBoxCapabilities?.({ capability }, meta)));
return writes;
}
async function tryRuntimeWrite(label, write) {
if (typeof write !== "function") {
return {
label,
written: false,
skipped: true,
reason: "runtimeStore method unavailable"
};
}
try {
const result = await write();
return {
label,
written: true,
result
};
} catch (error) {
return {
label,
written: false,
reason: error instanceof Error ? error.message : String(error),
code: error?.code ?? null,
data: error?.data ?? null
};
}
}
async function successControlResult(input) {
const records = await writeOutcomeRecords({
...input,
outcome: "succeeded",
reason: null
});
return {
serviceId: CLOUD_API_SERVICE_ID,
contractVersion: M3_IO_CONTROL_CONTRACT_VERSION,
status: "succeeded",
accepted: true,
action: input.action,
chain: M3_IO_CHAIN,
command: input.command,
operationId: input.operation.operationId,
traceId: input.meta.traceId,
auditId: records.auditEvent.auditId,
evidenceId: records.evidenceRecord.evidenceId,
operation: {
...input.operation,
status: "succeeded",
completedAt: input.now,
updatedAt: input.now
},
result: {
value: input.command.action === "di.read"
? readValueFromGatewayResult(input.dispatch.body)
: input.command.value,
dispatch: gatewayResultSummary(input.gateway),
patchPanel: input.patchPanel?.body ?? null,
targetReadback: input.targetReadback ?? null
},
controlPath: {
status: "succeeded",
cloudApi: true,
gatewaySimu: true,
boxSimu: true,
patchPanel: input.action === "do.write",
frontendBypass: false
},
auditEvent: records.auditEvent,
evidenceRecord: records.evidenceRecord,
evidenceState: evidenceState(records.runtimeAfter, records),
persistence: persistenceSummary(records.runtimeAfter, input.registration, input.operationWrite, records),
observedAt: input.now
};
}
async function blockedControlResult(input) {
const records = await writeOutcomeRecords({
...input,
outcome: "failed",
reason: input.reason
});
return {
serviceId: CLOUD_API_SERVICE_ID,
contractVersion: M3_IO_CONTROL_CONTRACT_VERSION,
status: "blocked",
accepted: false,
action: input.action,
chain: M3_IO_CHAIN,
command: input.command,
operationId: input.operation.operationId,
traceId: input.meta.traceId,
auditId: records.auditEvent.auditId,
evidenceId: records.evidenceRecord.evidenceId,
blocker: {
code: input.code,
message: input.reason,
zh: input.reason
},
operation: {
...input.operation,
status: "failed",
completedAt: input.now,
updatedAt: input.now,
error: {
code: input.code,
message: input.reason
}
},
result: {
dispatch: input.gateway ? gatewayResultSummary(input.gateway) : null,
patchPanel: input.patchPanel?.body ?? null,
targetReadback: input.targetReadback ?? null
},
controlPath: {
status: "blocked",
cloudApi: true,
gatewaySimu: Boolean(input.gateway),
boxSimu: input.code !== M3_IO_BLOCKER_CODES.gatewayUnavailable,
patchPanel: Boolean(input.patchPanel?.ok),
frontendBypass: false
},
auditEvent: records.auditEvent,
evidenceRecord: records.evidenceRecord,
evidenceState: evidenceState(records.runtimeAfter, records),
persistence: persistenceSummary(records.runtimeAfter, input.registration, input.operationWrite, records),
observedAt: input.now
};
}
function blockedResult({ action, command = null, meta, runtime, code, reason, httpStatus, now, details = {} }) {
return {
serviceId: CLOUD_API_SERVICE_ID,
contractVersion: M3_IO_CONTROL_CONTRACT_VERSION,
status: "blocked",
accepted: false,
action: action || "unknown",
chain: M3_IO_CHAIN,
command,
operationId: null,
traceId: meta.traceId,
auditId: null,
evidenceId: null,
blocker: {
code,
message: reason,
zh: reason
},
controlPath: {
status: "blocked",
cloudApi: true,
gatewaySimu: false,
boxSimu: false,
patchPanel: false,
frontendBypass: false
},
evidenceState: evidenceState(runtime, {
auditWrite: { written: false, reason: "operation was blocked before audit write" },
evidenceWrite: { written: false, reason: "operation was blocked before evidence write" }
}),
persistence: {
runtime,
writes: []
},
observedAt: now,
httpStatus,
...details
};
}
async function writeOutcomeRecords({
action,
command,
operation,
meta,
runtimeStore,
runtimeBefore,
registration,
operationWrite,
tracePayload,
outcome,
reason,
now
}) {
const auditEvent = createProtocolAuditEvent({
auditId: `aud_${operation.operationId.slice(3)}_${outcome}`,
traceId: meta.traceId,
actorType: "user",
actorId: meta.actorId,
action: action === "do.write" ? "m3.io.do.write" : "m3.io.di.read",
targetType: "hardware_operation",
targetId: operation.operationId,
projectId: M3_IO_CHAIN.projectId,
gatewaySessionId: operation.gatewaySessionId,
operationId: operation.operationId,
serviceId: CLOUD_API_SERVICE_ID,
outcome,
reason: reason ?? undefined,
metadata: {
route: `${M3_IO_CHAIN.sourceResourceId}:${M3_IO_CHAIN.sourcePort}->${M3_IO_CHAIN.patchPanelServiceId}->${M3_IO_CHAIN.targetResourceId}:${M3_IO_CHAIN.targetPort}`,
command,
frontendBypass: false,
runtimeDurableBefore: runtimeBefore?.status ?? "unknown"
},
occurredAt: now
});
const evidenceRecord = createEvidenceRecord({
evidenceId: `evd_${operation.operationId.slice(3)}_${outcome}`,
projectId: M3_IO_CHAIN.projectId,
operationId: operation.operationId,
traceId: meta.traceId,
createdAt: now,
payload: {
outcome,
reason,
tracePayload,
command,
frontendBypass: false,
patchPanelOnlyPropagation: true
}
});
const auditWrite = await tryRuntimeWrite("audit.event.write", () => runtimeStore?.writeAuditEvent?.({ event: auditEvent }, meta));
const evidenceWrite = await tryRuntimeWrite("evidence.record.write", () => runtimeStore?.writeEvidenceRecord?.({ record: evidenceRecord }, meta));
const runtimeAfter = await safeRuntimeSummary(runtimeStore);
return {
auditEvent,
evidenceRecord,
auditWrite,
evidenceWrite,
runtimeAfter,
registration,
operationWrite
};
}
function createEvidenceRecord({ evidenceId, projectId, operationId, traceId, payload, createdAt }) {
const metadata = {
traceId,
serviceId: CLOUD_API_SERVICE_ID,
route: `${M3_IO_CHAIN.sourceResourceId}:${M3_IO_CHAIN.sourcePort}->${M3_IO_CHAIN.patchPanelServiceId}->${M3_IO_CHAIN.targetResourceId}:${M3_IO_CHAIN.targetPort}`,
frontendBypass: false,
patchPanelOnlyPropagation: true
};
const serialized = JSON.stringify({
serviceId: CLOUD_API_SERVICE_ID,
projectId,
operationId,
traceId,
payload,
metadata,
createdAt
});
const record = {
evidenceId,
projectId,
operationId,
kind: "trace",
uri: `memory://hwlab-cloud-api/${operationId}/m3-io-control.json`,
mimeType: "application/json",
sha256: createHash("sha256").update(serialized).digest("hex"),
sizeBytes: Buffer.byteLength(serialized),
serviceId: CLOUD_API_SERVICE_ID,
environment: ENVIRONMENT_DEV,
metadata,
createdAt
};
assertProtocolRecord("evidenceRecord", record);
return record;
}
function evidenceState(runtime, records) {
const durableReady = isDurableRuntimeReady(runtime);
const recordsWritten = records.auditWrite?.written === true && records.evidenceWrite?.written === true;
if (durableReady && recordsWritten) {
return {
status: "green",
sourceKind: "DEV-LIVE",
durable: true,
reason: "operation/audit/evidence 已通过 durable runtime 写入;仍需 DEV 浏览器 E2E 才能宣称验收通过。"
};
}
const blocker = runtime?.blocker ?? M3_IO_BLOCKER_CODES.runtimeDurableBlocked;
return {
status: "blocked",
sourceKind: "BLOCKED",
durable: runtime?.durable === true,
blocker,
reason: `操作链路可达性与可信持久化分离:runtime durable 未 greenstatus=${runtime?.status ?? "unknown"}blocker=${blocker}),不能宣称可信闭环完全 green。`
};
}
function persistenceSummary(runtime, registration, operationWrite, records) {
return {
runtime,
writes: [
...(registration ?? []),
operationWrite,
records.auditWrite,
records.evidenceWrite
].filter(Boolean)
};
}
async function safeRuntimeSummary(runtimeStore) {
try {
if (typeof runtimeStore?.readiness === "function") {
return await runtimeStore.readiness();
}
if (typeof runtimeStore?.summary === "function") {
return runtimeStore.summary();
}
} catch (error) {
return {
adapter: "unknown",
durable: false,
ready: false,
status: "blocked",
blocker: "runtime_summary_unavailable",
reason: error instanceof Error ? error.message : String(error),
liveRuntimeEvidence: false
};
}
return {
adapter: "none",
durable: false,
ready: false,
status: "blocked",
blocker: "runtime_store_missing",
reason: "runtime store is not available",
liveRuntimeEvidence: false
};
}
function isDurableRuntimeReady(runtime) {
if (!runtime || typeof runtime !== "object") return false;
if (runtime.durable !== true) return false;
if (runtime.ready === false) return false;
if (runtime.liveRuntimeEvidence !== true) return false;
return !["blocked", "degraded", "failed"].includes(String(runtime.status ?? "").toLowerCase());
}
function readValueFromGatewayResult(result) {
return result?.result?.value ?? result?.value ?? null;
}
function gatewayResultSummary(gateway) {
return {
gatewayId: gateway.gatewayId,
gatewaySessionId: gateway.gatewaySessionId,
url: redactUrl(gateway.url),
role: gateway.role
};
}
function publicGatewayStatus(status) {
return {
gatewayId: status?.gatewayId ?? status?.session?.gatewayId ?? null,
gatewaySessionId: status?.session?.gatewaySessionId ?? status?.gatewaySessionId ?? null,
boxCount: Array.isArray(status?.registry?.boxes) ? status.registry.boxes.length : null
};
}
function finalizeControlResult(result) {
return result;
}
function ensureTrailingSlash(url) {
return url.endsWith("/") ? url : `${url}/`;
}
function trimUrl(value) {
const text = String(value ?? "").trim();
return text ? text : null;
}
function parseTimeout(value) {
const parsed = Number.parseInt(value ?? "", 10);
return Number.isInteger(parsed) && parsed > 0 ? parsed : DEFAULT_TIMEOUT_MS;
}
function redactUrl(url) {
try {
const parsed = new URL(url);
parsed.username = parsed.username ? "***" : "";
parsed.password = parsed.password ? "***" : "";
return parsed.toString();
} catch {
return "invalid-url";
}
}