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"; import { requestRuntimeJson, normalizeRuntimeJsonResponse, persistRuntimeRegistrations, tryRuntimeWrite, successControlResult, blockedControlResult, blockedResult, writeOutcomeRecords, createEvidenceRecord, evidenceState, auditState, recordWriteState, durableStatus, trustBlocker, redactedBlockerClassification, blockerCategory, redactBlockerMessage, controlTarget, persistenceSummary, safeRuntimeSummary, isDurableRuntimeReady, readValueFromGatewayResult, gatewayResultSummary, publicGatewayStatus, finalizeControlResult, ensureTrailingSlash, trimUrl, parseTimeout, redactUrl } from "./m3-io-control-runtime.ts"; import { HWLAB_M3_IO_CAPABILITY_LEVELS } from "../../skills/hwlab-agent-runtime/scripts/src/m3-io-skill-client.mjs"; export const M3_IO_CONTROL_CONTRACT_VERSION = "m3-io-control-v1"; export const M3_IO_CONTROL_ROUTE = "/v1/m3/io"; export const M3_STATUS_CONTRACT_VERSION = "m3-status-v1"; export const M3_STATUS_ROUTE = "/v1/m3/status"; export const M3_IO_RPC_METHODS = Object.freeze({ doWrite: "m3.io.do.write", diRead: "m3.io.di.read" }); 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", 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" }); export const DEFAULT_TIMEOUT_MS = 2200; const DEV_SERVICE_ENDPOINTS = Object.freeze({ gateway1: "", gateway2: "", patchPanel: "" }); 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", capabilityLevel: HWLAB_M3_IO_CAPABILITY_LEVELS.blocked, readinessStatus: "not_checked", capabilityLevels: [ HWLAB_M3_IO_CAPABILITY_LEVELS.blocked, HWLAB_M3_IO_CAPABILITY_LEVELS.ready ], 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 describeM3IoControlLive(options = {}) { const contract = describeM3IoControl(options); const env = options.env ?? process.env; const config = buildM3IoConfig(env); const runtime = await safeRuntimeSummary(options.runtimeStore); const readiness = await buildM3IoReadiness({ config, requestJson: options.m3IoRequestJson ?? options.requestJson, now: options.now }); const controlReady = readiness.status === "ready" && readiness.controlReady === true; const capabilityLevel = controlReady ? HWLAB_M3_IO_CAPABILITY_LEVELS.ready : HWLAB_M3_IO_CAPABILITY_LEVELS.blocked; const trustReadiness = buildM3IoTrustReadiness(runtime, { observedAt: readiness.observedAt, controlReady }); return { ...contract, status: controlReady ? "available" : "blocked", sourceKind: controlReady ? "DEV-LIVE" : "BLOCKED", capabilityLevel, readinessStatus: readiness.status, readiness: { ...readiness, capabilityLevel, trust: trustReadiness }, trustReadiness, controlReady, blockedReason: controlReady ? null : readiness.blocker?.zh ?? contract.blockedReason ?? "M3 IO 只读 readiness 未 green;控制面板保持阻塞。" }; } export async function describeM3StatusLive(options = {}) { const env = options.env ?? process.env; const config = buildM3IoConfig(env); const requestJson = options.m3IoRequestJson ?? options.requestJson; const observedAt = options.now?.() ?? new Date().toISOString(); const runtime = await safeRuntimeSummary(options.runtimeStore); const trust = await buildM3StatusTrust({ runtimeStore: options.runtimeStore, runtime }); const base = { serviceId: CLOUD_API_SERVICE_ID, contractVersion: M3_STATUS_CONTRACT_VERSION, route: M3_STATUS_ROUTE, observedAt, chain: M3_IO_CHAIN, boundaries: { frontendCallsOnly: M3_STATUS_ROUTE, cloudApiAggregatesRuntimeStatus: true, patchPanelOwnsPropagation: true, directFrontendGatewayOrBoxAccess: false, sourceTopologyFallbackMustStayUnverified: true } }; if (!config.enabled) { const blocker = { code: "m3_control_disabled", layer: "cloud-api", zh: "M3 状态聚合未启用;前端不能绕过 cloud-api 直连 gateway/box-simu。" }; const gateways = defaultM3Gateways({ sourceKind: "UNVERIFIED", observedAt }); const boxes = defaultM3Boxes({ sourceKind: "UNVERIFIED", observedAt }); const patchPanel = defaultM3PatchPanel({ observedAt }); const blockedTrust = { ...trust, blocker: "m3_control_disabled" }; return { ...base, status: "blocked", sourceKind: "BLOCKED", summary: "M3 状态聚合未启用;前端不能绕过 cloud-api 直连 gateway/box-simu。", gateways, boxes, patchPanel, io: m3StatusIoSummary({ boxes, patchPanel, hardwareLive: false, observedAt }), operation: m3StatusOperationSummary(blockedTrust), trace: m3StatusTraceSummary(blockedTrust), audit: m3StatusAuditSummary(blockedTrust), runtimeDurable: m3StatusRuntimeDurableSummary(blockedTrust), trust: blockedTrust, blocker }; } const [sourceGateway, targetGateway, patchStatus, patchWiring] = await Promise.all([ readGatewaySession(config.gateway1Url, { gatewayRole: "source", expectedGatewayId: M3_IO_CHAIN.sourceGatewayId, expectedGatewaySessionId: M3_IO_CHAIN.sourceGatewaySessionId, requestJson, timeoutMs: config.timeoutMs }), readGatewaySession(config.gateway2Url, { gatewayRole: "target", expectedGatewayId: M3_IO_CHAIN.targetGatewayId, expectedGatewaySessionId: M3_IO_CHAIN.targetGatewaySessionId, requestJson, timeoutMs: config.timeoutMs }), requestRuntimeJson(new URL("/status", ensureTrailingSlash(config.patchPanelUrl)).toString(), { method: "GET", requestJson, timeoutMs: config.timeoutMs }), requestRuntimeJson(new URL("/wiring", ensureTrailingSlash(config.patchPanelUrl)).toString(), { method: "GET", requestJson, timeoutMs: config.timeoutMs }) ]); const sourceCommand = normalizeCommand("do.write", { value: false }); const targetCommand = normalizeCommand("di.read", {}); const sourceBox = sourceGateway.available ? findGatewayBox(sourceGateway.status, sourceCommand) : null; const targetBox = targetGateway.available ? findGatewayBox(targetGateway.status, targetCommand) : null; const [sourceDoRead, targetDiRead] = await Promise.all([ sourceGateway.available && sourceBox ? readM3StatusPort({ gateway: sourceGateway, command: sourceCommand, requestJson, timeoutMs: config.timeoutMs, observedAt, port: M3_IO_CHAIN.sourcePort, direction: "output" }) : Promise.resolve(unverifiedPortRead({ port: M3_IO_CHAIN.sourcePort, direction: "output", source: "gateway-simu", reason: sourceGateway.reason ?? `${M3_IO_CHAIN.sourceResourceId} 未在 source gateway registry 中可见`, observedAt })), targetGateway.available && targetBox ? readM3StatusPort({ gateway: targetGateway, command: targetCommand, requestJson, timeoutMs: config.timeoutMs, observedAt, port: M3_IO_CHAIN.targetPort, direction: "input" }) : Promise.resolve(unverifiedPortRead({ port: M3_IO_CHAIN.targetPort, direction: "input", source: "gateway-simu", reason: targetGateway.reason ?? `${M3_IO_CHAIN.targetResourceId} 未在 target gateway registry 中可见`, observedAt })) ]); const gateways = [ gatewayStatusEntry(sourceGateway, { id: M3_IO_CHAIN.sourceGatewayId, sessionId: M3_IO_CHAIN.sourceGatewaySessionId, role: "source", observedAt }), gatewayStatusEntry(targetGateway, { id: M3_IO_CHAIN.targetGatewayId, sessionId: M3_IO_CHAIN.targetGatewaySessionId, role: "target", observedAt }) ]; const boxes = [ boxStatusEntry({ gateway: sourceGateway, box: sourceBox, boxId: M3_IO_CHAIN.sourceBoxId, resourceId: M3_IO_CHAIN.sourceResourceId, gatewayId: M3_IO_CHAIN.sourceGatewayId, gatewaySessionId: M3_IO_CHAIN.sourceGatewaySessionId, observedAt, ports: { [M3_IO_CHAIN.sourcePort]: sourceDoRead.port } }), boxStatusEntry({ gateway: targetGateway, box: targetBox, boxId: M3_IO_CHAIN.targetBoxId, resourceId: M3_IO_CHAIN.targetResourceId, gatewayId: M3_IO_CHAIN.targetGatewayId, gatewaySessionId: M3_IO_CHAIN.targetGatewaySessionId, observedAt, ports: { [M3_IO_CHAIN.targetPort]: targetDiRead.port } }) ]; const patchPanel = patchPanelStatusEntry({ statusResponse: patchStatus, wiringResponse: patchWiring, observedAt }); const blockers = [ ...gateways.filter((gateway) => !gateway.online).map((gateway) => ({ code: gateway.blocker, layer: gateway.id, zh: gateway.lastError })), ...boxes.filter((box) => !box.online).map((box) => ({ code: M3_IO_BLOCKER_CODES.boxUnavailable, layer: box.resourceId, zh: `${box.resourceId} 未在 gateway-simu registry 中可见` })), patchPanel.connectionActive ? null : { code: patchPanel.observable === false ? M3_IO_BLOCKER_CODES.patchPanelUnavailable : M3_IO_BLOCKER_CODES.wiringMissing, layer: M3_IO_CHAIN.patchPanelServiceId, zh: patchPanel.lastError ?? `hwlab-patch-panel 未确认 active ${M3_IO_CHAIN.sourceResourceId}:${M3_IO_CHAIN.sourcePort} -> ${M3_IO_CHAIN.targetResourceId}:${M3_IO_CHAIN.targetPort} 接线` }, sourceDoRead.error ? { code: M3_IO_BLOCKER_CODES.dispatchFailed, layer: M3_IO_CHAIN.sourceResourceId, zh: sourceDoRead.error } : null, targetDiRead.error ? { code: M3_IO_BLOCKER_CODES.dispatchFailed, layer: M3_IO_CHAIN.targetResourceId, zh: targetDiRead.error } : null ].filter(Boolean); const hardwareLive = blockers.length === 0 && sourceDoRead.port.sourceKind === "DEV-LIVE" && targetDiRead.port.sourceKind === "DEV-LIVE"; const trustGreen = trust.durableStatus === "green" && trust.evidenceId && trust.auditId && trust.operationId && trust.traceId; const status = blockers.length > 0 ? blockers.some((blocker) => blocker.code === M3_IO_BLOCKER_CODES.dispatchFailed || blocker.code === M3_IO_BLOCKER_CODES.patchPanelUnavailable) ? "error" : "blocked" : trustGreen ? "live" : hardwareLive ? "blocked" : "unverified"; const sourceKind = hardwareLive ? "DEV-LIVE" : "UNVERIFIED"; const blocker = blockers[0] ?? (trust.blocker ? { code: trust.blocker, layer: "runtime-durable", zh: `runtime durable 仍 blocked:${trust.blocker}` } : null); return { ...base, status, sourceKind, summary: status === "live" ? "M3 硬件状态与可信持久化均为 DEV-LIVE。" : hardwareLive ? "M3 DO1/DI1 实况读数来自 cloud-api 聚合入口;可信持久化仍保持 blocked,不得标为 trusted green。" : "M3 硬件状态尚未完整验证;SOURCE 拓扑只能作为未验证 fallback。", gateways, boxes, patchPanel, io: m3StatusIoSummary({ boxes, patchPanel, hardwareLive, observedAt }), operation: m3StatusOperationSummary(trust), trace: m3StatusTraceSummary(trust), audit: m3StatusAuditSummary(trust), runtimeDurable: m3StatusRuntimeDurableSummary(trust), trust, blocker }; } 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 async function buildM3IoReadiness({ config, requestJson, now } = {}) { const observedAt = now?.() ?? new Date().toISOString(); const checks = []; if (!config?.enabled) { return blockedReadiness({ checks, code: "m3_control_disabled", layer: "cloud-api", reason: "M3 IO 控制未启用;前端不能绕过 cloud-api 直连 gateway/box-simu。", observedAt }); } const sourceGateway = await readGatewaySession(config.gateway1Url, { gatewayRole: "source", expectedGatewayId: M3_IO_CHAIN.sourceGatewayId, expectedGatewaySessionId: M3_IO_CHAIN.sourceGatewaySessionId, requestJson, timeoutMs: config.timeoutMs }); checks.push(readinessCheckFromGateway("source-gateway", sourceGateway)); if (!sourceGateway.available) { return blockedReadiness({ checks, code: sourceGateway.code ?? M3_IO_BLOCKER_CODES.gatewayUnavailable, layer: "gateway-simu-1", reason: sourceGateway.reason, observedAt }); } const sourceCommand = normalizeCommand("do.write", {}); const sourceBox = findGatewayBox(sourceGateway.status, sourceCommand); checks.push(readinessCheckFromBox("source-box", sourceBox, sourceCommand)); if (!sourceBox) { return blockedReadiness({ checks, code: M3_IO_BLOCKER_CODES.boxUnavailable, layer: "box-simu-1", reason: `gateway-simu-1 已响应,但 ${M3_IO_CHAIN.sourceResourceId} 未注册/不可用;控制面板保持阻塞。`, observedAt }); } const targetGateway = await readGatewaySession(config.gateway2Url, { gatewayRole: "target", expectedGatewayId: M3_IO_CHAIN.targetGatewayId, expectedGatewaySessionId: M3_IO_CHAIN.targetGatewaySessionId, requestJson, timeoutMs: config.timeoutMs }); checks.push(readinessCheckFromGateway("target-gateway", targetGateway)); if (!targetGateway.available) { return blockedReadiness({ checks, code: targetGateway.code ?? M3_IO_BLOCKER_CODES.gatewayUnavailable, layer: "gateway-simu-2", reason: targetGateway.reason, observedAt }); } const targetCommand = normalizeCommand("di.read", {}); const targetBox = findGatewayBox(targetGateway.status, targetCommand); checks.push(readinessCheckFromBox("target-box", targetBox, targetCommand)); if (!targetBox) { return blockedReadiness({ checks, code: M3_IO_BLOCKER_CODES.boxUnavailable, layer: "box-simu-2", reason: `gateway-simu-2 已响应,但 ${M3_IO_CHAIN.targetResourceId} 未注册/不可用;控制面板保持阻塞。`, observedAt }); } const patchStatus = await requestRuntimeJson(new URL("/status", ensureTrailingSlash(config.patchPanelUrl)).toString(), { method: "GET", requestJson, timeoutMs: config.timeoutMs }); checks.push(readinessCheckFromPatchPanelStatus(patchStatus)); if (!patchStatus.ok) { return blockedReadiness({ checks, code: M3_IO_BLOCKER_CODES.patchPanelUnavailable, layer: "hwlab-patch-panel", reason: `hwlab-patch-panel 不可用:${patchStatus.error ?? `HTTP ${patchStatus.status}`}`, observedAt }); } const wiring = await requestRuntimeJson(new URL("/wiring", ensureTrailingSlash(config.patchPanelUrl)).toString(), { method: "GET", requestJson, timeoutMs: config.timeoutMs }); checks.push(readinessCheckFromPatchPanelWiring(wiring)); if (!wiring.ok) { return blockedReadiness({ checks, code: M3_IO_BLOCKER_CODES.wiringMissing, layer: "hwlab-patch-panel", reason: `hwlab-patch-panel 接线读取失败:${wiring.error ?? `HTTP ${wiring.status}`}`, observedAt }); } if (!hasExpectedPatchPanelWiring(patchStatus.body) || !hasExpectedPatchPanelWiring(wiring.body)) { return blockedReadiness({ checks, code: M3_IO_BLOCKER_CODES.wiringMissing, layer: "hwlab-patch-panel", reason: `hwlab-patch-panel 未确认 active ${M3_IO_CHAIN.sourceResourceId}:${M3_IO_CHAIN.sourcePort} -> ${M3_IO_CHAIN.targetResourceId}:${M3_IO_CHAIN.targetPort} 接线;控制面板保持阻塞。`, observedAt }); } return { status: "ready", controlReady: true, capabilityLevel: HWLAB_M3_IO_CAPABILITY_LEVELS.ready, sourceKind: "DEV-LIVE", evidenceLevel: "DEV-LIVE", observedAt, summary: "cloud-api 只读 readiness 已确认 gateway-simu、box-simu 与 hwlab-patch-panel 目标接线;这只表示控制面板可执行受控操作,不等于 M3 验收通过。", chain: M3_IO_CHAIN, checks, blocker: null }; } export async function handleM3IoRpc(action, params = {}, envelope = {}, context = {}) { return handleM3IoControl( { ...params, action, traceId: envelope.meta?.traceId ?? params.traceId, requestId: envelope.id ?? params.requestId, actorId: envelope.meta?.actorId ?? params.actorId }, { ...context, requestJson: context.requestJson ?? context.m3IoRequestJson, traceId: envelope.meta?.traceId ?? context.traceId, requestId: envelope.id ?? context.requestId, actorId: envelope.meta?.actorId ?? context.actorId } ); } export function buildM3IoConfig(env = process.env) { const enabledValue = String(env.HWLAB_M3_IO_CONTROL_ENABLED ?? env.HWLAB_M3_CONTROL_ENABLED ?? "false").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) }; } function blockedReadiness({ checks, code, layer, reason, observedAt }) { return { status: "blocked", controlReady: false, capabilityLevel: HWLAB_M3_IO_CAPABILITY_LEVELS.blocked, sourceKind: "BLOCKED", evidenceLevel: "BLOCKED", observedAt, summary: `M3 IO 只读 readiness 未 green:${reason}`, chain: M3_IO_CHAIN, blocker: { code, layer, zh: reason }, capabilityBlocker: { code, layer, zh: reason, category: blockerCategory(code) }, checks }; } function buildM3IoTrustReadiness(runtime, { observedAt, controlReady } = {}) { const durable = durableStatus(runtime); const ready = isDurableRuntimeReady(runtime); const blocker = ready ? null : { code: durable.blocker ?? M3_IO_BLOCKER_CODES.runtimeDurableBlocked, layer: "runtime-durable", zh: `runtime durable 未 green:${durable.blocker ?? M3_IO_BLOCKER_CODES.runtimeDurableBlocked}`, category: "runtime_durable" }; return { status: ready ? "ready" : "blocked", controlReady: Boolean(controlReady), trustedEvidenceReady: ready, capabilityLevel: controlReady ? HWLAB_M3_IO_CAPABILITY_LEVELS.ready : HWLAB_M3_IO_CAPABILITY_LEVELS.blocked, evidenceLevel: ready ? "DEV-LIVE" : "BLOCKED", sourceKind: ready ? "DEV-LIVE" : "BLOCKED", observedAt, durableStatus: durable, blocker, summary: ready ? "runtime durable 已 green;operation/audit/evidence 可作为可信记录写入。" : "受控硬件路径 readiness 与可信持久化分离;runtime durable 未 green 时不得宣称完整硬件控制验收。" }; } async function buildM3StatusTrust({ runtimeStore, runtime }) { const durable = durableStatus(runtime); const base = { operationId: null, traceId: null, auditId: null, evidenceId: null, durableStatus: isDurableRuntimeReady(runtime) ? "green" : "blocked", runtime: durable, blocker: isDurableRuntimeReady(runtime) ? null : durable.blocker }; const [audit, evidence] = await Promise.all([ safeRuntimeRead(() => runtimeStore?.queryAuditEvents?.({ projectId: M3_IO_CHAIN.projectId, limit: 8 })), safeRuntimeRead(() => runtimeStore?.queryEvidenceRecords?.({ projectId: M3_IO_CHAIN.projectId, limit: 8 })) ]); const latestAudit = latestM3AuditEvent(audit.value?.events ?? []); const latestEvidence = latestM3EvidenceRecord(evidence.value?.records ?? []); const operationId = latestEvidence?.operationId ?? latestAudit?.operationId ?? null; const traceId = latestEvidence?.traceId ?? latestEvidence?.metadata?.traceId ?? latestAudit?.traceId ?? null; return { ...base, operationId, traceId, auditId: latestAudit?.auditId ?? null, evidenceId: latestEvidence?.evidenceId ?? null, durableStatus: base.durableStatus, blocker: base.blocker ?? audit.error?.code ?? evidence.error?.code ?? null, readStatus: { audit: audit.ok ? "read" : "blocked", evidence: evidence.ok ? "read" : "blocked", auditError: audit.error?.message ?? null, evidenceError: evidence.error?.message ?? null } }; } function m3StatusIoSummary({ boxes, patchPanel, hardwareLive, observedAt }) { const sourceBox = boxes.find((box) => box.resourceId === M3_IO_CHAIN.sourceResourceId) ?? null; const targetBox = boxes.find((box) => box.resourceId === M3_IO_CHAIN.targetResourceId) ?? null; const do1 = sourceBox?.ports?.[M3_IO_CHAIN.sourcePort] ?? null; const di1 = targetBox?.ports?.[M3_IO_CHAIN.targetPort] ?? null; const blocker = !sourceBox?.online ? M3_IO_BLOCKER_CODES.boxUnavailable : !targetBox?.online ? M3_IO_BLOCKER_CODES.boxUnavailable : patchPanel?.connectionActive !== true ? M3_IO_BLOCKER_CODES.wiringMissing : do1?.sourceKind !== "DEV-LIVE" || di1?.sourceKind !== "DEV-LIVE" ? "m3_io_unverified" : null; return { status: hardwareLive ? "live" : blocker ? "blocked" : "unverified", sourceKind: hardwareLive ? "DEV-LIVE" : "UNVERIFIED", observedAt, chain: { from: `${M3_IO_CHAIN.sourceResourceId}:${M3_IO_CHAIN.sourcePort}`, via: M3_IO_CHAIN.patchPanelServiceId, to: `${M3_IO_CHAIN.targetResourceId}:${M3_IO_CHAIN.targetPort}` }, do1: m3StatusPortSummary(sourceBox, M3_IO_CHAIN.sourcePort, do1), di1: m3StatusPortSummary(targetBox, M3_IO_CHAIN.targetPort, di1), patchPanel: { serviceId: patchPanel?.serviceId ?? M3_IO_CHAIN.patchPanelServiceId, connectionActive: patchPanel?.connectionActive === true, sourceKind: patchPanel?.sourceKind ?? "UNVERIFIED", observedAt: patchPanel?.lastSyncAt ?? observedAt, blocker: patchPanel?.connectionActive === true ? null : patchPanel?.lastError ?? M3_IO_BLOCKER_CODES.wiringMissing }, blocker }; } function m3StatusPortSummary(box, port, state) { return { resourceId: box?.resourceId ?? null, boxId: box?.id ?? null, gatewayId: box?.gatewayId ?? null, gatewaySessionId: box?.gatewaySessionId ?? null, port, direction: state?.direction ?? null, value: state?.value ?? null, source: state?.source ?? "unverified", sourceKind: state?.sourceKind ?? "UNVERIFIED", observedAt: state?.observedAt ?? box?.observedAt ?? null, blocker: state?.lastError ?? (box?.online ? null : M3_IO_BLOCKER_CODES.boxUnavailable) }; } function m3StatusOperationSummary(trust) { return { status: trust.operationId ? "read" : "blocked", operationId: trust.operationId ?? null, source: trust.operationId ? "runtime-store audit/evidence" : "runtime-store", blocker: trust.operationId ? null : trust.blocker ?? "operation_not_persisted", readStatus: { audit: trust.readStatus?.audit ?? "blocked", evidence: trust.readStatus?.evidence ?? "blocked" } }; } function m3StatusTraceSummary(trust) { return { status: trust.traceId ? "read" : "blocked", traceId: trust.traceId ?? null, source: trust.traceId ? "runtime-store audit/evidence" : "runtime-store", blocker: trust.traceId ? null : trust.blocker ?? "trace_not_persisted" }; } function m3StatusAuditSummary(trust) { return { status: trust.auditId ? "read" : "blocked", auditId: trust.auditId ?? null, readStatus: trust.readStatus?.audit ?? "blocked", source: trust.auditId ? "runtime-store audit.event.query" : "runtime-store", blocker: trust.auditId ? null : trust.readStatus?.auditError ?? trust.blocker ?? "audit_not_persisted" }; } function m3StatusRuntimeDurableSummary(trust) { return { status: trust.durableStatus === "green" ? "green" : "blocked", green: trust.durableStatus === "green", blocker: trust.blocker ?? null, runtime: trust.runtime, readStatus: trust.readStatus ?? { audit: "blocked", evidence: "blocked" }, auditReady: trust.readStatus?.audit === "read", evidenceReady: trust.readStatus?.evidence === "read" }; } async function safeRuntimeRead(read) { if (typeof read !== "function") { return { ok: false, value: null, error: { code: "runtime_read_unavailable", message: "runtime read method is unavailable" } }; } try { const value = await read(); if (value === undefined || value === null) { return { ok: false, value: null, error: { code: "runtime_read_unavailable", message: "runtime read returned no value" } }; } return { ok: true, value, error: null }; } catch (error) { return { ok: false, value: null, error: { code: error?.code ?? "runtime_read_blocked", message: error instanceof Error ? redactBlockerMessage(error.message) : String(error) } }; } } function latestM3AuditEvent(events) { return [...events].reverse().find((event) => event?.operationId && event?.traceId && (event.action === "m3.io.do.write" || event.action === "m3.io.di.read" || String(event.metadata?.route ?? "").includes("res_boxsimu_1:DO1")) ) ?? null; } function latestM3EvidenceRecord(records) { return [...records].reverse().find((record) => record?.operationId && (record.serviceId === CLOUD_API_SERVICE_ID || String(record.metadata?.route ?? "").includes("res_boxsimu_1:DO1")) ) ?? null; } function defaultM3Gateways({ sourceKind, observedAt }) { return [ { id: M3_IO_CHAIN.sourceGatewayId, role: "source", online: false, observable: false, sessionId: M3_IO_CHAIN.sourceGatewaySessionId, lastSeenAt: observedAt, sourceKind, lastError: "未验证" }, { id: M3_IO_CHAIN.targetGatewayId, role: "target", online: false, observable: false, sessionId: M3_IO_CHAIN.targetGatewaySessionId, lastSeenAt: observedAt, sourceKind, lastError: "未验证" } ]; } function defaultM3Boxes({ sourceKind, observedAt }) { return [ { id: M3_IO_CHAIN.sourceBoxId, resourceId: M3_IO_CHAIN.sourceResourceId, gatewayId: M3_IO_CHAIN.sourceGatewayId, gatewaySessionId: M3_IO_CHAIN.sourceGatewaySessionId, online: false, observable: false, sourceKind, observedAt, ports: { [M3_IO_CHAIN.sourcePort]: { value: null, direction: "output", source: "未验证", sourceKind, observedAt } } }, { id: M3_IO_CHAIN.targetBoxId, resourceId: M3_IO_CHAIN.targetResourceId, gatewayId: M3_IO_CHAIN.targetGatewayId, gatewaySessionId: M3_IO_CHAIN.targetGatewaySessionId, online: false, observable: false, sourceKind, observedAt, ports: { [M3_IO_CHAIN.targetPort]: { value: null, direction: "input", source: "未验证", sourceKind, observedAt } } } ]; } function defaultM3PatchPanel({ observedAt }) { return { serviceId: M3_IO_CHAIN.patchPanelServiceId, observable: false, connectionActive: false, connectionVersion: null, wiringConfigId: null, lastSyncAt: observedAt, lastError: "未验证", sourceKind: "UNVERIFIED" }; } async function readM3StatusPort({ gateway, command, requestJson, timeoutMs, observedAt, port, direction }) { const capability = capabilityFor({ ...command, action: "di.read", port }, { boxId: command.boxId, resourceId: command.resourceId }); 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, operationId: `op_m3_status_read_${port.toLowerCase()}_${randomUUID()}`, traceId: `trc_m3_status_read_${port.toLowerCase()}_${randomUUID()}`, actorType: "service", actorId: `svc_${CLOUD_API_SERVICE_ID}`, source: "cloud-api-m3-status" }, { requestJson, timeoutMs }); if (!dispatch.ok || dispatch.body?.accepted !== true) { return unverifiedPortRead({ port, direction, source: "gateway-simu", reason: `gateway-simu ${port} 读取失败:${dispatch.error ?? dispatch.body?.error?.message ?? "unknown"}`, observedAt }); } return { port: { value: readValueFromGatewayResult(dispatch.body), direction, source: port === M3_IO_CHAIN.targetPort ? "gateway-simu/patch-panel" : "gateway-simu", sourceKind: "DEV-LIVE", observedAt: dispatch.body?.result?.state?.updatedAt ?? dispatch.body?.state?.updatedAt ?? observedAt, auditId: dispatch.body?.auditId ?? dispatch.body?.result?.auditId ?? null, evidenceId: dispatch.body?.evidenceId ?? dispatch.body?.result?.evidenceId ?? null, state: dispatch.body?.result?.state ?? dispatch.body?.state ?? null }, error: null }; } function unverifiedPortRead({ port, direction, source, reason, observedAt }) { return { port: { value: null, direction, source, sourceKind: "UNVERIFIED", observedAt, lastError: reason }, error: reason }; } function gatewayStatusEntry(gateway, { id, sessionId, role, observedAt }) { return { id: gateway.gatewayId ?? id, role, online: gateway.available === true, observable: gateway.available === true, sessionId: gateway.gatewaySessionId ?? sessionId, lastSeenAt: gateway.status?.lastHeartbeatAt ?? gateway.status?.updatedAt ?? gateway.status?.session?.lastSeenAt ?? observedAt, sourceKind: gateway.available === true ? "DEV-LIVE" : "UNVERIFIED", lastError: gateway.available === true ? null : gateway.reason, blocker: gateway.available === true ? null : gateway.code ?? M3_IO_BLOCKER_CODES.gatewayUnavailable }; } function boxStatusEntry({ gateway, box, boxId, resourceId, gatewayId, gatewaySessionId, observedAt, ports }) { return { id: box?.boxId ?? boxId, resourceId: box?.resourceId ?? resourceId, gatewayId: gateway.gatewayId ?? gatewayId, gatewaySessionId: gateway.gatewaySessionId ?? gatewaySessionId, online: gateway.available === true && Boolean(box), observable: gateway.available === true && Boolean(box), sourceKind: gateway.available === true && box ? "DEV-LIVE" : "UNVERIFIED", observedAt, ports }; } function patchPanelStatusEntry({ statusResponse, wiringResponse, observedAt }) { const status = statusResponse.body ?? {}; const wiring = wiringResponse.body ?? {}; const connectionActive = statusResponse.ok && wiringResponse.ok && hasExpectedPatchPanelWiring(status) && hasExpectedPatchPanelWiring(wiring); const lastError = !statusResponse.ok ? `hwlab-patch-panel status 不可读:${statusResponse.error ?? `HTTP ${statusResponse.status}`}` : !wiringResponse.ok ? `hwlab-patch-panel wiring 不可读:${wiringResponse.error ?? `HTTP ${wiringResponse.status}`}` : connectionActive ? status.metadata?.evidenceSummary?.diagnostics?.errorCount > 0 ? "hwlab-patch-panel diagnostics reports errors" : null : `未确认 active ${M3_IO_CHAIN.sourceResourceId}:${M3_IO_CHAIN.sourcePort} -> ${M3_IO_CHAIN.targetResourceId}:${M3_IO_CHAIN.targetPort}`; return { serviceId: status.serviceId ?? M3_IO_CHAIN.patchPanelServiceId, observable: statusResponse.ok, connectionActive, connectionVersion: status.metadata?.evidenceSummary?.tickCount ?? status.metadata?.evidenceSummary?.routeCount ?? null, wiringConfigId: status.wiringConfigId ?? wiring.wiringConfigId ?? null, lastSyncAt: status.metadata?.evidenceSummary?.lastSyncAt ?? status.observedAt ?? wiring.updatedAt ?? observedAt, lastError, sourceKind: connectionActive ? "DEV-LIVE" : statusResponse.ok ? "UNVERIFIED" : "BLOCKED", activeConnections: status.activeConnections ?? [], diagnostics: status.metadata?.diagnostics ?? status.metadata?.evidenceSummary?.diagnostics ?? null }; } function readinessCheckFromGateway(id, gateway) { return { id, status: gateway.available ? "pass" : "blocked", layer: gateway.role, sourceKind: gateway.available ? "DEV-LIVE" : "BLOCKED", summary: gateway.available ? `${gateway.role} gateway ${gateway.gatewayId}/${gateway.gatewaySessionId} 只读状态可用。` : gateway.reason, gatewayId: gateway.gatewayId ?? null, gatewaySessionId: gateway.gatewaySessionId ?? null, url: gateway.url ? redactUrl(gateway.url) : null, blocker: gateway.available ? null : gateway.code ?? M3_IO_BLOCKER_CODES.gatewayUnavailable }; } function readinessCheckFromBox(id, box, command) { return { id, status: box ? "pass" : "blocked", sourceKind: box ? "DEV-LIVE" : "BLOCKED", summary: box ? `${command.resourceId}/${command.port} 已在 gateway registry 中可见。` : `${command.resourceId}/${command.port} 未在 gateway registry 中可见。`, resourceId: box?.resourceId ?? command.resourceId, boxId: box?.boxId ?? command.boxId, port: command.port, blocker: box ? null : M3_IO_BLOCKER_CODES.boxUnavailable }; } function readinessCheckFromPatchPanelStatus(response) { const expected = response.ok && hasExpectedPatchPanelWiring(response.body); return { id: "patch-panel-status", status: expected ? "pass" : "blocked", sourceKind: expected ? "DEV-LIVE" : "BLOCKED", summary: expected ? "hwlab-patch-panel status 包含目标 active 接线。" : response.ok ? "hwlab-patch-panel status 未包含目标 active 接线。" : `hwlab-patch-panel status 不可读:${response.error ?? `HTTP ${response.status}`}`, serviceId: response.body?.serviceId ?? null, state: response.body?.state ?? null, blocker: expected ? null : M3_IO_BLOCKER_CODES.wiringMissing }; } function readinessCheckFromPatchPanelWiring(response) { const expected = response.ok && hasExpectedPatchPanelWiring(response.body); return { id: "patch-panel-wiring", status: expected ? "pass" : "blocked", sourceKind: expected ? "DEV-LIVE" : "BLOCKED", summary: expected ? "hwlab-patch-panel /wiring 包含目标 active 接线。" : response.ok ? "hwlab-patch-panel /wiring 未包含目标 active 接线。" : `hwlab-patch-panel /wiring 不可读:${response.error ?? `HTTP ${response.status}`}`, wiringConfigId: response.body?.wiringConfigId ?? null, wiringStatus: response.body?.status ?? null, blocker: expected ? null : M3_IO_BLOCKER_CODES.wiringMissing }; } 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 (typeof command.value !== "boolean") { return "M3 DO 写入值必须是 boolean。"; } } 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" }; } 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 hasExpectedPatchPanelWiring(body) { const connections = [ ...(Array.isArray(body?.activeConnections) ? body.activeConnections : []), ...(Array.isArray(body?.connections) ? body.connections : []), ...(Array.isArray(body?.wiringConfig?.connections) ? body.wiringConfig.connections : []) ]; const active = body?.state === undefined || !["faulted", "blocked", "disabled"].includes(String(body.state).toLowerCase()); const wiringActive = body?.status === undefined || String(body.status).toLowerCase() === "active"; return active && wiringActive && connections.some((connection) => { const from = connection.from ?? connection; const to = connection.to ?? connection; return String(from.resourceId ?? connection.fromResourceId ?? "") === M3_IO_CHAIN.sourceResourceId && normalizePort(from.port ?? connection.fromPort) === M3_IO_CHAIN.sourcePort && String(to.resourceId ?? connection.toResourceId ?? "") === M3_IO_CHAIN.targetResourceId && normalizePort(to.port ?? connection.toPort) === M3_IO_CHAIN.targetPort; }); } function firstDiagnostic(body) { return [ ...(Array.isArray(body?.diagnostics) ? body.diagnostics : []), ...(Array.isArray(body?.routes) ? body.routes.flatMap((route) => route.diagnostics ?? []) : []) ][0] ?? null; }