Files
pikasTech-HWLAB/web/hwlab-cloud-web/app.mjs
T
2026-05-23 02:39:41 +00:00

1998 lines
70 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 TRUSTED_CODE_AGENT_PROVIDERS = Object.freeze(["openai-responses", "codex-cli"]);
const UNTRUSTED_CODE_AGENT_PROVIDER_PATTERN = /\b(?:echo|mock|fixture|stub|sample)\b/iu;
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,
liveSurface: null
};
initRoutes();
initExplorerToggle();
syncMobileExplorer();
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 (helpPathnames().has(window.location.pathname.replace(/\/+$/, "") || "/")) return "help";
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");
setExplorerCollapsed(collapsed);
});
}
function setExplorerCollapsed(collapsed) {
el.shell.classList.toggle("explorer-collapsed", collapsed);
el.explorerToggle.setAttribute("aria-pressed", collapsed ? "true" : "false");
el.explorerToggle.setAttribute("aria-expanded", collapsed ? "false" : "true");
}
function syncMobileExplorer() {
const media = window.matchMedia("(max-width: 860px)");
const apply = () => {
if (media.matches) {
setExplorerCollapsed(true);
} else {
setExplorerCollapsed(false);
}
};
apply();
media.addEventListener("change", apply);
}
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);
collapseExplorerAfterMobileAction();
});
}
}
function initQuickActions() {
for (const button of document.querySelectorAll("[data-focus-command]")) {
button.addEventListener("click", () => {
el.commandInput.focus();
collapseExplorerAfterMobileAction();
});
}
}
function collapseExplorerAfterMobileAction() {
if (window.matchMedia("(max-width: 860px)").matches) {
setExplorerCollapsed(true);
}
}
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 traceId = nextProtocolId("trc");
const userMessage = {
id: nextProtocolId("msg"),
role: "user",
title: `用户 ${shortTime(new Date().toISOString())}`,
text: value,
status: "sent",
traceId,
createdAt: new Date().toISOString()
};
const pendingMessage = {
id: nextProtocolId("msg"),
role: "agent",
title: "Code Agent 处理中",
text: "正在调用真实 Code Agent / Codex 后端,请稍候。",
status: "running",
traceId,
createdAt: new Date().toISOString()
};
state.chatMessages.push(userMessage, pendingMessage);
state.chatPending = true;
el.commandInput.value = "";
renderAgentChatStatus("running");
renderConversation();
renderDrafts();
renderRecords(state.liveSurface);
try {
const result = await sendAgentMessage(value, state.conversationId, traceId);
state.conversationId = result.conversationId || result.sessionId || state.conversationId;
const index = state.chatMessages.findIndex((message) => message.id === pendingMessage.id);
const completion = classifyCodeAgentCompletion(result);
const status = completion.status;
state.chatMessages[index] = {
...pendingMessage,
title: completion.title,
text: completion.replied
? result.reply?.content || "Code Agent 没有返回文本。"
: result.status === "completed"
? untrustedCompletionMessage(result)
: failureMessage(result),
status,
traceId: result.traceId,
conversationId: result.conversationId || result.sessionId || state.conversationId,
messageId: result.messageId,
provider: result.provider,
model: result.model,
backend: result.backend,
sourceKind: completion.sourceKind,
providerTrace: result.providerTrace,
updatedAt: result.updatedAt,
error: result.error ?? (result.status === "completed" && !completion.replied
? {
code: "untrusted_completion",
message: "completed 回复缺少真实 provider/model/trace/conversation 证据,或 provider 属于 echo/mock/stub。"
}
: undefined),
availability: result.availability
};
if (result.availability) {
state.codeAgentAvailability = result.availability;
}
if (!completion.replied) {
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",
traceId: error.traceId || traceId,
updatedAt: new Date().toISOString(),
error: {
message: error.message
}
};
el.commandInput.value = value;
renderAgentChatStatus("failed");
} finally {
state.chatPending = false;
renderConversation();
renderDrafts();
renderRecords(state.liveSurface);
}
});
el.commandClear.addEventListener("click", () => {
state.chatMessages = [];
state.conversationId = null;
state.chatPending = false;
el.commandInput.value = "";
renderAgentChatStatus("idle");
renderConversation();
renderDrafts();
renderRecords(state.liveSurface);
});
}
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, 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) {
if (isBlockedAgentResponse(response.data, response.status)) {
return normalizeBlockedAgentResult(response.data, response.status, traceId, response.error);
}
const error = new Error(response.error || "Code Agent 请求失败");
error.traceId = traceId;
throw error;
}
return {
...response.data,
traceId: response.data?.traceId || traceId
};
}
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();
let data = null;
try {
data = text ? JSON.parse(text) : null;
} catch (error) {
if (response.ok) throw error;
return {
ok: false,
path,
status: response.status,
data: null,
error: `${path} HTTP ${response.status}:响应不是 JSON`
};
}
if (!response.ok) {
return {
ok: false,
path,
status: response.status,
data,
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) {
state.liveSurface = live;
const coreProbes = [live.healthLive, live.restIndex, live.health, live.adapter];
const surfaceStatus = workbenchApiSurfaceStatus(live, coreProbes);
const runtimeSummary = runtimeSummaryFrom(live);
const methods = methodsFrom(live);
el.liveStatus.textContent = surfaceStatus.label;
el.liveStatus.className = `tone-${toneClass(surfaceStatus.tone)}`;
state.codeAgentAvailability = codeAgentAvailabilityFrom(live);
el.liveDetail.textContent = surfaceStatus.detail ?? codeAgentAvailabilitySummary();
renderAgentChatStatus(deriveAgentChatStatus());
renderHardwareStatus(runtimeSummary);
renderWiringList(runtimeSummary);
renderRecords(live);
renderDiagnostics(live);
renderMethodList(methods.length > 0 ? methods.filter((method) => rpcReadMethods.includes(method)) : rpcReadMethods, surfaceStatus.methodTone);
renderConversation();
renderDrafts();
}
function workbenchApiSurfaceStatus(live, coreProbes = [live.healthLive, live.restIndex, live.health, live.adapter]) {
const surface = workbenchSurfaceReadiness(live, coreProbes);
if (!surface.reachable) {
return {
label: "离线查看",
tone: "source",
methodTone: "degraded",
detail: "同源 API 暂不可用,当前仍可使用本地草稿和已加载资源。"
};
}
if (surface.durableQueryBlocked) {
return {
label: "可信记录受阻 / 只读模式",
tone: "blocked",
methodTone: "degraded",
detail: "同源 API 可响应,但可信记录持久化尚未就绪;当前只保留只读查看和本地任务整理。"
};
}
if (surface.degraded) {
return {
label: "API 降级 / 只读模式",
tone: "degraded",
methodTone: "degraded",
detail: "同源 API 可响应但处于降级状态;当前保持只读查看和本地任务整理。"
};
}
return {
label: "只读连接可用",
tone: "dev-live",
methodTone: "live",
detail: null
};
}
function workbenchSurfaceReadiness(live, coreProbes = [live.healthLive, live.restIndex, live.health, live.adapter]) {
const payloads = coreProbes
.filter((probe) => probe?.ok)
.map((probe) => probe.data)
.filter((payload) => payload && typeof payload === "object");
const statuses = payloads.flatMap(readinessStatusValues).map(normalizedStatusValue);
const readyValues = payloads.flatMap(readinessBooleanValues);
return {
reachable: payloads.length > 0,
degraded:
statuses.some((status) => ["blocked", "degraded", "failed"].includes(status)) ||
readyValues.includes(false),
durableQueryBlocked: payloads.some(hasDurableRuntimeQueryBlock)
};
}
function readinessStatusValues(payload) {
return [
payload.status,
payload.readiness?.status,
payload.readiness?.durability?.status,
payload.runtime?.status
].filter((value) => value !== undefined && value !== null);
}
function readinessBooleanValues(payload) {
return [
payload.ready,
payload.readiness?.ready,
payload.readiness?.durability?.ready,
payload.runtime?.ready
].filter((value) => typeof value === "boolean");
}
function hasDurableRuntimeQueryBlock(payload) {
const codes = [
...(Array.isArray(payload.blockerCodes) ? payload.blockerCodes : []),
...(Array.isArray(payload.readiness?.blockerCodes) ? payload.readiness.blockerCodes : []),
...(Array.isArray(payload.blockers) ? payload.blockers.map((item) => item?.code) : []),
...(Array.isArray(payload.readiness?.blockers) ? payload.readiness.blockers.map((item) => item?.code) : []),
payload.runtime?.blocker,
payload.readiness?.durability?.blocker
].map(normalizedStatusValue);
const queryResult = normalizedStatusValue(payload.runtime?.connection?.queryResult ?? payload.readiness?.durability?.queryResult);
const blockedLayer = normalizedStatusValue(payload.runtime?.durabilityContract?.blockedLayer ?? payload.readiness?.durability?.blockedLayer);
return (
codes.includes("runtime_durable_adapter_query_blocked") ||
queryResult === "query_blocked" ||
blockedLayer === "durability_query"
);
}
function normalizedStatusValue(value) {
return String(value ?? "").trim().toLowerCase();
}
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) {
if (!isDurableRuntimeReady(summary)) return null;
const candidates = [
summary?.m3TrustedLoop,
summary?.m3HardwareLoop,
summary?.hardware?.m3TrustedLoop,
summary?.patchPanel?.m3TrustedLoop,
summary?.liveM3Evidence
].filter(Boolean);
return candidates.find(hasTrustedM3LiveEvidence) ?? null;
}
function liveRecordTone(record, live) {
const runtimeSummary = runtimeSummaryFrom(live);
return isDurableRuntimeReady(runtimeSummary) && hasTrustedM3LiveEvidence(record) ? "dev-live" : "blocked";
}
function liveRecordDurabilityBlocker(live) {
const runtimeSummary = runtimeSummaryFrom(live);
if (isDurableRuntimeReady(runtimeSummary)) return null;
if (runtimeSummary?.durableRequested === true || runtimeSummary?.adapter === "postgres") {
return `runtime durable adapter 未 readystatus=${runtimeSummary?.status ?? "unknown"}durable=${runtimeSummary?.durable === true});可信记录保持 BLOCKED。`;
}
if (runtimeSummary?.durable !== true) {
return "runtime.durable=false;只读 audit/evidence 不能标为 durable ready 或 DEV-LIVE。";
}
return `runtime durable adapter 未 readystatus=${runtimeSummary?.status ?? "unknown"});可信记录保持 BLOCKED。`;
}
function isDurableRuntimeReady(summary) {
if (!summary || typeof summary !== "object") return false;
if (summary.durable !== true) return false;
if (summary.ready === false) return false;
if (summary.liveRuntimeEvidence !== true) return false;
return !["blocked", "degraded", "failed"].includes(String(summary.status ?? "").toLowerCase());
}
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 groups = [
recordGroup({
title: "Code Agent 对话记录",
meta: "trace/messageId",
cards: codeAgentRecordCards()
}),
recordGroup({
title: "接线/operation",
meta: "hwlab-patch-panel + operation",
cards: operationRecordCards()
}),
recordGroup({
title: "audit 记录",
meta: "audit.event.query + SOURCE 回退",
cards: auditRecordCards(live)
}),
recordGroup({
title: "evidence 证据",
meta: "evidence.record.query + SOURCE 回退",
cards: evidenceRecordCards(live)
})
];
replaceChildren(el.recordsList, ...groups);
}
function codeAgentRecordCards() {
const messages = state.chatMessages.filter((message) => message.traceId || message.messageId);
if (messages.length === 0) {
return [
infoCard({
title: "尚无 Code Agent 请求记录",
detail: "发送消息后会在这里显示 conversationId、traceId 和 messageId;未发送时仅显示 SOURCE 说明。",
tone: "source"
})
];
}
return messages.slice(-4).reverse().map((message) =>
infoCard({
title: `${roleLabel(message.role)} ${statusLabel(message.status)}`,
detail: [
sourceKindLabel(message.status === "failed" ? "blocked" : "source"),
recordField("conversation", state.conversationId),
recordField("trace", message.traceId),
recordField("message", message.messageId),
message.error?.message ? `失败原因=${message.error.message}` : null
].filter(Boolean).join(" / "),
tone: message.status === "failed" ? "blocked" : "source"
})
);
}
function operationRecordCards() {
const wiring = gateSummary.topology.patchPanel;
const wiringCards = wiring.activeConnections.map((link) =>
infoCard({
title: `${link.fromResourceId}:${link.fromPort} -> ${link.toResourceId}:${link.toPort}`,
detail: [
sourceKindLabel("source"),
recordField("patch-panel", wiring.serviceId),
`状态=${statusLabel(wiring.state)}`,
recordField("statusId", wiring.patchPanelStatusId)
].filter(Boolean).join(" / "),
tone: "source"
})
);
const operationCards = gateSummary.operations.map((operation) =>
infoCard({
title: operation.operationId,
detail: [
sourceKindLabel(operation.dryRun ? "dry-run" : "source"),
`状态=${statusLabel(operation.status)}`,
recordField("resource", operation.resourceId),
recordField("capability", operation.capabilityId),
recordField("worker", operation.workerSessionId)
].filter(Boolean).join(" / "),
tone: operation.dryRun ? "dry-run" : "source"
})
);
return [...wiringCards, ...operationCards];
}
function auditRecordCards(live) {
const cards = [];
if (live) {
if (live.audit.ok) {
const liveEvents = live.audit.data?.events ?? [];
if (liveEvents.length === 0) {
cards.push(infoCard({
title: "audit.event.query",
detail: "只读查询成功但无 M3 trusted loop audit;下方显示 SOURCE/DRY-RUN 回退摘要,不能冒充 DEV-LIVE。",
tone: "source"
}));
} else {
cards.push(...liveEvents.map((event) => auditCard(event, liveRecordTone(event, live), liveRecordDurabilityBlocker(live))));
}
} else {
cards.push(liveFailureCard("audit.event.query", live.audit.error));
}
} else {
cards.push(infoCard({
title: "audit.event.query",
detail: "等待同源只读查询;未完成前显示 SOURCE/DRY-RUN 回退摘要。",
tone: "source"
}));
}
cards.push(...gateSummary.auditEvents.slice(0, 4).map((event) => auditCard(event, event.dryRun ? "dry-run" : "source")));
return cards;
}
function evidenceRecordCards(live) {
const cards = [];
if (live) {
if (live.evidence.ok) {
const liveRecords = live.evidence.data?.records ?? [];
if (liveRecords.length === 0) {
cards.push(infoCard({
title: "evidence.record.query",
detail: "只读查询成功但无 M3 trusted loop evidence;下方显示 SOURCE/DRY-RUN 回退摘要,不能冒充 DEV-LIVE。",
tone: "source"
}));
} else {
cards.push(...liveRecords.map((record) => evidenceCard(record, liveRecordTone(record, live), liveRecordDurabilityBlocker(live))));
}
} else {
cards.push(liveFailureCard("evidence.record.query", live.evidence.error));
}
} else {
cards.push(infoCard({
title: "evidence.record.query",
detail: "等待同源只读查询;未完成前显示 SOURCE/DRY-RUN 回退摘要。",
tone: "source"
}));
}
cards.push(...gateSummary.evidenceRecords.map((record) => evidenceCard(record, record.dryRun ? "dry-run" : "source")));
return cards;
}
function auditCard(event, tone, durabilityBlocker = null) {
return infoCard({
title: event.auditId,
detail: [
sourceKindLabel(tone),
durabilityBlocker,
tone === "blocked" ? "只读 audit 缺少 M3 patch-panel live report / operation / trace / evidence 绑定" : null,
recordField("action", event.action),
recordField("target", event.targetId),
recordField("operation", event.operationId),
recordField("trace", event.traceId),
recordField("service", event.serviceId)
].filter(Boolean).join(" / "),
tone
});
}
function evidenceCard(record, tone, durabilityBlocker = null) {
return infoCard({
title: record.evidenceId,
detail: [
sourceKindLabel(tone),
durabilityBlocker,
tone === "blocked" ? "只读 evidence 缺少 M3 patch-panel live report / operation / trace / audit 绑定" : null,
recordKindLabel(record.kind ?? "record"),
recordField("operation", record.operationId),
recordField("trace", record.traceId ?? record.metadata?.traceId),
recordField("service", record.serviceId)
].filter(Boolean).join(" / "),
tone
});
}
function liveFailureCard(title, reason) {
return infoCard({
title,
detail: `阻塞 BLOCKED:开发实况查询失败,失败原因:${reason || "未知错误"};当前显示 SOURCE 回退,不能冒充 DEV-LIVE。`,
tone: "blocked"
});
}
function recordGroup({ title, meta, cards }) {
const section = document.createElement("section");
section.className = "record-group";
const header = document.createElement("div");
header.className = "record-group-head";
header.append(textSpan(title, "record-group-title"), textSpan(meta, "record-group-meta"));
const body = document.createElement("div");
body.className = "record-group-list";
body.append(...cards);
section.append(header, body);
return section;
}
function recordField(label, value) {
if (value === undefined || value === null || value === "") return null;
return `${label}=${value}`;
}
function sourceKindLabel(tone) {
return statusLabel(tone);
}
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/dev-cloud-workbench-smoke.mjs --local-agent-fixture",
"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: "DEV-LIVE 回复",
source: "SOURCE 回复",
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 sourceFixtureTitle(result) {
return `Code Agent SOURCE 回复 ${shortTime(result.updatedAt ?? new Date().toISOString())}`;
}
function classifyCodeAgentCompletion(result) {
if (isRealCompletedChatResult(result)) {
return {
status: "completed",
replied: true,
sourceKind: "DEV-LIVE",
title: sourceTitle(result)
};
}
if (isSourceFixtureChatResult(result)) {
return {
status: "source",
replied: true,
sourceKind: "SOURCE",
title: sourceFixtureTitle(result)
};
}
return {
status: "failed",
replied: false,
sourceKind: "BLOCKED",
title: result?.status === "completed" ? "Code Agent 完成证据不足" : "Code Agent 返回失败"
};
}
function failureMessage(result) {
if (result?.error?.code === "provider_unavailable" || result?.availability?.status === "blocked") {
const availability = result.error?.providerStatus || result.availability?.status !== "blocked"
? codeAgentAvailabilityFromResult(result)
: 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 messageEvidence = messageEvidencePanel(message);
if (messageEvidence) article.append(messageEvidence);
return article;
}
function messageEvidencePanel(message) {
const fields = messageEvidenceFields(message).map(boundedEvidenceField);
if (fields.length === 0) return null;
const details = document.createElement("details");
details.className = "message-evidence";
details.open = message.status === "completed" || message.status === "source";
const summary = document.createElement("summary");
summary.className = "message-meta";
summary.textContent = messageEvidenceSummary(message, fields);
const chips = document.createElement("div");
chips.className = "message-evidence-chips";
chips.append(...fields.map((field) => textSpan(field, "message-evidence-chip")));
details.append(summary, chips);
return details;
}
function messageEvidenceSummary(message, fields) {
const sourceKind = message.sourceKind ?? (message.status === "completed" ? "DEV-LIVE" : message.status === "failed" ? "BLOCKED" : "SOURCE");
return `证据 ${sourceKind} / ${fields.slice(0, 3).join(" / ")}`;
}
function messageEvidenceFields(message) {
return [
recordField("source", message.sourceKind),
recordField("provider", message.provider),
recordField("model", message.model),
recordField("backend", message.backend),
recordField("conversation", message.conversationId),
recordField("trace", message.traceId),
recordField("message", message.messageId),
providerTraceSummary(message.providerTrace)
].filter(Boolean);
}
function boundedEvidenceField(field) {
const text = String(field ?? "").replace(/\s+/gu, " ").trim();
return text.length > 96 ? `${text.slice(0, 93)}...` : text;
}
function providerTraceSummary(providerTrace) {
if (!providerTrace || typeof providerTrace !== "object") return null;
const responseId = providerTrace.responseId ?? providerTrace.id;
if (responseId) return recordField("providerTrace", responseId);
if (providerTrace.command) return "providerTrace=codex-cli";
return null;
}
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 normalizeBlockedAgentResult(payload, httpStatus, traceId, fallbackMessage) {
const { reply, ...safePayload } = payload;
const providerStatus = payload.error?.providerStatus ?? httpStatus;
const error = {
...(payload.error && typeof payload.error === "object" ? payload.error : {}),
code: payload.error?.code ?? "provider_unavailable",
message: payload.error?.message ?? fallbackMessage ?? `Code Agent provider HTTP ${httpStatus}`,
providerStatus
};
return {
...safePayload,
status: "failed",
traceId: payload.traceId || traceId,
error,
availability: payload.availability ?? codeAgentAvailabilityFromResult({ error })
};
}
function isBlockedAgentResponse(payload, httpStatus) {
return Boolean(
payload &&
typeof payload === "object" &&
(
isHttpNon2xxStatus(httpStatus) ||
isHttpNon2xxStatus(payload.error?.providerStatus) ||
payload.status === "blocked" ||
payload.error?.code === "provider_unavailable" ||
payload.availability?.status === "blocked"
)
);
}
function isHttpNon2xxStatus(value) {
return typeof value === "number" && Number.isInteger(value) && (value < 200 || value > 299);
}
function codeAgentAvailabilityFrom(live) {
return live?.restIndex?.data?.codeAgent ?? live?.healthLive?.data?.codeAgent ?? null;
}
function codeAgentAvailabilityFromResult(result) {
const missingEnv = Array.isArray(result?.error?.missingEnv) ? result.error.missingEnv : [];
const providerStatus = result?.error?.providerStatus;
const credentialBlocked = missingEnv.includes("OPENAI_API_KEY");
return {
status: "blocked",
blocker: credentialBlocked ? "凭证缺口" : "provider_unavailable",
reason: result?.error?.code ?? "provider_unavailable",
summary: providerStatus
? `真实后端已接入,但当前 provider 上游返回 HTTP ${providerStatus};本次保持 BLOCKED/provider_unavailable,不会冒充真实 Code Agent 回复。`
: "真实后端已接入,但当前 DEV provider Secret hwlab-code-agent-provider/openai-api-key 未注入,因此不能返回真实回复。",
missingEnv,
secretRefs: credentialBlocked ? [
{
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 (String(availability?.sourceKind ?? availability?.evidenceLevel ?? "").toUpperCase() === "SOURCE") {
return {
role: "system",
title: "Code Agent 状态:SOURCE fixture",
text: "当前只用于本地浏览器 smoke:可以验证发送、处理中、回复渲染和 evidence 展示,但不会标记为 DEV-LIVE。",
status: "source"
};
}
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 接口,并带有 provider/model/trace/conversation 证据。`
: "最近一次真实 completed 回复来自 cloud-api 受控 Code Agent 接口,并带有 provider/model/trace/conversation 证据。";
}
if (isSourceFixtureCompletedChatMessage(latestChatResult())) {
return "最近一次为 SOURCE fixture 回复;它证明本地浏览器对话 UX,不作为 DEV-LIVE 或 #143 完成证据。";
}
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 untrustedCompletionMessage(result) {
const provider = result?.provider ?? "unknown";
return `Code Agent 返回 completed,但缺少真实 provider/model/trace/conversation 证据,或 provider=${provider} 属于 echo/mock/stub;本次不会标记为真实完成。SOURCE fixture 只可显示为 SOURCE 回复,不能冒充 DEV-LIVE。`;
}
function codeAgentAvailabilitySummary() {
if (state.codeAgentAvailability?.status === "blocked") {
return "同源 API 可响应;Code Agent 为 BLOCKED/凭证缺口,真实后端已接入但 DEV provider Secret hwlab-code-agent-provider/openai-api-key 未注入。";
}
if (String(state.codeAgentAvailability?.sourceKind ?? state.codeAgentAvailability?.evidenceLevel ?? "").toUpperCase() === "SOURCE") {
return "同源 API 可响应;当前 Code Agent 回复来源是 SOURCE fixture,仅用于本地浏览器 smoke,不是 DEV-LIVE。";
}
if (state.codeAgentAvailability?.status === "available") {
return "同源 API 可响应;Code Agent 真实通道已接入,只有 completed 回复才标 DEV-LIVE。";
}
return "同源只读接口可响应。技术复核详情已放在二级页面。";
}
function latestCompletedAgentMessage() {
return [...state.chatMessages].reverse().find((message) =>
message.role === "agent" &&
isRealCompletedChatMessage(message)
) ?? null;
}
function isRealCompletedChatResult(result) {
const assistantReply = typeof result?.reply?.content === "string" ? result.reply.content.trim() : "";
return result?.status === "completed" && assistantReply.length > 0 && !isSourceFixtureCompletion(result) && hasRealCodeAgentEvidence(result);
}
function isSourceFixtureChatResult(result) {
const assistantReply = typeof result?.reply?.content === "string" ? result.reply.content.trim() : "";
return result?.status === "completed" && assistantReply.length > 0 && isSourceFixtureCompletion(result);
}
function isRealCompletedChatMessage(message) {
return message?.status === "completed" && !message.error && hasRealCodeAgentEvidence(message);
}
function isSourceFixtureCompletedChatMessage(message) {
return message?.status === "source" && !message.error && message.sourceKind === "SOURCE";
}
function hasRealCodeAgentEvidence(value) {
return (
isTrustedCodeAgentProvider(value?.provider) &&
Boolean(value?.model) &&
Boolean(value?.backend) &&
Boolean(value?.traceId) &&
Boolean(value?.conversationId || value?.sessionId) &&
!UNTRUSTED_CODE_AGENT_PROVIDER_PATTERN.test(`${value?.provider ?? ""} ${value?.backend ?? ""}`)
);
}
function isTrustedCodeAgentProvider(provider) {
const normalized = String(provider ?? "").trim().toLowerCase();
return TRUSTED_CODE_AGENT_PROVIDERS.includes(normalized);
}
function isSourceFixtureCompletion(value) {
const sourceKind = String(value?.sourceKind ?? value?.evidenceLevel ?? value?.providerTrace?.sourceKind ?? "").trim().toUpperCase();
const traceSource = String(value?.providerTrace?.source ?? value?.providerTrace?.mode ?? "").trim().toUpperCase();
return sourceKind === "SOURCE" || traceSource.startsWith("SOURCE");
}
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 === "source") return "source";
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 (isSourceFixtureCompletedChatMessage(latestChatResult())) return "source";
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 helpPathnames() {
return new Set(["/help"]);
}
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"));
}