Files
pikasTech-HWLAB/web/hwlab-cloud-web/app.mjs
T
2026-05-22 10:51:31 +00:00

982 lines
32 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: "可用",
completed: "完成",
connected: "已连接",
degraded: "降级",
disabled: "禁用",
entry: "入口",
failed: "失败",
healthy: "健康",
live: "实况",
ok: "正常",
open: "未关闭",
pass: "通过",
pending: "等待",
probing: "探测中",
ready: "就绪",
recorded: "已记录",
requires: "依赖",
source: "来源 SOURCE",
"dry-run": "演练 DRY-RUN",
"dev-live": "开发实况 DEV-LIVE",
blocked: "阻塞 BLOCKED"
});
const SOURCE_KIND_LABELS = Object.freeze({
SOURCE: "来源 SOURCE",
"DRY-RUN": "演练 DRY-RUN",
"DEV-LIVE": "开发实况 DEV-LIVE",
BLOCKED: "阻塞 BLOCKED"
});
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"),
sourceStrip: byId("source-strip"),
treeCount: byId("tree-count"),
resourceTree: byId("resource-tree"),
blockerCount: byId("blocker-count"),
explorerBlockers: byId("explorer-blockers"),
routePath: byId("route-path"),
liveStatus: byId("live-status"),
liveDetail: byId("live-detail"),
conversationList: byId("conversation-list"),
traceList: byId("trace-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"),
commandClear: byId("command-clear"),
hardwareList: byId("hardware-list"),
controlList: byId("control-list"),
wiringList: byId("wiring-list"),
recordsList: byId("records-list"),
diagnosticsList: byId("diagnostics-list"),
methodList: byId("method-list"),
helpContent: byId("help-content"),
helpStatus: byId("help-status")
};
const state = {
drafts: []
};
initRoutes();
initExplorerToggle();
initSideTabs();
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 (window.location.pathname.replace(/\/+$/, "").endsWith("/gate")) 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 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", (event) => {
event.preventDefault();
const value = el.commandInput.value.trim();
if (!value) return;
state.drafts.unshift({
text: value,
createdAt: new Date().toISOString()
});
el.commandInput.value = "";
renderDrafts();
});
el.commandClear.addEventListener("click", () => {
state.drafts = [];
renderDrafts();
});
}
function renderStaticWorkbench() {
el.explorerStatus.textContent = statusLabel(gateSummary.blocked ? "blocked" : gateSummary.gateStatus);
el.explorerStatus.className = `status-dot tone-${toneClass(gateSummary.blocked ? "blocked" : gateSummary.gateStatus)}`;
el.routePath.textContent = runtime.serviceRoute.join(" -> ");
renderStateStrip(false);
renderResourceTree();
renderBlockerLists();
renderConversation();
renderTrace();
renderHardwareStatus(null);
renderDrafts();
renderWiringList();
renderRecords(null);
renderDiagnostics(null);
renderGateDiagnostics();
}
function renderProbePending() {
el.liveStatus.textContent = "探测中";
el.liveStatus.className = "tone-probing";
el.liveDetail.textContent = "正在从当前同源检查 /health/live、/v1 与只读 /json-rpc。";
renderMethodList(rpcReadMethods, "pending");
}
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 healthStatus = live.health.data?.status ?? live.healthLive.data?.status ?? "degraded";
const runtimeSummary = runtimeSummaryFrom(live);
const methods = methodsFrom(live);
const dbReady = dbReadyFrom(live);
const devLiveReady = reachable && healthStatus === "ok" && dbReady === true && !gateSummary.blocked;
const blocked = gateSummary.blocked || !devLiveReady;
el.liveStatus.textContent = devLiveReady ? "开发实况 DEV-LIVE 就绪" : reachable ? "阻塞 BLOCKED" : "降级 degraded";
el.liveStatus.className = `tone-${toneClass(devLiveReady ? "dev-live" : reachable ? "blocked" : "degraded")}`;
el.liveDetail.textContent = reachable
? `同源只读界面已响应;health=${healthStatus}db.ready=${String(dbReady)}DEV-LIVE=${devLiveReady ? "true" : "false"}。`
: `同源 API 不可用:${probeErrors(coreProbes).join(" | ")}`;
renderStateStrip(devLiveReady);
renderHardwareStatus(runtimeSummary);
renderRecords(live);
renderDiagnostics(live);
renderMethodList(methods.length > 0 ? methods.filter((method) => rpcReadMethods.includes(method)) : rpcReadMethods, reachable ? "live" : "degraded");
if (blocked) {
el.explorerStatus.textContent = "阻塞 BLOCKED";
el.explorerStatus.className = "status-dot tone-blocked";
}
}
function renderStateStrip(devLiveReady) {
replaceChildren(
el.sourceStrip,
stateTag("SOURCE", `报告 ${shortHash(gateSummary.reportCommitId)}`),
stateTag("DRY-RUN", statusLabel(gateSummary.dryRun.status)),
stateTag("DEV-LIVE", devLiveReady ? "就绪" : "未声明"),
stateTag("BLOCKED", gateSummary.blocked ? `${gateSummary.blockers.length} 个未关闭` : "已清除")
);
}
function renderResourceTree() {
const items = [
{
label: gateSummary.topology.projectId,
meta: `${gateSummary.environment} 项目`,
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} / ${statusLabel(gateSummary.agent.agentStatus)}`,
tone: gateSummary.agent.agentStatus,
children: [
{
label: gateSummary.agent.workerSessionId,
meta: `${gateSummary.agent.workerServiceId} / ${statusLabel(gateSummary.agent.workerStatus)}`,
tone: gateSummary.agent.workerStatus
}
]
},
{
label: "诊断",
meta: "Gate 二级入口",
tone: "source",
children: gateSummary.milestones.map((milestone) => ({
label: milestone.id,
meta: statusLabel(milestone.status),
tone: milestone.status
}))
}
];
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 renderBlockerLists() {
el.blockerCount.textContent = String(gateSummary.blockers.length);
replaceChildren(el.explorerBlockers, ...gateSummary.blockers.map(blockerCard));
replaceChildren(el.gateBlockers, ...gateSummary.blockers.map(blockerCard));
}
function renderConversation() {
const messages = [
{
role: "system",
title: "界面模式",
text: "此界面面向 M3 可信闭环用户,展示路由、拓扑、执行轨迹与诊断,不发送硬件变更。"
},
{
role: "agent",
title: "实况验收状态",
text: userFacingSummary(gateSummary.devPreconditions.summary)
},
...gateSummary.blockers.map((blocker) => ({
role: "blocker",
title: blockerTitle(blocker),
text: userFacingSummary(blocker.summary)
})),
{
role: "user",
title: "可用操作",
text: "可在底部输入区编写指令草稿。草稿只保留在当前浏览器会话中,直到可信后端工作流就绪。"
}
];
replaceChildren(el.conversationList, ...messages.map(messageCard));
}
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 renderHardwareStatus(summary) {
const counts = summary?.counts ?? {};
const cards = [
{
title: "Gateway-SIMU",
value: valueOrUnavailable(counts.gatewaySessions, gateSummary.gatewaySessionCount),
meta: "只读数量;来源 SOURCE 回退",
tone: summary ? "dev-live" : "source"
},
{
title: "BOX-SIMU",
value: valueOrUnavailable(counts.boxResources, gateSummary.boxResourceCount),
meta: "只读数量;来源 SOURCE 回退",
tone: summary ? "dev-live" : "source"
},
{
title: "接线面板 hwlab-patch-panel",
value: statusLabel(gateSummary.topology.patchPanel.state),
meta: `${gateSummary.topology.patchPanel.activeConnectionCount} 条来源 SOURCE 接线`,
tone: gateSummary.blocked ? "blocked" : gateSummary.topology.patchPanel.state
},
{
title: "硬件写入界面",
value: "已禁用",
meta: "Web 不提供硬件写 RPC",
tone: "blocked"
}
];
replaceChildren(el.hardwareList, ...cards.map(statusCard));
}
function controlRows() {
return [
{
title: "硬件操作",
detail: "此 Web 工作台不开放硬件写入。阻塞清除后仍需通过 patch-panel 管控工作流执行。",
tone: "blocked"
},
{
title: "Agent 自动化",
detail: "当前仅作为可信轨迹与演练 DRY-RUN 证据展示;实况调度仍受 DB 就绪状态阻塞。",
tone: "dry-run"
},
{
title: "只读探测",
detail: "允许同源 /health/live、/v1 与只读 /json-rpc 诊断。",
tone: "source"
}
];
}
function renderDrafts() {
const rows = [
...state.drafts.map((draft) => ({
title: `本地草稿 ${shortTime(draft.createdAt)}`,
detail: draft.text,
tone: "dry-run"
})),
...(state.drafts.length === 0
? [
{
title: "草稿队列",
detail: "暂无本地草稿。输入区只在本地记录文本,不调用 cloud-api 变更方法。",
tone: "source"
}
]
: []),
...controlRows()
];
replaceChildren(el.controlList, ...rows.map(infoCard));
}
function renderWiringList() {
const patchPanel = gateSummary.topology.patchPanel;
const cards = [
infoCard({
title: patchPanel.serviceId,
detail: `${patchPanel.patchPanelStatusId} / ${statusLabel(patchPanel.state)} / ${patchPanel.environment}`,
tone: patchPanel.state
}),
...patchPanel.activeConnections.map((link) =>
infoCard({
title: `${link.fromResourceId}:${link.fromPort}`,
detail: `接线到 ${link.toResourceId}:${link.toPort}`,
tone: "source"
})
)
];
replaceChildren(el.wiringList, ...cards);
}
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) =>
infoCard({
title: record.evidenceId,
detail: `${recordKindLabel(record.kind ?? "record")} / ${record.operationId ?? "n/a"} / ${record.serviceId ?? "未知服务"}`,
tone: "dev-live"
})
);
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 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)}`;
article.append(textSpan(roleLabel(message.role), "message-role"));
article.append(textSpan(message.title, "message-title"));
article.append(textSpan(message.text, "message-copy"));
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 stateTag(label, detail) {
const span = document.createElement("span");
span.className = `state-tag tone-${toneClass(label)}`;
span.textContent = `${sourceKindLabel(label)} ${detail}`;
return span;
}
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 dbReadyFrom(live) {
return live.healthLive.data?.db?.ready ?? live.health.data?.db?.ready ?? live.restIndex.data?.db?.ready ?? null;
}
function probeErrors(probes) {
return probes.filter((probe) => !probe.ok).map((probe) => `${probe.method ?? probe.path}${probe.error}`);
}
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 statusLabel(value) {
const key = String(value ?? "unknown").trim();
const normalized = key.toLowerCase();
return STATUS_LABELS[normalized] ?? key;
}
function sourceKindLabel(value) {
return SOURCE_KIND_LABELS[String(value ?? "").trim().toUpperCase()] ?? statusLabel(value);
}
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 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"));
}