Files

1738 lines
56 KiB
TypeScript
Raw Permalink 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";
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 已 greenoperation/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;
}