Files
pikasTech-HWLAB/web/hwlab-cloud-web/app.mjs
T
Lyon f3ae03202e feat: enrich cloud workbench hardware status
feat: enrich cloud workbench hardware status
2026-05-22 23:33:02 +08:00

1450 lines
50 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { gateSummary } from "./gate-summary.mjs";
import { runtime } from "./runtime.mjs";
import { marked } from "./third_party/marked/marked.esm.js";
const API_TIMEOUT_MS = 4500;
const rpcReadMethods = Object.freeze([
"system.health",
"cloud.adapter.describe",
"audit.event.query",
"evidence.record.query"
]);
const STATUS_LABELS = Object.freeze({
active: "活动",
available: "可用",
blocked: "阻塞 BLOCKED",
completed: "完成",
connected: "已连接",
degraded: "降级",
disabled: "禁用",
entry: "入口",
failed: "失败",
healthy: "健康",
live: "实况",
ok: "正常",
open: "未关闭",
pass: "通过",
pending: "等待",
running: "处理中",
probing: "探测中",
ready: "就绪",
recorded: "已记录",
sent: "已发送",
requires: "依赖",
source: "来源 SOURCE",
"dry-run": "演练 DRY-RUN",
"dev-live": "开发实况 DEV-LIVE"
});
const M3_TRUSTED_ROUTE = Object.freeze({
fromResourceId: "res_boxsimu_1",
fromPort: "DO1",
patchPanelServiceId: "hwlab-patch-panel",
toResourceId: "res_boxsimu_2",
toPort: "DI1"
});
const LIVE_M3_ID_FIELDS = Object.freeze(["operationId", "traceId", "auditId", "evidenceId"]);
const LIVE_M3_PASS_STATUSES = Object.freeze(["pass", "passed", "verified", "succeeded", "trusted", "completed"]);
const NON_LIVE_ID_VALUES = Object.freeze(["", "n/a", "none", "null", "undefined", "not_observed", "not-observed"]);
const viewIds = new Set([...document.querySelectorAll("[data-view]")].map((view) => view.dataset.view));
let rpcSequence = 0;
const el = {
shell: query("[data-app-shell]"),
explorerToggle: byId("explorer-toggle"),
explorerStatus: byId("explorer-status"),
treeCount: byId("tree-count"),
resourceTree: byId("resource-tree"),
capabilityCount: byId("capability-count"),
explorerCapabilities: byId("explorer-capabilities"),
routePath: byId("route-path"),
liveStatus: byId("live-status"),
liveDetail: byId("live-detail"),
agentChatStatus: byId("agent-chat-status"),
conversationList: byId("conversation-list"),
taskList: byId("task-list"),
traceList: byId("trace-list"),
unblockList: byId("unblock-list"),
gateMetadata: byId("gate-metadata"),
milestoneGrid: byId("milestone-grid"),
healthBody: byId("health-body"),
gateBlockers: byId("gate-blockers"),
validationList: byId("validation-list"),
commandForm: byId("command-form"),
commandInput: byId("command-input"),
commandSend: byId("command-send"),
commandClear: byId("command-clear"),
hardwareSourceStatus: byId("hardware-source-status"),
hardwareList: byId("hardware-list"),
controlList: byId("control-list"),
wiringBody: byId("wiring-body"),
recordsList: byId("records-list"),
diagnosticsList: byId("diagnostics-list"),
methodList: byId("method-list"),
helpContent: byId("help-content"),
helpStatus: byId("help-status")
};
const state = {
conversationId: null,
codeAgentAvailability: null,
chatMessages: [],
chatPending: false
};
initRoutes();
initExplorerToggle();
initSideTabs();
initQuickActions();
initCommandBar();
renderStaticWorkbench();
renderProbePending();
loadHelpSurface();
loadLiveSurface().then(renderLiveSurface);
function byId(id) {
const element = document.getElementById(id);
if (!element) {
throw new Error(`missing required element #${id}`);
}
return element;
}
function query(selector) {
const element = document.querySelector(selector);
if (!element) {
throw new Error(`missing required element ${selector}`);
}
return element;
}
function initRoutes() {
window.addEventListener("hashchange", () => showView(routeFromLocation()));
for (const button of document.querySelectorAll("[data-route]")) {
button.addEventListener("click", () => {
window.location.hash = button.dataset.route;
});
}
showView(routeFromLocation());
}
function routeFromLocation() {
const hashRoute = window.location.hash.replace(/^#/, "");
if (viewIds.has(hashRoute)) return hashRoute;
if (internalGatePathnames().has(window.location.pathname.replace(/\/+$/, "") || "/")) return "gate";
return "workspace";
}
function showView(route) {
const activeRoute = viewIds.has(route) ? route : "workspace";
for (const view of document.querySelectorAll("[data-view]")) {
view.hidden = view.dataset.view !== activeRoute;
}
for (const button of document.querySelectorAll("[data-route]")) {
const active = button.dataset.route === activeRoute;
button.classList.toggle("active", active);
button.setAttribute("aria-current", active ? "page" : "false");
}
}
function initExplorerToggle() {
el.explorerToggle.addEventListener("click", () => {
const collapsed = !el.shell.classList.contains("explorer-collapsed");
el.shell.classList.toggle("explorer-collapsed", collapsed);
el.explorerToggle.setAttribute("aria-pressed", collapsed ? "true" : "false");
});
}
async function loadHelpSurface() {
el.helpStatus.textContent = "加载中";
el.helpStatus.className = "state-tag tone-dry-run";
el.helpContent.dataset.helpState = "loading";
try {
const response = await fetch("/help.md", { cache: "no-store" });
if (!response.ok) {
throw new Error(`help.md HTTP ${response.status}`);
}
const markdown = await response.text();
el.helpContent.innerHTML = marked.parse(markdown, { gfm: true, breaks: false });
el.helpContent.dataset.helpState = "ready";
el.helpStatus.textContent = "已就绪";
el.helpStatus.className = "state-tag tone-source";
} catch (error) {
el.helpContent.dataset.helpState = "error";
el.helpContent.replaceChildren();
const notice = document.createElement("div");
notice.className = "help-error";
notice.textContent = `帮助内容加载失败:${error.message}。默认工作台仍可继续使用。`;
el.helpContent.append(notice);
el.helpStatus.textContent = "加载失败";
el.helpStatus.className = "state-tag tone-blocked";
}
}
function initSideTabs() {
for (const tab of document.querySelectorAll("[data-side-tab]")) {
tab.addEventListener("click", () => selectSideTab(tab.dataset.sideTab));
}
for (const jump of document.querySelectorAll("[data-side-tab-jump]")) {
jump.addEventListener("click", () => selectSideTab(jump.dataset.sideTabJump));
}
}
function initQuickActions() {
for (const button of document.querySelectorAll("[data-focus-command]")) {
button.addEventListener("click", () => {
el.commandInput.focus();
});
}
}
function selectSideTab(tabId) {
for (const tab of document.querySelectorAll("[data-side-tab]")) {
const active = tab.dataset.sideTab === tabId;
tab.classList.toggle("active", active);
tab.setAttribute("aria-selected", active ? "true" : "false");
}
for (const panel of document.querySelectorAll("[data-side-panel]")) {
const active = panel.dataset.sidePanel === tabId;
panel.classList.toggle("active", active);
panel.hidden = !active;
}
}
function initCommandBar() {
el.commandForm.addEventListener("submit", async (event) => {
event.preventDefault();
const value = el.commandInput.value.trim();
if (!value || state.chatPending) return;
const userMessage = {
id: nextProtocolId("msg"),
role: "user",
title: `用户 ${shortTime(new Date().toISOString())}`,
text: value,
status: "sent",
createdAt: new Date().toISOString()
};
const pendingMessage = {
id: nextProtocolId("msg"),
role: "agent",
title: "Code Agent 处理中",
text: "正在调用真实 Code Agent / Codex 后端,请稍候。",
status: "running",
createdAt: new Date().toISOString()
};
state.chatMessages.push(userMessage, pendingMessage);
state.chatPending = true;
el.commandInput.value = "";
renderAgentChatStatus("running");
renderConversation();
renderDrafts();
try {
const result = await sendAgentMessage(value, state.conversationId);
state.conversationId = result.conversationId || result.sessionId || state.conversationId;
const index = state.chatMessages.findIndex((message) => message.id === pendingMessage.id);
const status = result.status === "completed" ? "completed" : "failed";
state.chatMessages[index] = {
...pendingMessage,
title: result.status === "completed" ? sourceTitle(result) : "Code Agent 返回失败",
text: result.status === "completed"
? result.reply?.content || "Code Agent 没有返回文本。"
: failureMessage(result),
status,
traceId: result.traceId,
messageId: result.messageId,
provider: result.provider,
model: result.model,
backend: result.backend,
updatedAt: result.updatedAt,
error: result.error,
availability: result.availability
};
if (result.availability) {
state.codeAgentAvailability = result.availability;
}
if (result.status !== "completed") {
el.commandInput.value = value;
}
renderAgentChatStatus(status, result);
} catch (error) {
const index = state.chatMessages.findIndex((message) => message.id === pendingMessage.id);
state.chatMessages[index] = {
...pendingMessage,
title: "发送失败",
text: `发送失败:${error.message}`,
status: "failed",
updatedAt: new Date().toISOString(),
error: {
message: error.message
}
};
el.commandInput.value = value;
renderAgentChatStatus("failed");
} finally {
state.chatPending = false;
renderConversation();
renderDrafts();
}
});
el.commandClear.addEventListener("click", () => {
state.chatMessages = [];
state.conversationId = null;
state.chatPending = false;
el.commandInput.value = "";
renderAgentChatStatus("idle");
renderConversation();
renderDrafts();
});
}
function renderStaticWorkbench() {
el.explorerStatus.textContent = "只读工作区";
el.explorerStatus.className = "status-dot tone-source";
el.routePath.textContent = "当前浏览器会话只保存任务草稿,硬件动作需通过受控后端流程。";
renderResourceTree();
renderCapabilityList();
renderConversation();
renderTaskList();
renderTrace();
renderUnblockList();
renderHardwareStatus(null);
renderDrafts();
renderWiringList(null);
renderRecords(null);
renderDiagnostics(null);
renderGateDiagnostics();
}
function renderProbePending() {
el.liveStatus.textContent = "只读模式";
el.liveStatus.className = "tone-source";
el.liveDetail.textContent = "可整理任务、查看资源、核对接线和可信记录;不会触发硬件变更。";
renderMethodList(rpcReadMethods, "pending");
}
async function sendAgentMessage(message, conversationId) {
const traceId = nextProtocolId("trc");
const response = await fetchJson("/v1/agent/chat", {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-Trace-Id": traceId
},
body: JSON.stringify({
message,
conversationId,
traceId,
projectId: gateSummary.topology.projectId
})
});
if (!response.ok) {
throw new Error(response.error || "Code Agent 请求失败");
}
return response.data;
}
async function loadLiveSurface() {
const projectId = gateSummary.topology.projectId;
const [healthLive, restIndex, health, adapter, audit, evidence] = await Promise.all([
fetchJson("/health/live"),
fetchJson("/v1"),
callRpc("system.health"),
callRpc("cloud.adapter.describe"),
callRpc("audit.event.query", { projectId, limit: 6 }),
callRpc("evidence.record.query", { projectId, limit: 6 })
]);
return {
observedAt: new Date().toISOString(),
healthLive,
restIndex,
health,
adapter,
audit,
evidence
};
}
async function fetchJson(path, options = {}) {
const controller = new AbortController();
const timer = window.setTimeout(() => controller.abort(), API_TIMEOUT_MS);
try {
const response = await fetch(path, {
...options,
headers: {
Accept: "application/json",
...(options.headers ?? {})
},
signal: controller.signal
});
const text = await response.text();
const data = text ? JSON.parse(text) : null;
if (!response.ok) {
throw new Error(`${path} HTTP ${response.status}${messageFromPayload(data)}`);
}
return { ok: true, path, status: response.status, data };
} catch (error) {
return {
ok: false,
path,
error: error.name === "AbortError" ? `${path} 请求超时 ${API_TIMEOUT_MS}ms` : `请求失败:${error.message}`
};
} finally {
window.clearTimeout(timer);
}
}
async function callRpc(method, params = {}) {
const id = nextProtocolId("req");
const traceId = nextProtocolId("trc");
const response = await fetchJson("/json-rpc", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
jsonrpc: "2.0",
id,
method,
params,
meta: {
traceId,
serviceId: "hwlab-cloud-web",
environment: "dev"
}
})
});
if (!response.ok) {
return { ...response, method };
}
if (response.data?.error) {
return {
ok: false,
path: response.path,
status: response.status,
method,
error: `RPC 错误 ${method}${response.data.error.message}`,
envelope: response.data
};
}
return {
ok: true,
path: response.path,
status: response.status,
method,
data: response.data?.result,
envelope: response.data
};
}
function nextProtocolId(prefix) {
rpcSequence += 1;
return `${prefix}_cloud-web-workbench-${Date.now()}-${rpcSequence}`;
}
function renderLiveSurface(live) {
const coreProbes = [live.healthLive, live.restIndex, live.health, live.adapter];
const reachable = coreProbes.some((probe) => probe.ok);
const runtimeSummary = runtimeSummaryFrom(live);
const methods = methodsFrom(live);
el.liveStatus.textContent = reachable ? "只读连接可用" : "离线查看";
el.liveStatus.className = `tone-${toneClass(reachable ? "dev-live" : "source")}`;
state.codeAgentAvailability = codeAgentAvailabilityFrom(live);
el.liveDetail.textContent = reachable
? codeAgentAvailabilitySummary()
: "同源 API 暂不可用,当前仍可使用本地草稿和已加载资源。";
renderAgentChatStatus(deriveAgentChatStatus());
renderHardwareStatus(runtimeSummary);
renderWiringList(runtimeSummary);
renderRecords(live);
renderDiagnostics(live);
renderMethodList(methods.length > 0 ? methods.filter((method) => rpcReadMethods.includes(method)) : rpcReadMethods, reachable ? "live" : "degraded");
renderConversation();
renderDrafts();
}
function renderResourceTree() {
const items = [
{
label: gateSummary.topology.projectId,
meta: "当前项目",
tone: "source",
children: [
...gateSummary.topology.gateways.map((gateway) => ({
label: gateway.gatewaySessionId,
meta: `${gateway.serviceId} / ${statusLabel(gateway.status)}`,
tone: gateway.status
})),
...gateSummary.topology.boxResources.map((resource) => ({
label: resource.resourceId,
meta: `${resourceTypeLabel(resource.resourceType)} / ${statusLabel(resource.state)}`,
tone: resource.state
})),
{
label: gateSummary.topology.patchPanel.patchPanelStatusId,
meta: `${gateSummary.topology.patchPanel.serviceId} / ${statusLabel(gateSummary.topology.patchPanel.state)}`,
tone: gateSummary.topology.patchPanel.state
}
]
},
{
label: gateSummary.agent.agentSessionId,
meta: `${gateSummary.agent.agentServiceId} / Agent 会话`,
tone: gateSummary.agent.agentStatus,
children: [
{
label: gateSummary.agent.workerSessionId,
meta: `${gateSummary.agent.workerServiceId} / ${statusLabel(gateSummary.agent.workerStatus)}`,
tone: gateSummary.agent.workerStatus
}
]
}
];
el.treeCount.textContent = String(items.reduce((count, item) => count + 1 + (item.children?.length ?? 0), 0));
replaceChildren(el.resourceTree, ...items.map(treeGroup));
}
function treeGroup(item) {
const details = document.createElement("details");
details.open = true;
const summary = document.createElement("summary");
summary.append(statusLight(item.tone), textSpan(item.label, "tree-label"), textSpan(item.meta, "tree-meta"));
details.append(summary);
for (const child of item.children ?? []) {
const row = document.createElement("button");
row.type = "button";
row.className = "tree-row";
row.append(statusLight(child.tone), textSpan(child.label, "tree-label"), textSpan(child.meta, "tree-meta"));
details.append(row);
}
return details;
}
function renderCapabilityList() {
const capabilities = [
{
title: "任务草稿",
detail: "在底部输入区记录要交给 Agent 的任务,不会直接发送硬件请求。",
tone: "source"
},
{
title: "资源查看",
detail: "查看 Gateway-SIMU、BOX-SIMU 与 hwlab-patch-panel 的只读资源状态。",
tone: "source"
},
{
title: "接线核对",
detail: "在右侧接线表核对源端口、接线盘、目标端口与证据记录。",
tone: "source"
}
];
el.capabilityCount.textContent = String(capabilities.length);
replaceChildren(el.explorerCapabilities, ...capabilities.map(infoCard));
replaceChildren(el.gateBlockers, ...gateSummary.blockers.map(blockerCard));
}
function renderConversation() {
const introMessages = [
{
role: "system",
title: "界面模式",
text: "这里是用户工作台:可以整理任务、查看资源、核对接线和可信记录。对话会发送到受控 Code Agent 后端;页面不会直接发送硬件变更。",
status: "source"
},
codeAgentStatusMessage(state.codeAgentAvailability)
];
replaceChildren(el.conversationList, ...[...introMessages, ...state.chatMessages].map(messageCard));
}
function renderTaskList() {
const rows = [
{
title: "1. 描述目标",
detail: "例如要检查哪个 BOX-SIMU、哪个端口、期望看到什么信号。",
tone: "source"
},
{
title: "2. 核对接线",
detail: "打开右侧“接线”表,确认源设备、接线盘、目标设备和 trace/evidence 是否匹配。",
tone: "source"
},
{
title: "3. 留存记录",
detail: "在“可信记录”面板查看已有证据;新增硬件动作不从前端直接发起。",
tone: "source"
}
];
replaceChildren(el.taskList, ...rows.map(infoCard));
}
function renderTrace() {
replaceChildren(
el.traceList,
...gateSummary.steps.map((step) => {
const article = document.createElement("article");
article.className = "trace-item";
article.append(textSpan(String(step.order).padStart(2, "0"), "trace-order"));
const body = document.createElement("div");
body.append(textSpan(stepTitle(step.title), "trace-title"));
body.append(textSpan(`${stepKindLabel(step.kind)} / ${step.outputs.length} 个输出`, "trace-meta"));
article.append(body, badge(step.requires.length === 0 ? "入口" : "依赖前序", step.requires.length === 0 ? "source" : "dry-run"));
return article;
})
);
}
function renderUnblockList() {
const rows = [
{
title: "DB live readiness",
detail: "先让 cloud-api /health/live 返回 db.ready=true、db.connected=true,再进入 Agent runtime 调度。",
tone: "blocked"
},
{
title: "patch-panel DO1 -> DI1",
detail: "再由 hwlab-patch-panel 建立 res_boxsimu_1:DO1 -> res_boxsimu_2:DI1 的活动链路,并附带可信记录。",
tone: "blocked"
},
{
title: "证据升级规则",
detail: "SOURCE、LOCAL、DRY-RUN 与 fixture 只能作为解释材料,不能升级为 DEV-LIVE 验收证据。",
tone: "dry-run"
}
];
replaceChildren(el.unblockList, ...rows.map(infoCard));
}
function renderHardwareStatus(summary) {
const counts = summary?.counts ?? null;
const patchPanel = gateSummary.topology.patchPanel;
const sourceLink = trustedM3Link(patchPanel.activeConnections);
const liveM3Evidence = trustedM3LiveEvidenceFrom(summary);
const m3Live = hasTrustedM3LiveEvidence(liveM3Evidence);
const runtimeCountsSourceKind = "SOURCE";
const runtimeCountsTone = "source";
const patchTone = m3Live ? "live" : patchPanel.state === "active" ? "source" : patchPanel.state;
const linkTone = m3Live ? "live" : sourceLink || patchPanel.activeConnectionCount > 0 ? "degraded" : "blocked";
el.hardwareSourceStatus.textContent = m3Live ? "DEV-LIVE" : "SOURCE";
el.hardwareSourceStatus.className = `state-tag tone-${toneClass(m3Live ? "dev-live" : "source")}`;
replaceChildren(
el.hardwareList,
hardwareGroup({
title: "Gateway/Box 在线",
rows: [
{
label: "Gateway-SIMU",
value: counts ? `${counts.gatewaySessions ?? 0} 会话` : "离线查看",
detail: counts
? `${counts.gatewaySessions ?? 0} 个 Gateway 会话来自只读 runtime counts;不是 M3 DEV-LIVE 证据。`
: `${gateSummary.gatewaySessionCount} 个 Gateway 会话来自 SOURCE fixture。`,
sourceKind: runtimeCountsSourceKind,
tone: runtimeCountsTone
},
{
label: "BOX-SIMU",
value: counts ? `${counts.boxResources ?? 0} 资源` : "离线查看",
detail: counts
? `${counts.boxResources ?? 0} 个 BOX 资源来自只读 runtime counts;不是 M3 DEV-LIVE 证据。`
: `${gateSummary.boxResourceCount} 个 BOX 资源来自 SOURCE fixture。`,
sourceKind: runtimeCountsSourceKind,
tone: runtimeCountsTone
}
]
}),
hardwareGroup({
title: "BOX-SIMU 端口",
rows: [
{
label: "DO1",
value: m3Live ? "verified" : "not wired",
detail: m3Live ? m3LiveDetail(liveM3Evidence) : "BLOCKED:没有 DO1 电平的 live M3 operation/trace/audit/evidence 证据。",
sourceKind: m3Live ? "DEV-LIVE" : "BLOCKED",
tone: m3Live ? "live" : "blocked"
},
{
label: "DI1",
value: m3Live ? "verified" : "not wired",
detail: m3Live ? m3LiveDetail(liveM3Evidence) : "BLOCKED:没有 DI1 回读的 live M3 operation/trace/audit/evidence 证据。",
sourceKind: m3Live ? "DEV-LIVE" : "BLOCKED",
tone: m3Live ? "live" : "blocked"
},
{
label: "AI/AO/FREQ",
value: "future capability",
detail: "当前未声明模拟量/频率闭环,按 future capability 处理。",
sourceKind: "SOURCE",
tone: "disabled"
}
]
}),
hardwareGroup({
title: "Patch Panel 连线",
rows: [
{
label: "hwlab-patch-panel",
value: statusLabel(patchPanel.state),
detail: m3Live
? `DEV-LIVE patch-panel live report ${liveM3Evidence.patchPanelLiveReport.reportId ?? liveM3Evidence.patchPanelLiveReport.patchPanelStatusId} 已绑定 M3 route。`
: `${patchPanel.activeConnectionCount} 条 SOURCE fixture 接线;工作台只展示 required M3 route,未升级为 DEV-LIVE。`,
sourceKind: m3Live ? "DEV-LIVE" : "SOURCE",
tone: patchTone
},
{
label: "DO1 -> patch-panel -> DI1",
value: linkStatusLabel(linkTone),
detail: m3LinkDetail({ liveM3Evidence, sourceLink, patchPanel, counts }),
sourceKind: m3Live ? "DEV-LIVE" : "BLOCKED",
tone: linkTone
}
]
}),
hardwareGroup({
title: "写入边界",
rows: [
{
label: "硬件控制",
value: "已禁用",
detail: "Web 不提供硬件写 RPC;不会新增伪硬件写操作。",
sourceKind: "SOURCE",
tone: "source"
}
]
})
);
}
function hardwareGroup(group) {
const article = document.createElement("article");
article.className = "hardware-section";
article.append(textSpan(group.title, "hardware-section-title"));
const rows = document.createElement("div");
rows.className = "hardware-rows";
rows.append(...group.rows.map(hardwareRow));
article.append(rows);
return article;
}
function hardwareRow(item) {
const row = document.createElement("div");
row.className = `hardware-row tone-border-${toneClass(item.tone)}`;
row.dataset.sourceKind = item.sourceKind;
const head = document.createElement("div");
head.className = "hardware-row-head";
head.append(textSpan(item.label, "hardware-row-label"), badge(item.sourceKind, item.sourceKind));
row.append(head);
row.append(textSpan(item.value, `hardware-row-value tone-${toneClass(item.tone)}`));
row.append(textSpan(item.detail, "hardware-row-detail"));
return row;
}
function trustedM3Link(connections) {
return (connections ?? []).find(
(link) =>
String(link.fromResourceId ?? "") === M3_TRUSTED_ROUTE.fromResourceId &&
normalizePort(link.fromPort) === "DO1" &&
String(link.toResourceId ?? "") === M3_TRUSTED_ROUTE.toResourceId &&
normalizePort(link.toPort) === "DI1" &&
connectionPatchPanelServiceId(link) === M3_TRUSTED_ROUTE.patchPanelServiceId
);
}
function m3LinkDetail({ liveM3Evidence, sourceLink, patchPanel, counts }) {
if (hasTrustedM3LiveEvidence(liveM3Evidence)) {
return `DEV-LIVE M3${M3_TRUSTED_ROUTE.fromResourceId}:${M3_TRUSTED_ROUTE.fromPort} -> ${M3_TRUSTED_ROUTE.patchPanelServiceId} -> ${M3_TRUSTED_ROUTE.toResourceId}:${M3_TRUSTED_ROUTE.toPort}${m3LiveDetail(liveM3Evidence)}。`;
}
if (sourceLink) {
return `BLOCKEDSOURCE fixture 包含目标接线 ${sourceLink.fromResourceId}:${sourceLink.fromPort} -> ${patchPanel.serviceId} -> ${sourceLink.toResourceId}:${sourceLink.toPort},但缺少 patch-panel live report 与 operation/trace/audit/evidence IDs。`;
}
if (counts) {
return "BLOCKEDruntime counts 已返回,但 counts 不是 patch-panel live report,不能证明 M3 DEV-LIVE。";
}
if (patchPanel.activeConnectionCount > 0) {
return `SOURCE 仅看到非 M3 patch-panel fixture 接线;required M3 route ${M3_TRUSTED_ROUTE.fromResourceId}:${M3_TRUSTED_ROUTE.fromPort} -> ${M3_TRUSTED_ROUTE.patchPanelServiceId} -> ${M3_TRUSTED_ROUTE.toResourceId}:${M3_TRUSTED_ROUTE.toPort} 仍 BLOCKED,未冒充 live。`;
}
return "BLOCKED:当前没有 DO1 -> patch-panel -> DI1 的可信链路证据。";
}
function linkStatusLabel(status) {
return (
{
live: "live",
degraded: "degraded",
blocked: "blocked"
}[status] ?? statusLabel(status)
);
}
function normalizePort(port) {
return String(port ?? "").trim().toUpperCase();
}
function trustedM3LiveEvidenceFrom(summary) {
const candidates = [
summary?.m3TrustedLoop,
summary?.m3HardwareLoop,
summary?.hardware?.m3TrustedLoop,
summary?.patchPanel?.m3TrustedLoop,
summary?.liveM3Evidence
].filter(Boolean);
return candidates.find(hasTrustedM3LiveEvidence) ?? null;
}
function hasTrustedM3LiveEvidence(candidate) {
if (!candidate || typeof candidate !== "object") return false;
if (candidate.dryRun === true || candidate.fixture === true || candidate.sourceKind === "SOURCE" || candidate.sourceKind === "DRY-RUN") return false;
if (!hasLiveSourceKind(candidate)) return false;
if (!LIVE_M3_PASS_STATUSES.includes(String(candidate.status ?? candidate.outcome ?? "verified").toLowerCase())) return false;
if (!LIVE_M3_ID_FIELDS.every((field) => isLiveIdentifier(candidate[field] ?? candidate.operation?.[field] ?? candidate.identifiers?.[field]))) return false;
const patchPanelLiveReport = candidate.patchPanelLiveReport ?? candidate.patchPanelReport;
return hasTrustedPatchPanelLiveReport(patchPanelLiveReport);
}
function hasTrustedPatchPanelLiveReport(report) {
if (!report || typeof report !== "object") return false;
if (report.dryRun === true || report.fixture === true || report.sourceKind === "SOURCE" || report.sourceKind === "DRY-RUN") return false;
if (!hasLiveSourceKind(report)) return false;
if (report.serviceId !== M3_TRUSTED_ROUTE.patchPanelServiceId) return false;
if (!isLiveIdentifier(report.reportId ?? report.patchPanelStatusId ?? report.statusId ?? report.evidenceId)) return false;
return (report.activeConnections ?? report.connections ?? []).some((link) => trustedM3Link([link]));
}
function hasLiveSourceKind(value) {
return value.live === true || value.sourceKind === "DEV-LIVE" || value.evidenceLevel === "DEV-LIVE";
}
function isLiveIdentifier(value) {
const text = String(value ?? "").trim();
return text !== "" && !NON_LIVE_ID_VALUES.includes(text.toLowerCase());
}
function connectionPatchPanelServiceId(link) {
return String(link.patchPanelServiceId ?? link.patchPanel?.serviceId ?? link.serviceId ?? M3_TRUSTED_ROUTE.patchPanelServiceId);
}
function m3LiveDetail(evidence) {
return `operation=${evidence.operationId} / trace=${evidence.traceId} / audit=${evidence.auditId} / evidence=${evidence.evidenceId}`;
}
function controlRows() {
return [
{
title: "硬件操作",
detail: "此 Web 工作台不开放硬件写入;需要硬件动作时交由受控后端流程处理。",
tone: "source"
},
{
title: "Code Agent 对话",
detail: codeAgentControlSummary(state.codeAgentAvailability),
tone: currentConversationTone()
},
{
title: "只读探测",
detail: "技术复核探测仍保持 same-origin read-only,并放在二级页面。",
tone: "source"
}
];
}
function renderDrafts() {
const rows = [
...state.chatMessages.slice(-3).reverse().map((message) => ({
title: `${roleLabel(message.role)} ${statusLabel(message.status)}`,
detail: message.text,
tone: message.status || "source"
})),
...(state.chatMessages.length === 0
? [
{
title: "对话状态",
detail: codeAgentPromptText(state.codeAgentAvailability),
tone: state.codeAgentAvailability?.status === "blocked" ? "blocked" : "source"
}
]
: []),
...controlRows()
];
replaceChildren(el.controlList, ...rows.map(infoCard));
}
function renderWiringList(summary = null) {
const patchPanel = gateSummary.topology.patchPanel;
const liveM3Evidence = trustedM3LiveEvidenceFrom(summary);
const m3Live = hasTrustedM3LiveEvidence(liveM3Evidence);
const traceEvidence = m3Live
? [liveM3Evidence.operationId, liveM3Evidence.traceId, liveM3Evidence.auditId, liveM3Evidence.evidenceId].join(" / ")
: "m3-route-required / waiting-for-dev-live";
const tr = document.createElement("tr");
tr.append(cell(M3_TRUSTED_ROUTE.fromResourceId, "mono wrap"));
tr.append(cell(M3_TRUSTED_ROUTE.fromPort, "mono"));
tr.append(cell(M3_TRUSTED_ROUTE.patchPanelServiceId, "mono wrap"));
tr.append(cell(M3_TRUSTED_ROUTE.toResourceId, "mono wrap"));
tr.append(cell(M3_TRUSTED_ROUTE.toPort, "mono"));
const status = document.createElement("td");
status.append(badge(m3Live ? "开发实况 DEV-LIVE" : "阻塞 BLOCKED", m3Live ? "dev-live" : "blocked"));
tr.append(status);
tr.append(cell(m3Live ? "DEV-LIVE" : "SOURCE / required M3 route", "wrap"));
tr.append(cell(traceEvidence, "mono wrap"));
replaceChildren(el.wiringBody, tr);
}
function renderRecords(live) {
const sourceCards = gateSummary.evidenceRecords.map((record) =>
infoCard({
title: record.evidenceId,
detail: `${recordKindLabel(record.kind)} / ${record.operationId} / ${record.dryRun ? "演练 DRY-RUN" : "未知来源"}`,
tone: record.dryRun ? "dry-run" : "source"
})
);
const liveRecords = live?.evidence.ok ? live.evidence.data?.records ?? [] : [];
const liveCards = liveRecords.map((record) => {
const m3Trusted = hasTrustedM3LiveEvidence(record);
return infoCard({
title: record.evidenceId,
detail: [
m3Trusted ? "开发实况 DEV-LIVE M3" : "阻塞 BLOCKED:只读 evidence 查询不是 M3 trusted loop",
recordKindLabel(record.kind ?? "record"),
record.operationId ?? "n/a",
record.serviceId ?? "未知服务"
].join(" / "),
tone: m3Trusted ? "dev-live" : "blocked"
});
});
if (live && !live.evidence.ok) {
liveCards.push(infoCard({ title: "evidence.record.query", detail: live.evidence.error, tone: "blocked" }));
}
replaceChildren(el.recordsList, ...liveCards, ...sourceCards);
}
function renderDiagnostics(live) {
const diagnostics = [
{
title: "前端",
detail: runtime.endpoints.frontend,
tone: "source"
},
{
title: "API 边界",
detail: runtime.endpoints.api,
tone: "source"
},
{
title: "运行路由",
detail: runtime.serviceRoute.join(" -> "),
tone: "source"
}
];
if (live) {
diagnostics.push(
{
title: "/health/live",
detail: live.healthLive.ok ? `HTTP ${live.healthLive.status} ${live.healthLive.data?.status ?? "ok"}` : live.healthLive.error,
tone: live.healthLive.ok ? live.healthLive.data?.status ?? "dev-live" : "blocked"
},
{
title: "/v1",
detail: live.restIndex.ok ? `HTTP ${live.restIndex.status} ${live.restIndex.data?.status ?? "ok"}` : live.restIndex.error,
tone: live.restIndex.ok ? live.restIndex.data?.status ?? "dev-live" : "blocked"
},
{
title: "system.health",
detail: live.health.ok ? `RPC ${live.health.data?.status ?? "ok"}` : live.health.error,
tone: live.health.ok ? live.health.data?.status ?? "dev-live" : "blocked"
},
{
title: "audit.event.query",
detail: live.audit.ok ? `${live.audit.data?.count ?? live.audit.data?.events?.length ?? 0} 条记录` : live.audit.error,
tone: live.audit.ok ? "dev-live" : "blocked"
}
);
} else {
diagnostics.push(
{
title: "/health/live",
detail: "正在探测同源 Web/API 健康状态",
tone: "dev-live"
},
{
title: "/v1 + /json-rpc",
detail: "只读诊断等待中",
tone: "source"
}
);
}
replaceChildren(el.diagnosticsList, ...diagnostics.map(infoCard));
}
function renderGateDiagnostics() {
appendMetricGrid(el.gateMetadata, [
["环境", gateSummary.environment, "source"],
["前端", runtime.endpoints.frontend, "source"],
["API", runtime.endpoints.api, "source"],
["Gate", statusLabel(gateSummary.gateStatus), gateSummary.gateStatus],
["演练 DRY-RUN", statusLabel(gateSummary.dryRun.status), "dry-run"],
["报告", shortHash(gateSummary.reportCommitId), "source"]
]);
replaceChildren(
el.milestoneGrid,
...gateSummary.milestones.map((milestone) => {
const item = document.createElement("article");
item.className = "milestone-card";
const header = document.createElement("div");
header.className = "panel-title-row";
header.append(textSpan(milestone.id, "milestone-id"), badge(statusLabel(milestone.status), milestone.status));
item.append(header);
item.append(textSpan(userFacingSummary(milestone.summary), "milestone-summary"));
item.append(textSpan(`${milestone.commandCount} 条命令 / ${milestone.evidenceCount} 条证据`, "milestone-meta"));
return item;
})
);
el.healthBody.replaceChildren();
for (const row of gateSummary.health) {
const tr = document.createElement("tr");
tr.append(cell(componentLabel(row.component)));
tr.append(cell(row.serviceId, "mono"));
const status = document.createElement("td");
status.append(badge(statusLabel(row.status), row.status));
tr.append(status);
tr.append(cell(observedByLabel(row.observedBy)));
tr.append(cell(row.endpoint ?? gateSummary.endpoint, "mono wrap"));
el.healthBody.append(tr);
}
replaceChildren(
el.validationList,
...[
"node --check web/hwlab-cloud-web/app.mjs",
"node --check web/hwlab-cloud-web/scripts/check.mjs",
"node web/hwlab-cloud-web/scripts/check.mjs",
"node scripts/l6-cli-web-smoke.mjs",
"git diff --check"
].map((command) => {
const li = document.createElement("li");
li.textContent = command;
return li;
})
);
}
function renderMethodList(methods, tone) {
replaceChildren(el.methodList, ...methods.map((method) => badge(method, tone === "live" ? "dev-live" : tone)));
}
function renderAgentChatStatus(status, result = null) {
const labels = {
idle: "等待输入",
running: "处理中",
completed: "已回复",
failed: "发送失败",
blocked: "BLOCKED 凭证缺口"
};
el.agentChatStatus.textContent = labels[status] ?? statusLabel(status);
el.agentChatStatus.className = `state-tag tone-${toneClass(status === "completed" ? "dev-live" : status === "failed" || status === "blocked" ? "blocked" : "source")}`;
el.commandInput.disabled = status === "running";
el.commandSend.disabled = status === "running";
el.commandSend.textContent = status === "running" ? "发送中" : "发送";
if (result?.conversationId) {
el.agentChatStatus.title = `${result.provider ?? "provider"} / ${result.model ?? "model"} / ${result.backend ?? "backend"}`;
} else {
el.agentChatStatus.removeAttribute("title");
}
}
function sourceTitle(result) {
return `Code Agent 回复 ${shortTime(result.updatedAt ?? new Date().toISOString())}`;
}
function failureMessage(result) {
if (result?.error?.code === "provider_unavailable" || result?.availability?.status === "blocked") {
const availability = result.availability ?? codeAgentAvailabilityFromResult(result);
const missing = Array.isArray(result.error?.missingEnv) && result.error.missingEnv.length > 0
? ` 当前缺口:${result.error.missingEnv.join("、")}。`
: "";
return `${availability.summary}${missing}`;
}
const missing = [
...(result.error?.missingCommands ?? []),
...(result.error?.missingEnv ?? [])
].filter(Boolean);
const suffix = missing.length ? ` 缺口:${missing.join("、")}。` : "";
return `Code Agent 调用失败:${result.error?.message ?? "未知错误"}${suffix}请稍后重试或联系维护者补齐后端 provider。`;
}
function blockerCard(blocker) {
return infoCard({
title: blockerTitle(blocker),
detail: userFacingSummary(blocker.summary),
tone: blocker.status === "open" ? "blocked" : blocker.status
});
}
function messageCard(message) {
const article = document.createElement("article");
article.className = `message-card message-${toneClass(message.role)} status-${toneClass(message.status)}`;
article.append(textSpan(roleLabel(message.role), "message-role"));
article.append(textSpan(message.title, "message-title"));
article.append(textSpan(message.text, "message-copy"));
const meta = [message.provider, message.model, message.backend, message.traceId].filter(Boolean).join(" / ");
const messageMeta = [message.messageId, meta].filter(Boolean).join(" / ");
if (messageMeta) {
article.append(textSpan(messageMeta, "message-meta"));
}
return article;
}
function statusCard(item) {
const article = document.createElement("article");
article.className = "status-card";
article.append(textSpan(item.title, "metric-label"));
article.append(textSpan(String(item.value), `status-value tone-${toneClass(item.tone)}`));
article.append(textSpan(item.meta, "status-meta"));
return article;
}
function infoCard(item) {
const article = document.createElement("article");
article.className = "info-card";
article.append(badge(statusLabel(item.tone), item.tone));
const body = document.createElement("div");
body.append(textSpan(item.title, "info-title"));
body.append(textSpan(item.detail, "info-detail"));
article.append(body);
return article;
}
function statusLight(tone) {
const span = document.createElement("span");
span.className = `tree-light tone-bg-${toneClass(tone)}`;
return span;
}
function badge(text, tone = text) {
const span = document.createElement("span");
span.className = `badge tone-${toneClass(tone)}`;
span.textContent = text ?? "无";
return span;
}
function appendMetricGrid(parent, entries) {
replaceChildren(
parent,
...entries.map(([label, value, tone]) => {
const item = document.createElement("div");
item.className = "metric";
item.append(textSpan(label, "metric-label"), textSpan(String(value ?? "无"), `metric-value tone-${toneClass(tone ?? "source")}`));
return item;
})
);
}
function textSpan(text, className) {
const span = document.createElement("span");
span.textContent = text ?? "无";
if (className) span.className = className;
return span;
}
function cell(text, className) {
const td = document.createElement("td");
td.textContent = text ?? "无";
if (className) td.className = className;
return td;
}
function runtimeSummaryFrom(live) {
return live.health.data?.runtime ?? live.healthLive.data?.runtime ?? live.adapter.data?.runtime ?? live.restIndex.data?.runtime ?? null;
}
function methodsFrom(live) {
const restMethods = live.restIndex.data?.methods;
const adapterMethods = live.adapter.data?.methods;
if (Array.isArray(restMethods)) return restMethods;
if (Array.isArray(adapterMethods)) return adapterMethods;
return [];
}
function messageFromPayload(payload) {
if (!payload || typeof payload !== "object") return "非 JSON 或空响应";
return payload.error?.message || payload.error?.code || payload.message || JSON.stringify(payload).slice(0, 160);
}
function codeAgentAvailabilityFrom(live) {
return live?.restIndex?.data?.codeAgent ?? live?.healthLive?.data?.codeAgent ?? null;
}
function codeAgentAvailabilityFromResult(result) {
return {
status: "blocked",
blocker: "凭证缺口",
reason: result?.error?.code ?? "provider_unavailable",
summary: "真实后端已接入,但当前 DEV provider Secret hwlab-code-agent-provider/openai-api-key 未注入,因此不能返回真实回复。",
missingEnv: result?.error?.missingEnv ?? [],
secretRefs: [
{
env: "OPENAI_API_KEY",
secretName: "hwlab-code-agent-provider",
secretKey: "openai-api-key",
redacted: true
}
]
};
}
function codeAgentStatusMessage(availability) {
if (availability?.status === "blocked") {
return {
role: "system",
title: "Code Agent 状态:BLOCKED 凭证缺口",
text: codeAgentBlockedSummary(availability),
status: "blocked"
};
}
if (availability?.status === "available") {
return {
role: "system",
title: "Code Agent 状态:真实通道已接入",
text: "真实 /v1/agent/chat 后端已接入;发送后只有实际 completed 回复才会标记为 DEV-LIVE。",
status: "source"
};
}
return {
role: "system",
title: "Code Agent 状态:等待只读探测",
text: "正在读取 same-origin /v1 与 /health/live;首屏不会宣称 Code Agent 可用,也不会把失败或静态内容标成 DEV-LIVE。",
status: "source"
};
}
function codeAgentPromptText(availability) {
if (availability?.status === "blocked") {
return "可以继续输入并保留草稿;发送会走真实 /v1/agent/chat,但当前 DEV provider 凭证缺口会返回结构化失败,不会冒充真实 Codex 回复。";
}
return "请在底部输入中文消息并点击发送;完成态只来自真实 /v1/agent/chat completed 回复。";
}
function codeAgentControlSummary(availability) {
if (latestCompletedAgentMessage()) {
return state.conversationId
? `当前会话 ${state.conversationId};最近一次真实 completed 回复来自 cloud-api 受控 Code Agent 接口。`
: "最近一次真实 completed 回复来自 cloud-api 受控 Code Agent 接口。";
}
if (availability?.status === "blocked") {
return codeAgentBlockedSummary(availability);
}
return "输入区会调用 cloud-api 受控 Code Agent 接口;只有真实 completed 回复才标 DEV-LIVE,不能因为只有 conversationId 标为开发实况。";
}
function codeAgentBlockedSummary(availability) {
return availability?.summary ?? "真实后端已接入,但当前 DEV provider Secret hwlab-code-agent-provider/openai-api-key 未注入,因此不能返回真实回复。";
}
function codeAgentAvailabilitySummary() {
if (state.codeAgentAvailability?.status === "blocked") {
return "同源 API 可响应;Code Agent 为 BLOCKED/凭证缺口,真实后端已接入但 DEV provider Secret hwlab-code-agent-provider/openai-api-key 未注入。";
}
if (state.codeAgentAvailability?.status === "available") {
return "同源 API 可响应;Code Agent 真实通道已接入,只有 completed 回复才标 DEV-LIVE。";
}
return "同源只读接口可响应。技术复核详情已放在二级页面。";
}
function latestCompletedAgentMessage() {
return [...state.chatMessages].reverse().find((message) =>
message.role === "agent" &&
message.status === "completed" &&
!message.error &&
Boolean(message.provider) &&
Boolean(message.model) &&
Boolean(message.backend)
) ?? null;
}
function latestChatResult() {
return [...state.chatMessages].reverse().find((message) => message.role === "agent") ?? null;
}
function deriveAgentChatStatus() {
if (state.chatPending) return "running";
const latest = latestChatResult();
if (latest?.status === "completed") return "completed";
if (latest?.status === "failed") return latest.error?.code === "provider_unavailable" ? "blocked" : "failed";
if (state.codeAgentAvailability?.status === "blocked") return "blocked";
return "idle";
}
function currentConversationTone() {
if (latestCompletedAgentMessage()) return "dev-live";
if (state.codeAgentAvailability?.status === "blocked" || latestChatResult()?.status === "failed") return "blocked";
return "source";
}
function statusLabel(value) {
const key = String(value ?? "unknown").trim();
const normalized = key.toLowerCase();
return STATUS_LABELS[normalized] ?? key;
}
function roleLabel(role) {
return (
{
agent: "Agent",
blocker: "阻塞",
system: "系统",
user: "用户"
}[String(role ?? "").toLowerCase()] ?? statusLabel(role)
);
}
function resourceTypeLabel(type) {
return (
{
board: "板卡",
relay: "继电器"
}[String(type ?? "").toLowerCase()] ?? statusLabel(type)
);
}
function stepKindLabel(kind) {
return (
{
agent: "Agent 会话",
audit: "审计",
cleanup: "清理",
evidence: "证据",
hardware_operation: "硬件操作",
health: "健康检查",
status: "状态检查",
wiring: "接线"
}[String(kind ?? "").toLowerCase()] ?? statusLabel(kind)
);
}
function stepTitle(title) {
return (
{
audit: "审计",
cleanup: "清理",
"agent hardware call": "Agent 硬件调用",
"agent session": "Agent 会话",
"DEV health": "DEV 健康",
"direct hardware call": "直接硬件调用",
evidence: "证据",
"gateway/box status": "Gateway/Box 状态",
wiring: "接线"
}[String(title ?? "")] ?? statusLabel(title)
);
}
function recordKindLabel(kind) {
return (
{
audit: "审计",
evidence: "证据",
operation: "操作",
record: "记录",
report: "报告",
trace: "轨迹"
}[String(kind ?? "").toLowerCase()] ?? statusLabel(kind)
);
}
function componentLabel(component) {
return (
{
"Agent manager": "Agent 管理器",
"Agent skills": "Agent 技能",
"Agent worker": "Agent Worker",
"Box simulator": "Box 模拟器",
CLI: "CLI",
"Cloud API": "Cloud API",
"Cloud Web": "Cloud Web",
"D601 router": "D601 路由器",
"DEV ingress": "DEV 入口",
Gateway: "Gateway",
"Gateway simulator": "Gateway 模拟器",
"Patch panel": "Patch Panel",
frp: "frp",
"master edge proxy": "master 边界代理"
}[String(component ?? "")] ?? statusLabel(component)
);
}
function observedByLabel(value) {
return (
{
fixture: "fixture 来源 SOURCE"
}[String(value ?? "").toLowerCase()] ?? statusLabel(value)
);
}
function blockerTitle(blocker) {
const scope = blocker?.scope ?? "unknown";
return `阻塞:${scope}`;
}
function internalGatePathnames() {
return new Set(["/gate", "/diagnostics/gate"]);
}
function userFacingSummary(text) {
return String(text ?? "")
.replaceAll("Public DEV route/frp", "公共 DEV 路由/frp")
.replaceAll("live MVP remains blocked", "实况 MVP 仍被阻塞")
.replaceAll("DB live readiness", "DB 实况就绪")
.replaceAll("M3 hardware loop topology", "M3 硬件闭环拓扑")
.replaceAll("no historical 6667 failure is counted as active evidence", "历史 6667 失败不计为当前有效证据")
.replaceAll("cloud-api /health/live reports DB degraded", "cloud-api /health/live 报告 DB 降级")
.replaceAll("live DB evidence is not established", "尚未建立实况 DB 证据")
.replaceAll("Live M3 smoke reached DEV simulators", "实况 M3 smoke 已触达 DEV 模拟器")
.replaceAll("patch-panel active DO1 -> DI1 connection is missing", "缺少 patch-panel 上的活动 DO1 -> DI1 连接")
.replaceAll("Frozen contract, artifact, boundary, drift, and evidence fixtures are parseable and aligned to the DEV boundary.", "冻结的契约、产物、边界、漂移与证据 fixture 可解析,并已对齐 DEV 边界。")
.replaceAll("The local M1 contract smoke passes.", "本地 M1 契约 smoke 已通过。")
.replaceAll("The deploy smoke remains fixture-only and validates the frozen route phases and artifact metadata.", "部署 smoke 仍仅使用 fixture,用于验证冻结路由阶段与产物元数据。")
.replaceAll("Local M3 remains green, but active DEV-LIVE M3 is blocked by hardware loop topology.", "本地 M3 仍为通过,但活动 DEV-LIVE M3 受硬件闭环拓扑阻塞。")
.replaceAll("Local M4 remains green, but active DEV-LIVE M4 is blocked at DB live readiness before agent runtime scheduling.", "本地 M4 仍为通过,但活动 DEV-LIVE M4 在 Agent 运行时调度前受 DB 实况就绪阻塞。")
.replaceAll("The M5 orchestration is green in dry-run mode, but active DEV-LIVE acceptance is blocked by DB live and hardware loop evidence.", "M5 编排在 DRY-RUN 模式下通过,但活动 DEV-LIVE 验收受 DB 实况与硬件闭环证据阻塞。");
}
function valueOrUnavailable(liveValue, sourceValue) {
if (liveValue !== undefined && liveValue !== null) return liveValue;
if (sourceValue !== undefined && sourceValue !== null) return `${sourceValue} 来源 SOURCE`;
return "不可用";
}
function shortTime(value) {
return new Date(value).toISOString().replace("T", " ").replace(/\.\d{3}Z$/, "Z");
}
function shortHash(value) {
return String(value ?? "unknown").slice(0, 7);
}
function toneClass(tone) {
return String(tone ?? "unknown").replace(/[^a-z0-9_-]/gi, "-").toLowerCase();
}
function replaceChildren(parent, ...children) {
parent.replaceChildren(...children.filter((child) => child && typeof child !== "string"));
}