Files
pikasTech-HWLAB/web/hwlab-cloud-web/app.mjs
T
Lyon 997b96dc65 fix: add structured Code Agent blockers
Add structured Code Agent blocker taxonomy across /v1/agent/chat, runner/session/tool, and HWLAB M3 Skill CLI paths.\n\nValidated with npm run check and focused server/session/Skill CLI/web tests.
2026-05-23 23:17:13 +08:00

3212 lines
119 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 {
codeAgentFactsFromMessage,
compactHwlabApiFact,
runnerTraceSummary as compactRunnerTraceSummary,
skillsSummary as compactSkillsSummary,
toolCallsSummary as compactToolCallsSummary
} from "./code-agent-facts.mjs";
import { classifyCodeAgentStatusSummary, codeAgentAvailabilityFromLive } from "./code-agent-status.mjs";
import { classifyWorkbenchLiveStatus } from "./live-status.mjs";
import { marked } from "./third_party/marked/marked.esm.js";
import { wiringPresentationFromSummary } from "./wiring-status.mjs";
const DEFAULT_API_TIMEOUT_MS = 4500;
const DEFAULT_CODE_AGENT_TIMEOUT_MS = 150000;
const API_TIMEOUT_MS = resolveTimeoutMs("apiTimeoutMs", DEFAULT_API_TIMEOUT_MS, { min: 500, max: 30000 });
const CODE_AGENT_TIMEOUT_MS = resolveTimeoutMs("codeAgentTimeoutMs", DEFAULT_CODE_AGENT_TIMEOUT_MS, { min: 50, max: 300000 });
const rpcReadMethods = Object.freeze([
"system.health",
"cloud.adapter.describe",
"audit.event.query",
"evidence.record.query"
]);
const STATUS_LABELS = Object.freeze({
active: "活动",
available: "可用",
blocked_after_cloud_api: "cloud-api 后阻塞",
blocked: "待处理",
completed: "完成",
connected: "已连接",
degraded: "降级",
disabled: "禁用",
entry: "入口",
failed: "失败",
healthy: "健康",
live: "实况",
ok: "正常",
open: "未关闭",
pass: "通过",
pending: "等待",
running: "处理中",
probing: "探测中",
ready: "就绪",
recorded: "已记录",
sent: "已发送",
requires: "依赖",
source: "来源",
unverified: "未验证",
"dry-run": "演练",
"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", "codex-readonly-runner", "codex-stdio"]);
const CODEX_RUNNER_CAPABLE_PROVIDERS = Object.freeze(["codex-stdio"]);
const CODEX_READONLY_PARTIAL_PROVIDERS = Object.freeze(["codex-readonly-runner"]);
const CODEX_STDIO_BLOCKER_MARKERS = Object.freeze([
"codex_stdio_supervisor_disabled",
"not-codex-stdio",
"not-write-capable",
"not-durable-session"
]);
const UNTRUSTED_CODE_AGENT_PROVIDER_PATTERN = /\b(?:echo|mock|fixture|stub|sample)\b/iu;
const LAYOUT_STORAGE_KEY = "hwlab.workbench.layout.v1";
const EXPLORER_DEFAULT_WIDTH = 292;
const EXPLORER_MIN_WIDTH = 220;
const EXPLORER_MAX_WIDTH = 420;
const RIGHT_SIDEBAR_DEFAULT_WIDTH = 620;
const RIGHT_SIDEBAR_MIN_WIDTH = 560;
const RIGHT_SIDEBAR_MAX_WIDTH = 760;
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"),
explorerResize: byId("explorer-resize"),
rightSidebarResize: byId("right-sidebar-resize"),
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"),
codeAgentSummary: byId("code-agent-summary"),
codeAgentSummaryIcon: byId("code-agent-summary-icon"),
codeAgentSummaryLabel: byId("code-agent-summary-label"),
codeAgentSummaryCapability: byId("code-agent-summary-capability"),
codeAgentSummaryTrace: byId("code-agent-summary-trace"),
codeAgentSummaryDetail: byId("code-agent-summary-detail"),
conversationList: byId("conversation-list"),
taskList: byId("task-list"),
gateSourceStatus: byId("gate-source-status"),
gateStatusFilter: byId("gate-status-filter"),
gateSearch: byId("gate-search"),
gateRefresh: byId("gate-refresh"),
gateReviewBody: byId("gate-review-body"),
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"),
m3ControlForm: byId("m3-control-form"),
m3ControlStatus: byId("m3-control-status"),
m3GatewaySelect: byId("m3-gateway-select"),
m3BoxSelect: byId("m3-box-select"),
m3PortSelect: byId("m3-port-select"),
m3ValueSelect: byId("m3-value-select"),
m3WriteDo: byId("m3-write-do"),
m3ReadDi: byId("m3-read-di"),
helpContent: byId("help-content"),
helpStatus: byId("help-status")
};
const state = {
conversationId: null,
sessionId: null,
codeAgentAvailability: null,
chatMessages: [],
chatPending: false,
liveSurface: null,
gateDiagnostics: {
loading: false,
payload: null,
rows: [],
error: null,
loadedAt: null
},
m3Control: {
contract: null,
status: null,
hardwareTab: "overview",
operation: null,
pending: false
},
layout: {
explorerWidth: EXPLORER_DEFAULT_WIDTH,
rightSidebarWidth: RIGHT_SIDEBAR_DEFAULT_WIDTH,
drag: null,
rightDrag: null
}
};
initRoutes();
initLayoutSizing();
initExplorerToggle();
syncMobileExplorer();
initExplorerResize();
initRightSidebarResize();
initSideTabs();
initHardwareTabs();
initQuickActions();
initCommandBar();
initM3Control();
initGateControls();
renderStaticWorkbench();
renderProbePending();
renderCodeAgentSummary();
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 resolveTimeoutMs(name, fallback, { min, max }) {
const config = globalThis.HWLAB_CLOUD_WEB_CONFIG?.timeouts ?? globalThis.HWLAB_CLOUD_WEB_TIMEOUTS ?? {};
const configured = Number(config?.[name]);
if (!Number.isFinite(configured)) return fallback;
return Math.min(Math.max(Math.trunc(configured), min), max);
}
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 initLayoutSizing() {
const persisted = readPersistedLayout();
const explorerWidth = clampExplorerWidth(persisted?.explorerWidth ?? EXPLORER_DEFAULT_WIDTH);
const rightSidebarWidth = clampRightSidebarWidth(persisted?.rightSidebarWidth ?? RIGHT_SIDEBAR_DEFAULT_WIDTH);
setExplorerWidth(explorerWidth, { persist: false });
setRightSidebarWidth(rightSidebarWidth, { persist: false });
}
function readPersistedLayout() {
try {
const raw = window.localStorage?.getItem(LAYOUT_STORAGE_KEY);
if (!raw) return null;
const payload = JSON.parse(raw);
if (payload?.version !== 1) return null;
return {
explorerWidth: Number(payload.explorerWidth),
rightSidebarWidth: Number(payload.rightSidebarWidth)
};
} catch {
return null;
}
}
function persistLayout() {
try {
window.localStorage?.setItem(
LAYOUT_STORAGE_KEY,
JSON.stringify({
version: 1,
explorerWidth: state.layout.explorerWidth,
rightSidebarWidth: state.layout.rightSidebarWidth
})
);
} catch {
// Browser storage can be unavailable; layout still works for the session.
}
}
function setExplorerWidth(width, options = {}) {
const clamped = clampExplorerWidth(width);
state.layout.explorerWidth = clamped;
el.shell.style.setProperty("--explorer-width", `${clamped}px`);
syncExplorerResizeA11y();
syncRightSidebarResizeA11y();
if (options.persist !== false) persistLayout();
return clamped;
}
function setRightSidebarWidth(width, options = {}) {
const clamped = clampRightSidebarWidth(width);
state.layout.rightSidebarWidth = clamped;
el.shell.style.setProperty("--right-sidebar-width", `${clamped}px`);
syncRightSidebarResizeA11y();
if (options.persist !== false) persistLayout();
return clamped;
}
function clampExplorerWidth(value) {
const numeric = Number(value);
const width = Number.isFinite(numeric) ? numeric : EXPLORER_DEFAULT_WIDTH;
const bounds = explorerResizeBounds();
return Math.min(Math.max(Math.round(width), bounds.min), bounds.max);
}
function clampRightSidebarWidth(value) {
const numeric = Number(value);
const width = Number.isFinite(numeric) ? numeric : RIGHT_SIDEBAR_DEFAULT_WIDTH;
const bounds = rightSidebarResizeBounds();
return Math.min(Math.max(Math.round(width), bounds.min), bounds.max);
}
function explorerResizeBounds() {
const styles = getComputedStyle(el.shell);
const cssMin = readCssPixel(styles.getPropertyValue("--explorer-min-width"), EXPLORER_MIN_WIDTH);
const cssMax = readCssPixel(styles.getPropertyValue("--explorer-max-width"), EXPLORER_MAX_WIDTH);
if (window.matchMedia("(max-width: 860px)").matches) {
return { min: EXPLORER_MIN_WIDTH, max: EXPLORER_MAX_WIDTH };
}
if (window.matchMedia("(max-width: 1240px)").matches) {
const railWidth = readCssPixel(styles.getPropertyValue("--rail-width"), 86);
const dynamicMax = window.innerWidth - railWidth - 420 - 4;
return boundedResizeRange(cssMin, cssMax, dynamicMax);
}
const railWidth = readCssPixel(styles.getPropertyValue("--rail-width"), 92);
const rightWidth = Math.min(Math.max(window.innerWidth * 0.45, 560), 740);
const dynamicMax = window.innerWidth - railWidth - 360 - rightWidth - 4;
return boundedResizeRange(cssMin, cssMax, dynamicMax);
}
function boundedResizeRange(min, max, dynamicMax) {
const safeMin = Math.max(0, Math.round(min));
const safeMax = Math.max(safeMin, Math.round(Math.min(max, Number.isFinite(dynamicMax) ? dynamicMax : max)));
return { min: safeMin, max: safeMax };
}
function rightSidebarResizeBounds() {
const styles = getComputedStyle(el.shell);
const cssMin = readCssPixel(styles.getPropertyValue("--right-sidebar-min-width"), RIGHT_SIDEBAR_MIN_WIDTH);
const cssMax = readCssPixel(styles.getPropertyValue("--right-sidebar-max-width"), RIGHT_SIDEBAR_MAX_WIDTH);
if (window.matchMedia("(max-width: 1240px)").matches) {
return { min: 0, max: Math.max(0, window.innerWidth) };
}
const railWidth = readCssPixel(styles.getPropertyValue("--rail-width"), 92);
const explorerWidth = el.shell.classList.contains("explorer-collapsed") ? 0 : state.layout.explorerWidth;
const centerMinWidth = el.shell.classList.contains("explorer-collapsed") ? 420 : 360;
const dynamicMax = window.innerWidth - railWidth - explorerWidth - centerMinWidth - 4;
return boundedResizeRange(cssMin, cssMax, dynamicMax);
}
function readCssPixel(value, fallback) {
const parsed = Number.parseFloat(String(value ?? "").trim());
return Number.isFinite(parsed) ? parsed : fallback;
}
function syncExplorerResizeA11y() {
const bounds = explorerResizeBounds();
el.explorerResize.setAttribute("aria-valuemin", String(bounds.min));
el.explorerResize.setAttribute("aria-valuemax", String(bounds.max));
el.explorerResize.setAttribute("aria-valuenow", String(state.layout.explorerWidth));
el.explorerResize.setAttribute("aria-valuetext", `左侧资源树宽度 ${state.layout.explorerWidth} 像素`);
}
function syncRightSidebarResizeA11y() {
const bounds = rightSidebarResizeBounds();
const disabled = !canResizeRightSidebar();
el.rightSidebarResize.tabIndex = disabled ? -1 : 0;
el.rightSidebarResize.setAttribute("aria-disabled", disabled ? "true" : "false");
el.rightSidebarResize.setAttribute("aria-hidden", disabled ? "true" : "false");
el.rightSidebarResize.setAttribute("aria-valuemin", String(bounds.min));
el.rightSidebarResize.setAttribute("aria-valuemax", String(bounds.max));
el.rightSidebarResize.setAttribute("aria-valuenow", String(state.layout.rightSidebarWidth));
el.rightSidebarResize.setAttribute("aria-valuetext", `右侧硬件状态栏宽度 ${state.layout.rightSidebarWidth} 像素`);
}
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");
}
if (activeRoute === "gate" && state.gateDiagnostics.rows.length === 0 && !state.gateDiagnostics.loading) {
loadGateDiagnostics();
}
}
function initExplorerToggle() {
el.explorerToggle.addEventListener("click", () => {
const collapsed = !el.shell.classList.contains("explorer-collapsed");
setExplorerCollapsed(collapsed);
});
}
function initExplorerResize() {
window.addEventListener("resize", () => {
if (canResizeExplorer()) setExplorerWidth(state.layout.explorerWidth, { persist: false });
syncExplorerResizeA11y();
});
el.explorerResize.addEventListener("pointerdown", (event) => {
if (!canResizeExplorer()) return;
event.preventDefault();
el.explorerResize.setPointerCapture?.(event.pointerId);
state.layout.drag = {
pointerId: event.pointerId,
startX: event.clientX,
startWidth: state.layout.explorerWidth
};
el.shell.classList.add("is-resizing");
});
el.explorerResize.addEventListener("pointermove", (event) => {
const drag = state.layout.drag;
if (!drag || drag.pointerId !== event.pointerId) return;
setExplorerWidth(drag.startWidth + event.clientX - drag.startX, { persist: false });
});
el.explorerResize.addEventListener("pointerup", finishExplorerResize);
el.explorerResize.addEventListener("pointercancel", finishExplorerResize);
el.explorerResize.addEventListener("keydown", (event) => {
if (!canResizeExplorer()) return;
const keySteps = {
ArrowLeft: -16,
ArrowRight: 16,
PageDown: -48,
PageUp: 48
};
if (Object.hasOwn(keySteps, event.key)) {
event.preventDefault();
setExplorerWidth(state.layout.explorerWidth + keySteps[event.key]);
} else if (event.key === "Home") {
event.preventDefault();
setExplorerWidth(explorerResizeBounds().min);
} else if (event.key === "End") {
event.preventDefault();
setExplorerWidth(explorerResizeBounds().max);
}
});
}
function initRightSidebarResize() {
window.addEventListener("resize", () => {
if (canResizeRightSidebar()) setRightSidebarWidth(state.layout.rightSidebarWidth, { persist: false });
syncRightSidebarResizeA11y();
});
el.rightSidebarResize.addEventListener("pointerdown", (event) => {
if (!canResizeRightSidebar()) return;
event.preventDefault();
el.rightSidebarResize.setPointerCapture?.(event.pointerId);
state.layout.rightDrag = {
pointerId: event.pointerId,
startX: event.clientX,
startWidth: state.layout.rightSidebarWidth
};
el.shell.classList.add("is-resizing");
});
el.rightSidebarResize.addEventListener("pointermove", (event) => {
const drag = state.layout.rightDrag;
if (!drag || drag.pointerId !== event.pointerId) return;
setRightSidebarWidth(drag.startWidth + drag.startX - event.clientX, { persist: false });
});
el.rightSidebarResize.addEventListener("pointerup", finishRightSidebarResize);
el.rightSidebarResize.addEventListener("pointercancel", finishRightSidebarResize);
el.rightSidebarResize.addEventListener("keydown", (event) => {
if (!canResizeRightSidebar()) return;
const keySteps = {
ArrowLeft: 16,
ArrowRight: -16,
PageUp: 48,
PageDown: -48
};
if (Object.hasOwn(keySteps, event.key)) {
event.preventDefault();
setRightSidebarWidth(state.layout.rightSidebarWidth + keySteps[event.key]);
} else if (event.key === "Home") {
event.preventDefault();
setRightSidebarWidth(rightSidebarResizeBounds().min);
} else if (event.key === "End") {
event.preventDefault();
setRightSidebarWidth(rightSidebarResizeBounds().max);
}
});
}
function finishExplorerResize(event) {
const drag = state.layout.drag;
if (!drag || drag.pointerId !== event.pointerId) return;
state.layout.drag = null;
el.shell.classList.remove("is-resizing");
persistLayout();
}
function finishRightSidebarResize(event) {
const drag = state.layout.rightDrag;
if (!drag || drag.pointerId !== event.pointerId) return;
state.layout.rightDrag = null;
el.shell.classList.remove("is-resizing");
persistLayout();
}
function canResizeExplorer() {
return !window.matchMedia("(max-width: 860px)").matches && !el.shell.classList.contains("explorer-collapsed");
}
function canResizeRightSidebar() {
return !window.matchMedia("(max-width: 1240px)").matches;
}
function setExplorerCollapsed(collapsed) {
if (!collapsed) {
setExplorerWidth(state.layout.explorerWidth, { persist: false });
}
el.shell.classList.toggle("explorer-collapsed", collapsed);
el.explorerToggle.setAttribute("aria-pressed", collapsed ? "true" : "false");
el.explorerToggle.setAttribute("aria-expanded", collapsed ? "false" : "true");
el.explorerToggle.setAttribute("aria-label", collapsed ? "展开左侧资源树" : "收起左侧资源树");
el.explorerToggle.title = collapsed ? "展开左侧资源树,恢复导航和硬件资源" : "收起左侧资源树,给工作区更多宽度";
el.explorerToggle.textContent = collapsed ? "展开资源树" : "收起资源树";
const resizeDisabled = collapsed || window.matchMedia("(max-width: 860px)").matches;
el.explorerResize.tabIndex = resizeDisabled ? -1 : 0;
el.explorerResize.setAttribute("aria-disabled", resizeDisabled ? "true" : "false");
el.explorerResize.setAttribute("aria-hidden", resizeDisabled ? "true" : "false");
setRightSidebarWidth(state.layout.rightSidebarWidth, { persist: false });
}
function syncMobileExplorer() {
const media = window.matchMedia("(max-width: 860px)");
const apply = () => {
if (media.matches) {
setExplorerCollapsed(true);
} else {
setExplorerCollapsed(false);
setExplorerWidth(state.layout.explorerWidth, { persist: 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();
});
}
for (const button of document.querySelectorAll("[data-agent-quick-prompt]")) {
button.addEventListener("click", () => {
fillAgentQuickPrompt(button);
collapseExplorerAfterMobileAction();
});
}
}
function fillAgentQuickPrompt(button) {
const prompt = button.dataset.promptText ?? "";
if (!prompt) return;
el.commandInput.value = prompt;
el.commandInput.focus();
el.commandInput.setSelectionRange(prompt.length, prompt.length);
}
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 initHardwareTabs() {
for (const tab of document.querySelectorAll("[data-hardware-tab]")) {
tab.addEventListener("click", () => {
state.m3Control.hardwareTab = tab.dataset.hardwareTab;
syncHardwareTabs();
renderHardwareStatus(state.m3Control.status);
});
}
syncHardwareTabs();
}
function syncHardwareTabs() {
for (const tab of document.querySelectorAll("[data-hardware-tab]")) {
const active = tab.dataset.hardwareTab === state.m3Control.hardwareTab;
tab.classList.toggle("active", active);
tab.setAttribute("aria-selected", active ? "true" : "false");
}
}
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 后端,复杂问题可能需要 1-2 分钟;超时上限 ${Math.round(CODE_AGENT_TIMEOUT_MS / 1000)} 秒,失败时会保留输入供重试。`,
status: "running",
traceId,
createdAt: new Date().toISOString()
};
state.chatMessages.push(userMessage, pendingMessage);
state.chatPending = true;
el.commandInput.value = "";
renderAgentChatStatus("running");
renderCodeAgentSummary();
renderConversation();
renderDrafts();
renderRecords(state.liveSurface);
try {
const result = await sendAgentMessage(value, state.conversationId, traceId);
state.conversationId = result.conversationId || result.sessionId || state.conversationId;
state.sessionId = result.sessionId || state.sessionId || state.conversationId;
const index = state.chatMessages.findIndex((message) => message.id === pendingMessage.id);
const completion = classifyCodeAgentCompletion(result);
const status = completion.status;
const structuredBlockedError = structuredBlockedErrorFromResult(result);
state.chatMessages[index] = {
...pendingMessage,
title: completion.title,
text: completion.replied
? result.reply?.content || "Code Agent 没有返回文本。"
: structuredBlockedError
? failureMessage({ ...result, error: structuredBlockedError })
: 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,
workspace: result.workspace,
sandbox: result.sandbox,
session: result.session,
sessionMode: result.sessionMode,
sessionReuse: result.sessionReuse,
implementationType: result.implementationType,
runnerLimitations: result.runnerLimitations,
codexStdioFeasibility: result.codexStdioFeasibility,
longLivedSessionGate: result.longLivedSessionGate,
toolCalls: result.toolCalls,
skills: result.skills,
runner: result.runner,
runnerTrace: result.runnerTrace,
capabilityLevel: result.capabilityLevel,
sourceKind: completion.sourceKind,
providerTrace: result.providerTrace,
blocker: result.blocker,
blockers: result.blockers,
updatedAt: result.updatedAt,
error: structuredBlockedError ?? 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);
renderCodeAgentSummary();
} catch (error) {
const index = state.chatMessages.findIndex((message) => message.id === pendingMessage.id);
const presentation = agentFailurePresentation(error, { traceId });
state.chatMessages[index] = {
...pendingMessage,
title: presentation.title,
text: presentation.text,
status: "failed",
traceId: error.traceId || traceId,
updatedAt: new Date().toISOString(),
error: {
code: error.code || "request_failed",
category: presentation.category,
layer: error.layer,
blocker: error.blocker,
retryable: error.retryable,
userMessage: error.userMessage,
message: error.message,
timeoutMs: error.timeoutMs,
providerStatus: error.providerStatus,
missingConfig: error.missingConfig,
route: error.route,
toolName: error.toolName
}
};
el.commandInput.value = value;
renderAgentChatStatus("failed", state.chatMessages[index]);
renderCodeAgentSummary();
} finally {
state.chatPending = false;
renderCodeAgentSummary();
renderConversation();
renderDrafts();
renderRecords(state.liveSurface);
}
});
el.commandClear.addEventListener("click", () => {
state.chatMessages = [];
state.conversationId = null;
state.sessionId = null;
state.chatPending = false;
el.commandInput.value = "";
renderAgentChatStatus("idle");
renderCodeAgentSummary();
renderConversation();
renderDrafts();
renderRecords(state.liveSurface);
});
}
function initM3Control() {
el.m3ControlForm.addEventListener("submit", async (event) => {
event.preventDefault();
await runM3IoAction("do.write");
});
el.m3ReadDi.addEventListener("click", async () => {
await runM3IoAction("di.read");
});
}
function initGateControls() {
el.gateStatusFilter.addEventListener("change", renderGateTable);
el.gateSearch.addEventListener("input", renderGateTable);
el.gateRefresh.addEventListener("click", () => {
loadGateDiagnostics();
});
}
function renderStaticWorkbench() {
el.explorerStatus.textContent = "只读工作区";
el.explorerStatus.className = "status-dot tone-source";
el.routePath.textContent = "当前浏览器会话只保存任务草稿,硬件动作需通过受控后端流程。";
renderResourceTree();
renderCapabilityList();
renderConversation();
renderTaskList();
renderHardwareStatus(null);
renderDrafts();
renderM3ControlStatus();
renderWiringList(null);
renderRecords(null);
renderGateTable();
renderCodeAgentSummary();
}
function renderProbePending() {
el.liveStatus.textContent = "等待验证";
el.liveStatus.className = "tone-pending";
el.liveDetail.textContent = "正在验证 hwlab-cloud-api /health/live、/v1、/v1/agent/chat 与 /v1/m3/io;未完成前不标为通过。";
renderCodeAgentSummary();
}
async function sendAgentMessage(message, conversationId, traceId = nextProtocolId("trc")) {
const sessionId = state.conversationId === conversationId ? state.sessionId : undefined;
const response = await fetchJson("/v1/agent/chat", {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-Trace-Id": traceId
},
timeoutMs: CODE_AGENT_TIMEOUT_MS,
timeoutName: "Code Agent",
body: JSON.stringify({
message,
conversationId,
sessionId,
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 = agentErrorFromHttpResponse(response, traceId);
throw error;
}
return {
...response.data,
traceId: response.data?.traceId || traceId
};
}
async function loadLiveSurface() {
const projectId = gateSummary.topology.projectId;
const [healthLive, restIndex, m3Control, m3Status, health, adapter, audit, evidence] = await Promise.all([
fetchJson("/health/live"),
fetchJson("/v1"),
fetchJson("/v1/m3/io"),
fetchJson("/v1/m3/status"),
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,
m3Control,
m3Status,
health,
adapter,
audit,
evidence
};
}
async function loadGateDiagnostics() {
if (state.gateDiagnostics.loading) return;
state.gateDiagnostics.loading = true;
state.gateDiagnostics.error = null;
renderGateTable();
const response = await fetchJson("/v1/diagnostics/gate", {
timeoutMs: API_TIMEOUT_MS,
timeoutName: "内部复核 live 聚合"
});
state.gateDiagnostics.loading = false;
if (response.ok && Array.isArray(response.data?.rows)) {
state.gateDiagnostics.payload = response.data;
state.gateDiagnostics.rows = response.data.rows.map(normalizeGateRow);
state.gateDiagnostics.loadedAt = new Date().toISOString();
} else {
state.gateDiagnostics.payload = null;
state.gateDiagnostics.rows = [gateLiveBlockerRow(response)];
state.gateDiagnostics.error = response.error ?? "内部复核 live 后端聚合不可用";
state.gateDiagnostics.loadedAt = new Date().toISOString();
}
renderGateTable();
}
async function fetchJson(path, options = {}) {
const {
timeoutMs = API_TIMEOUT_MS,
timeoutName = path,
...fetchOptions
} = options;
const controller = new AbortController();
const timer = window.setTimeout(() => controller.abort(), timeoutMs);
try {
const response = await fetch(path, {
...fetchOptions,
headers: {
Accept: "application/json",
...(fetchOptions.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) {
const timedOut = error.name === "AbortError";
return {
ok: false,
path,
timeout: timedOut,
timeoutMs,
error: timedOut ? `${timeoutName} 请求超时 ${timeoutMs}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
};
}
async function runM3IoAction(action) {
if (state.m3Control.pending) return;
const traceId = nextProtocolId("trc");
if (!m3ControlCanOperate()) {
state.m3Control.operation = {
status: "blocked",
action,
traceId,
blocker: {
code: state.m3Control.contract?.readiness?.blocker?.code ?? "m3_control_readiness_blocked",
zh: m3ControlBlockedReason()
},
observedAt: new Date().toISOString(),
sourceKind: "BLOCKED"
};
renderM3ControlStatus();
renderRecords(state.liveSurface);
renderDrafts();
return;
}
state.m3Control.pending = true;
state.m3Control.operation = {
status: "running",
action,
traceId,
observedAt: new Date().toISOString()
};
renderM3ControlStatus();
renderDrafts();
renderRecords(state.liveSurface);
const body = action === "do.write"
? {
action,
gatewayId: el.m3GatewaySelect.value,
resourceId: el.m3BoxSelect.value,
boxId: "boxsimu_1",
port: el.m3PortSelect.value,
value: el.m3ValueSelect.value === "true",
projectId: gateSummary.topology.projectId,
traceId
}
: {
action,
gatewayId: "gwsimu_2",
resourceId: M3_TRUSTED_ROUTE.toResourceId,
boxId: "boxsimu_2",
port: M3_TRUSTED_ROUTE.toPort,
projectId: gateSummary.topology.projectId,
traceId
};
const response = await fetchJson("/v1/m3/io", {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-Trace-Id": traceId,
"X-Actor-Id": "usr_hwlab_cloud_web"
},
body: JSON.stringify(body)
});
if (!response.ok) {
state.m3Control.operation = {
status: "blocked",
action,
traceId,
blocker: {
zh: response.error || "M3 IO 控制请求失败"
},
observedAt: new Date().toISOString(),
sourceKind: "BLOCKED"
};
} else {
state.m3Control.operation = {
...response.data,
sourceKind: response.data?.evidenceState?.status === "green" ? "DEV-LIVE" : "BLOCKED"
};
}
const refreshedStatus = await fetchJson("/v1/m3/status");
if (refreshedStatus.ok) {
state.m3Control.status = refreshedStatus.data;
}
state.m3Control.pending = false;
renderM3ControlStatus();
renderHardwareStatus(state.m3Control.status);
renderWiringList(state.m3Control.status);
renderRecords(state.liveSurface);
renderDrafts();
}
function nextProtocolId(prefix) {
rpcSequence += 1;
return `${prefix}_cloud-web-workbench-${Date.now()}-${rpcSequence}`;
}
function renderLiveSurface(live) {
state.liveSurface = live;
state.m3Control.contract = live.m3Control?.ok ? live.m3Control.data : null;
state.m3Control.status = live.m3Status?.ok ? live.m3Status.data : null;
const coreProbes = [live.healthLive, live.restIndex, live.health, live.adapter];
const surfaceStatus = workbenchApiSurfaceStatus(live, coreProbes);
const runtimeSummary = runtimeSummaryFrom(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());
renderCodeAgentSummary();
renderHardwareStatus(state.m3Control.status);
renderWiringList(state.m3Control.status ?? runtimeSummary);
renderRecords(live);
renderM3ControlStatus();
renderConversation();
renderDrafts();
}
function workbenchApiSurfaceStatus(live, coreProbes = [live.healthLive, live.restIndex, live.health, live.adapter]) {
const status = classifyWorkbenchLiveStatus({
...live,
healthLive: live.healthLive ?? coreProbes[0],
restIndex: live.restIndex ?? coreProbes[1],
health: live.health ?? coreProbes[2],
adapter: live.adapter ?? coreProbes[3]
});
return {
...status,
methodTone: status.kind === "pass" ? "live" : status.kind
};
}
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: "在右侧两列接线表核对互连设备、DO/DI 对应关系与证据记录。",
tone: "source"
}
];
el.capabilityCount.textContent = String(capabilities.length);
replaceChildren(el.explorerCapabilities, ...capabilities.map(infoCard));
}
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: "打开右侧“接线”表,确认 res_boxsimu_1 与 res_boxsimu_2 的 DO/DI 对应关系,以及接线盘和轨迹/证据摘要。",
tone: "source"
},
{
title: "3. 留存记录",
detail: "在“可信记录”面板查看已有证据;新增硬件动作不从前端直接发起。",
tone: "source"
}
];
replaceChildren(el.taskList, ...rows.map(infoCard));
}
function renderHardwareStatus(m3Status) {
syncHardwareTabs();
const status = m3Status ?? sourceFallbackM3Status();
const sourceKind = status.sourceKind ?? "UNVERIFIED";
const trustedGreen = status.trust?.durableStatus === "green";
const statusTone = status.status === "live" && trustedGreen
? "dev-live"
: status.status === "error" || status.status === "blocked"
? "blocked"
: sourceKind === "DEV-LIVE"
? "dev-live"
: "source";
el.hardwareSourceStatus.textContent = hardwareStatusLabel(status);
el.hardwareSourceStatus.className = `state-tag tone-${toneClass(statusTone)}`;
const groups = {
overview: hardwareOverviewGroups(status),
gateways: hardwareGatewayGroups(status),
box1: hardwareBoxGroups(status, M3_TRUSTED_ROUTE.fromResourceId),
box2: hardwareBoxGroups(status, M3_TRUSTED_ROUTE.toResourceId),
patch: hardwarePatchPanelGroups(status)
};
// Tab bodies are compact Key/Value groups; SOURCE fallback stays unverified.
replaceChildren(el.hardwareList, ...(groups[state.m3Control.hardwareTab] ?? groups.overview));
}
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.badgeLabel ?? hardwareEvidenceLabel(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 hardwareStatusLabel(status) {
if (!status) return "未验证";
if (status.status === "live" && status.trust?.durableStatus === "green") return "实况可信";
if (status.status === "error") return "错误";
if (status.status === "blocked") return "实况受阻";
if (status.sourceKind === "DEV-LIVE") return "DEV-LIVE 状态";
return "未验证";
}
function sourceFallbackM3Status() {
const observedAt = new Date().toISOString();
return {
status: "unverified",
sourceKind: "SOURCE",
observedAt,
chain: {
sourceGatewayId: "gwsimu_1",
sourceGatewaySessionId: "gws_gwsimu_1",
sourceResourceId: M3_TRUSTED_ROUTE.fromResourceId,
sourceBoxId: "boxsimu_1",
sourcePort: M3_TRUSTED_ROUTE.fromPort,
targetGatewayId: "gwsimu_2",
targetGatewaySessionId: "gws_gwsimu_2",
targetResourceId: M3_TRUSTED_ROUTE.toResourceId,
targetBoxId: "boxsimu_2",
targetPort: M3_TRUSTED_ROUTE.toPort,
patchPanelServiceId: M3_TRUSTED_ROUTE.patchPanelServiceId
},
gateways: [
{
id: "gwsimu_1",
role: "source",
online: false,
sessionId: "gws_gwsimu_1",
sourceKind: "SOURCE",
lastSeenAt: observedAt,
lastError: "等待 cloud-api /v1/m3/status 聚合读取"
},
{
id: "gwsimu_2",
role: "target",
online: false,
sessionId: "gws_gwsimu_2",
sourceKind: "SOURCE",
lastSeenAt: observedAt,
lastError: "等待 cloud-api /v1/m3/status 聚合读取"
}
],
boxes: [
sourceFallbackBox("boxsimu_1", M3_TRUSTED_ROUTE.fromResourceId, "gwsimu_1", "gws_gwsimu_1", "DO1", "output", observedAt),
sourceFallbackBox("boxsimu_2", M3_TRUSTED_ROUTE.toResourceId, "gwsimu_2", "gws_gwsimu_2", "DI1", "input", observedAt)
],
patchPanel: {
serviceId: M3_TRUSTED_ROUTE.patchPanelServiceId,
connectionActive: false,
connectionVersion: null,
wiringConfigId: gateSummary.topology.patchPanel.wiringConfigId ?? null,
lastSyncAt: observedAt,
lastError: "SOURCE 拓扑 fallback,未验证 DEV-LIVE 接线盘状态",
sourceKind: "SOURCE"
},
trust: {
operationId: null,
traceId: null,
auditId: null,
evidenceId: null,
durableStatus: "blocked",
blocker: "unverified_cloud_api_m3_status"
},
blocker: {
code: "unverified_cloud_api_m3_status",
layer: "cloud-api",
zh: "等待 cloud-api /v1/m3/status 聚合读取;SOURCE 拓扑不能冒充实况硬件状态。"
}
};
}
function sourceFallbackBox(id, resourceId, gatewayId, gatewaySessionId, port, direction, observedAt) {
return {
id,
resourceId,
gatewayId,
gatewaySessionId,
online: false,
sourceKind: "SOURCE",
observedAt,
ports: {
[port]: {
value: null,
direction,
source: "SOURCE fallback",
sourceKind: "SOURCE",
observedAt,
lastError: "未验证"
}
}
};
}
function hardwareOverviewGroups(status) {
const box1 = findM3Box(status, M3_TRUSTED_ROUTE.fromResourceId);
const box2 = findM3Box(status, M3_TRUSTED_ROUTE.toResourceId);
const do1 = box1?.ports?.DO1;
const di1 = box2?.ports?.DI1;
const patch = status.patchPanel ?? {};
const trust = status.trust ?? {};
return [
hardwareKvGroup("总览", [
["状态", hardwareStatusLabel(status), statusTone(status)],
["观测时间", status.observedAt ?? "未验证", "source"],
["Gateway 在线", `${onlineCount(status.gateways)} / ${status.gateways?.length ?? 0}`, onlineCount(status.gateways) === 2 ? "live" : "blocked"],
["BOX 在线", `${onlineCount(status.boxes)} / ${status.boxes?.length ?? 0}`, onlineCount(status.boxes) === 2 ? "live" : "blocked"],
["链路", `${M3_TRUSTED_ROUTE.fromResourceId}:${M3_TRUSTED_ROUTE.fromPort} -> ${M3_TRUSTED_ROUTE.patchPanelServiceId} -> ${M3_TRUSTED_ROUTE.toResourceId}:${M3_TRUSTED_ROUTE.toPort}`, patch.connectionActive ? "live" : "blocked"],
["DO1", valueLabel(do1), portTone(do1)],
["DI1", valueLabel(di1), portTone(di1)],
["patchPanel connectionActive", String(Boolean(patch.connectionActive)), patch.connectionActive ? "live" : "blocked"],
["runtime durable", trust.durableStatus ?? "blocked", trust.durableStatus === "green" ? "dev-live" : "blocked"],
["blocker", trust.blocker ?? status.blocker?.code ?? "none", trust.blocker || status.blocker ? "blocked" : "source"]
]),
hardwareKvGroup("可信证据", [
["operationId", trust.operationId ?? "未持久化", trust.operationId ? "source" : "blocked"],
["traceId", trust.traceId ?? "未持久化", trust.traceId ? "source" : "blocked"],
["auditId", trust.auditId ?? "未持久化", trust.auditId ? "source" : "blocked"],
["evidenceId", trust.evidenceId ?? "未持久化", trust.evidenceId ? "source" : "blocked"]
])
];
}
function hardwareGatewayGroups(status) {
return (status.gateways ?? []).map((gateway) =>
hardwareKvGroup(`${gateway.id} / ${gateway.role}`, [
["online", String(Boolean(gateway.online)), gateway.online ? "live" : "blocked"],
["sessionId", gateway.sessionId ?? "未验证", gateway.sessionId ? "source" : "blocked"],
["lastSeenAt", gateway.lastSeenAt ?? status.observedAt ?? "未验证", gateway.online ? "live" : "source"],
["source", gateway.sourceKind ?? "UNVERIFIED", sourceKindTone(gateway.sourceKind)],
["lastError", gateway.lastError ?? "none", gateway.lastError ? "blocked" : "source"]
])
);
}
function hardwareBoxGroups(status, resourceId) {
const box = findM3Box(status, resourceId);
const portEntries = Object.entries(box?.ports ?? {});
return [
hardwareKvGroup(box ? `${box.id} / ${box.resourceId}` : resourceId, [
["online", String(Boolean(box?.online)), box?.online ? "live" : "blocked"],
["gateway", `${box?.gatewayId ?? "未验证"} / ${box?.gatewaySessionId ?? "未验证"}`, box?.gatewayId ? "source" : "blocked"],
["更新时间", box?.observedAt ?? status.observedAt ?? "未验证", sourceKindTone(box?.sourceKind)],
["来源", box?.sourceKind ?? "UNVERIFIED", sourceKindTone(box?.sourceKind)],
...portEntries.map(([port, portState]) => [port, valueLabel(portState), portTone(portState)]),
["AI1", "DISABLED 后续能力", "disabled"],
["AO1", "DISABLED 后续能力", "disabled"],
["FREQ", "DISABLED 后续能力", "disabled"]
])
];
}
function hardwarePatchPanelGroups(status) {
const patch = status.patchPanel ?? {};
return [
hardwareKvGroup("hwlab-patch-panel", [
["serviceId", patch.serviceId ?? M3_TRUSTED_ROUTE.patchPanelServiceId, "source"],
["connectionActive", String(Boolean(patch.connectionActive)), patch.connectionActive ? "live" : "blocked"],
["connectionVersion", patch.connectionVersion ?? "未验证", patch.connectionVersion !== null && patch.connectionVersion !== undefined ? "source" : "blocked"],
["wiringConfigId", patch.wiringConfigId ?? "未验证", patch.wiringConfigId ? "source" : "blocked"],
["lastSyncAt", patch.lastSyncAt ?? status.observedAt ?? "未验证", patch.connectionActive ? "live" : "source"],
["lastError", patch.lastError ?? "none", patch.lastError ? "blocked" : "source"],
["来源", patch.sourceKind ?? "UNVERIFIED", sourceKindTone(patch.sourceKind)]
])
];
}
function hardwareKvGroup(title, rows) {
const article = document.createElement("article");
article.className = "hardware-section hardware-kv-section";
article.append(textSpan(title, "hardware-section-title"));
const list = document.createElement("dl");
list.className = "kv-list";
for (const [key, value, tone = "source"] of rows) {
const row = document.createElement("div");
row.className = `kv-row tone-border-${toneClass(tone)}`;
row.append(textSpan(key, "kv-key"), textSpan(value, `kv-value tone-${toneClass(tone)}`));
list.append(row);
}
article.append(list);
return article;
}
function findM3Box(status, resourceId) {
return (status?.boxes ?? []).find((box) => box.resourceId === resourceId) ?? null;
}
function onlineCount(items = []) {
return items.filter((item) => item?.online === true).length;
}
function valueLabel(portState) {
if (!portState) return "未验证";
if (portState.value === null || portState.value === undefined) return portState.lastError ? `未验证:${portState.lastError}` : "未验证";
return String(portState.value);
}
function portTone(portState) {
if (!portState) return "blocked";
if (portState.sourceKind === "DEV-LIVE") return "live";
if (portState.sourceKind === "SOURCE") return "source";
return "blocked";
}
function statusTone(status) {
if (status.status === "live") return "dev-live";
if (status.status === "error" || status.status === "blocked") return "blocked";
if (status.sourceKind === "DEV-LIVE") return "live";
return "source";
}
function sourceKindTone(sourceKind) {
const normalized = String(sourceKind ?? "").toUpperCase();
if (normalized === "DEV-LIVE") return "live";
if (normalized === "SOURCE") return "source";
if (normalized === "BLOCKED") return "blocked";
return "blocked";
}
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 `实况链路:${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 `来源拓扑包含目标接线 ${sourceLink.fromResourceId}:${sourceLink.fromPort} -> ${patchPanel.serviceId} -> ${sourceLink.toResourceId}:${sourceLink.toPort},仍在等待完整可信记录。`;
}
if (counts) {
return "只读运行计数已返回,但还不能证明接线盘闭环。";
}
if (patchPanel.activeConnectionCount > 0) {
return `当前只看到非目标接线;目标链路 ${M3_TRUSTED_ROUTE.fromResourceId}:${M3_TRUSTED_ROUTE.fromPort} -> ${M3_TRUSTED_ROUTE.patchPanelServiceId} -> ${M3_TRUSTED_ROUTE.toResourceId}:${M3_TRUSTED_ROUTE.toPort} 仍在等待可信记录。`;
}
return "当前还没有 DO1 -> patch-panel -> DI1 的可信链路记录。";
}
function hardwareEvidenceLabel(sourceKind) {
const normalized = String(sourceKind ?? "").toUpperCase();
return (
{
SOURCE: "来源拓扑",
BLOCKED: "待可信记录",
"DEV-LIVE": "实况已验证",
"DRY-RUN": "演练记录"
}[normalized] ?? statusLabel(sourceKind)
);
}
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 m3ControlLiveEvidence() {
const operation = state.m3Control.operation;
if (!operation || operation.status !== "succeeded") return null;
return {
status: operation.evidenceState?.status === "green" ? "verified" : "blocked",
sourceKind: operation.evidenceState?.status === "green" ? "DEV-LIVE" : "BLOCKED",
live: operation.evidenceState?.status === "green",
operationId: operation.operationId,
traceId: operation.traceId,
auditId: operation.auditId,
evidenceId: operation.evidenceId,
patchPanelLiveReport: {
serviceId: M3_TRUSTED_ROUTE.patchPanelServiceId,
sourceKind: operation.evidenceState?.status === "green" ? "DEV-LIVE" : "BLOCKED",
live: operation.evidenceState?.status === "green",
reportId: operation.evidenceId,
activeConnections: [
{
fromResourceId: M3_TRUSTED_ROUTE.fromResourceId,
fromPort: M3_TRUSTED_ROUTE.fromPort,
toResourceId: M3_TRUSTED_ROUTE.toResourceId,
toPort: M3_TRUSTED_ROUTE.toPort,
patchPanelServiceId: M3_TRUSTED_ROUTE.patchPanelServiceId
}
]
}
};
}
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 renderM3ControlStatus() {
const contract = state.m3Control.contract;
const operation = state.m3Control.operation;
const controlReady = m3ControlCanOperate(contract);
const contractObserved = Boolean(contract);
const pending = state.m3Control.pending;
const operationSucceeded = operation?.status === "succeeded";
const operationBlocked = operation?.status === "blocked";
const label = pending
? "执行中"
: operationSucceeded
? operation.evidenceState?.status === "green"
? "控制可达 / 记录完整"
: "控制可达 / 等待记录"
: operationBlocked
? "操作待处理"
: controlReady
? "受控路径可执行"
: contractObserved
? "受控路径受阻"
: "等待探测";
const tone = pending
? "dry-run"
: operationSucceeded
? operation.evidenceState?.status === "green" ? "dev-live" : "degraded"
: operationBlocked || !controlReady
? "blocked"
: "source";
el.m3ControlStatus.textContent = label;
el.m3ControlStatus.className = `state-tag tone-${toneClass(tone)}`;
el.m3ControlStatus.title = m3ControlStatusTitle({ operation, contract, controlReady });
const disabled = pending || !controlReady;
for (const input of [el.m3GatewaySelect, el.m3BoxSelect, el.m3PortSelect, el.m3ValueSelect, el.m3WriteDo, el.m3ReadDi]) {
input.disabled = disabled;
}
el.m3WriteDo.textContent = pending ? "执行中" : "写入 DO1";
}
function m3ControlCanOperate(contract = state.m3Control.contract) {
return contract?.status === "available" &&
contract?.readiness?.status === "ready" &&
contract?.readiness?.controlReady === true &&
contract?.readiness?.sourceKind === "DEV-LIVE" &&
contract?.readiness?.evidenceLevel === "DEV-LIVE";
}
function m3ControlStatusTitle({ operation, contract, controlReady }) {
if (operation?.blocker?.zh) return operation.blocker.zh;
if (operation?.evidenceState?.reason) return operation.evidenceState.reason;
if (controlReady) {
return "M3 IO 只读 readiness 已确认;控制仅通过 cloud-api 受控路径执行,不直连 gateway/box-simu。";
}
return m3ControlBlockedReason(contract);
}
function m3ControlBlockedReason(contract = state.m3Control.contract) {
if (!contract) return "等待 cloud-api 返回 M3 IO 只读 readiness;未确认前控制保持阻塞。";
return contract.readiness?.blocker?.zh ??
contract.blockedReason ??
"M3 IO 只读 readiness 未 green;控制面板保持阻塞,不直连 gateway/box-simu。";
}
function controlRows() {
const operation = state.m3Control.operation;
const controlReady = m3ControlCanOperate();
const operationCards = operation
? [
{
title: `M3 ${operationActionLabel(operation.action)} ${statusLabel(operation.status)}`,
detail: m3OperationDetail(operation),
tone: operation.status === "succeeded"
? operation.evidenceState?.status === "green" ? "dev-live" : "degraded"
: operation.status === "running"
? "dry-run"
: "blocked"
}
]
: [];
return [
{
title: "控制路径",
detail: controlReady
? "按钮只走受控后端,再通过 gateway-simu -> box-simu -> hwlab-patch-panel 执行 M3 DO/DI。"
: `${m3ControlBlockedReason()} 不可用时页面不直连模拟器,也不伪造硬件状态。`,
tone: controlReady ? "source" : "blocked"
},
...operationCards,
{
title: "读取目标",
detail: `${M3_TRUSTED_ROUTE.toResourceId}:${M3_TRUSTED_ROUTE.toPort} 只能通过受控路径读取;浏览器不直连 box-simu。`,
tone: "source"
},
{
title: "Code Agent 对话",
detail: codeAgentControlSummary(state.codeAgentAvailability),
tone: currentConversationTone()
},
{
title: "只读探测",
detail: "内部复核探测保持只读,并放在二级页面。",
tone: "source"
}
];
}
function operationActionLabel(action) {
return action === "do.write" ? "DO 写入" : action === "di.read" ? "DI 读取" : "操作";
}
function m3OperationDetail(operation) {
if (operation.status === "running") {
return `trace=${operation.traceId} / 正在通过 cloud-api 提交。`;
}
const ids = [
recordField("operation", operation.operationId),
recordField("trace", operation.traceId),
recordField("audit", operation.auditId),
recordField("evidence", operation.evidenceId)
].filter(Boolean).join(" / ");
const value = operation.result?.value !== undefined && operation.result?.value !== null
? ` / value=${String(operation.result.value)}`
: "";
const blocker = operation.blocker?.zh || operation.blocker?.message
? ` / 阻塞原因=${operation.blocker.zh || operation.blocker.message}`
: "";
const evidence = operation.evidenceState?.reason ? ` / evidence=${operation.evidenceState.reason}` : "";
return `${ids}${value}${blocker}${evidence}`;
}
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 presentation = wiringPresentationFromSummary(summary, {
liveM3Evidence: trustedM3LiveEvidenceFrom(summary)
});
const sourceHeader = byId("wiring-source-device");
const targetHeader = byId("wiring-target-device");
if (sourceHeader) sourceHeader.textContent = M3_TRUSTED_ROUTE.fromResourceId;
if (targetHeader) targetHeader.textContent = M3_TRUSTED_ROUTE.toResourceId;
const tr = document.createElement("tr");
tr.dataset.sourceKind = presentation.rowSourceKind;
tr.dataset.wiringRow = "io";
const sourceCell = cell("", "");
sourceCell.append(textSpan(M3_TRUSTED_ROUTE.fromPort, "wiring-port mono"));
sourceCell.append(textSpan(`源端口;经 ${M3_TRUSTED_ROUTE.patchPanelServiceId};状态:${presentation.stateLabel}`, "wiring-detail"));
const targetCell = cell("", "");
targetCell.append(textSpan(M3_TRUSTED_ROUTE.toPort, "wiring-port mono"));
targetCell.append(textSpan(`目标端口;证据来源:${presentation.sourceLabel};轨迹/证据:${presentation.traceEvidence}`, "wiring-detail mono wrap"));
tr.append(sourceCell, targetCell);
replaceChildren(el.wiringBody, tr);
const summaryElement = byId("wiring-summary");
if (summaryElement) {
replaceChildren(
summaryElement,
document.createTextNode(`接线盘:${M3_TRUSTED_ROUTE.patchPanelServiceId};状态:`),
badge(presentation.stateLabel, presentation.badgeTone),
document.createTextNode(`;证据来源:${presentation.sourceLabel};轨迹/证据:${presentation.traceEvidence}。`)
);
}
}
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 ? `失败原因=${safeFailureReason(message.error.message)}` : null
].filter(Boolean).join(" / "),
tone: message.status === "failed" ? "blocked" : "source"
})
);
}
function operationRecordCards() {
const wiring = gateSummary.topology.patchPanel;
const controlOperation = state.m3Control.operation;
const controlCards = controlOperation
? [
infoCard({
title: controlOperation.operationId ?? controlOperation.traceId ?? "m3-control-operation",
detail: [
sourceKindLabel(controlOperation.evidenceState?.status === "green" ? "dev-live" : "blocked_after_cloud_api"),
"via=/v1/m3/io",
recordField("action", controlOperation.action),
recordField("trace", controlOperation.traceId),
recordField("audit", controlOperation.auditId),
recordField("evidence", controlOperation.evidenceId),
controlOperation.controlPath?.frontendBypass === false ? "frontendBypass=false" : null,
controlOperation.evidenceState?.reason
].filter(Boolean).join(" / "),
tone: controlOperation.status === "succeeded"
? controlOperation.evidenceState?.status === "green" ? "dev-live" : "degraded"
: "blocked"
})
]
: [];
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 [...controlCards, ...wiringCards, ...operationCards];
}
function auditRecordCards(live) {
const cards = [];
if (state.m3Control.operation?.auditEvent) {
cards.push(auditCard(
state.m3Control.operation.auditEvent,
state.m3Control.operation.evidenceState?.status === "green" ? "dev-live" : "blocked",
state.m3Control.operation.evidenceState?.status === "green" ? null : state.m3Control.operation.evidenceState?.reason
));
}
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 (state.m3Control.operation?.evidenceRecord) {
cards.push(evidenceCard(
state.m3Control.operation.evidenceRecord,
state.m3Control.operation.evidenceState?.status === "green" ? "dev-live" : "blocked",
state.m3Control.operation.evidenceState?.status === "green" ? null : state.m3Control.operation.evidenceState?.reason
));
}
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}`;
}
const sensitiveFailureReasonNeedles = Object.freeze([
["OPENAI", "API", "KEY"].join("_"),
["hwlab", "code", "agent", "provider"].join("-"),
["Se", "cret"].join(""),
["secret", "Ref"].join("")
]);
function safeFailureReason(value) {
const text = String(value ?? "").trim();
if (!text) return "未知错误";
const lowerText = text.toLowerCase();
if (sensitiveFailureReasonNeedles.some((needle) => lowerText.includes(needle.toLowerCase()))) {
return "服务暂不可用";
}
return text.length > 120 ? `${text.slice(0, 117)}...` : text;
}
function sourceKindLabel(tone) {
return statusLabel(tone);
}
function renderGateTable() {
const rows = filteredGateRows();
const loading = state.gateDiagnostics.loading;
const payload = state.gateDiagnostics.payload;
const statusTone = gatePayloadTone();
const loadedAt = state.gateDiagnostics.loadedAt;
el.gateSourceStatus.textContent = loading
? "读取 live 后端中"
: payload
? `live 后端 ${rows.length}/${state.gateDiagnostics.rows.length} 行`
: state.gateDiagnostics.error
? "live 后端阻塞"
: "等待 live 后端";
el.gateSourceStatus.className = `state-tag tone-${toneClass(statusTone)}`;
el.gateSourceStatus.title = payload
? `route=${payload.route}; observedAt=${payload.observedAt ?? loadedAt ?? "unknown"}`
: state.gateDiagnostics.error ?? "等待 /v1/diagnostics/gate";
if (loading && state.gateDiagnostics.rows.length === 0) {
const tr = document.createElement("tr");
tr.append(tableCell("正在读取 live 后端聚合。", "wrap", 8));
replaceChildren(el.gateReviewBody, tr);
return;
}
if (rows.length === 0) {
const tr = document.createElement("tr");
tr.append(tableCell("没有匹配当前过滤条件的复核行。", "wrap", 8));
replaceChildren(el.gateReviewBody, tr);
return;
}
replaceChildren(el.gateReviewBody, ...rows.map(gateTableRow));
}
function filteredGateRows() {
const filter = el.gateStatusFilter.value;
const query = el.gateSearch.value.trim().toLowerCase();
return state.gateDiagnostics.rows.filter((row) => {
if (filter !== "all" && row.status !== filter) return false;
if (!query) return true;
return [
row.category,
row.check,
row.status,
row.owner,
row.detail,
...(row.evidence ?? []),
row.updatedAt,
row.next
].join(" ").toLowerCase().includes(query);
});
}
function gateTableRow(row) {
const tr = document.createElement("tr");
tr.dataset.status = row.status;
tr.dataset.statusKey = row.statusKey;
tr.append(tableCell(row.category, "wrap"));
tr.append(tableCell(row.check, "wrap"));
const status = document.createElement("td");
status.append(badge(row.status, row.statusKey));
tr.append(status);
tr.append(tableCell(row.owner, "mono wrap"));
tr.append(tableCell(row.detail, "wrap"));
tr.append(tableCell((row.evidence ?? []).join(" / "), "mono wrap"));
tr.append(tableCell(formatTimestamp(row.updatedAt), "mono wrap"));
tr.append(tableCell(row.next, "wrap"));
return tr;
}
function normalizeGateRow(row) {
const statusKey = normalizeGateStatusKey(row.statusKey ?? row.status);
return {
category: oneLine(row.category || "内部复核"),
check: oneLine(row.check || "未命名检查项"),
status: gateStatusLabel(row.status, statusKey),
statusKey,
owner: oneLine(row.owner || "未标明"),
detail: oneLine(row.detail || "live 后端未返回关键细节"),
evidence: normalizeEvidenceList(row.evidence),
updatedAt: row.updatedAt || new Date().toISOString(),
next: oneLine(row.next || "等待维护者补充下一步。"),
sourceKind: "LIVE-BACKEND"
};
}
function gateLiveBlockerRow(response) {
const now = new Date().toISOString();
return normalizeGateRow({
category: "健康检查",
check: "内部复核 live 后端聚合",
status: "阻塞",
statusKey: response.timeout ? "failed" : "blocked",
owner: "hwlab-cloud-api /v1/diagnostics/gate",
detail: response.error || "live 后端聚合不可用;页面不会回退到 SOURCE 静态拓扑。",
evidence: [
"GET /v1/diagnostics/gate",
response.status ? `HTTP ${response.status}` : null,
response.timeout ? `timeoutMs=${response.timeoutMs}` : null
].filter(Boolean),
updatedAt: now,
next: "先恢复后端聚合接口,再刷新本表。"
});
}
function gatePayloadTone() {
if (state.gateDiagnostics.loading) return "pending";
if (!state.gateDiagnostics.payload) return state.gateDiagnostics.error ? "blocked" : "source";
const rows = state.gateDiagnostics.rows;
if (rows.some((row) => row.statusKey === "failed")) return "failed";
if (rows.some((row) => row.statusKey === "blocked")) return "blocked";
if (rows.some((row) => row.statusKey === "pending")) return "pending";
return "pass";
}
function renderAgentChatStatus(status, result = null) {
const labels = {
idle: "等待输入",
running: "处理中",
completed: "DEV-LIVE 回复",
source: "SOURCE 回复",
failed: "发送失败",
blocked: "服务受阻"
};
el.agentChatStatus.textContent = agentStatusLabel(status, result, labels);
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 renderCodeAgentSummary() {
const summary = currentCodeAgentStatusSummary();
el.codeAgentSummary.className = `code-agent-summary tone-border-${toneClass(summary.tone)}`;
el.codeAgentSummary.dataset.codeAgentStatusKind = summary.kind;
el.codeAgentSummaryIcon.textContent = summary.icon;
el.codeAgentSummaryIcon.className = `code-agent-summary-icon tone-${toneClass(summary.tone)}`;
el.codeAgentSummaryLabel.textContent = summary.label;
el.codeAgentSummaryLabel.className = `code-agent-summary-label tone-${toneClass(summary.tone)}`;
el.codeAgentSummaryCapability.textContent = summary.capabilityLevel;
el.codeAgentSummaryTrace.textContent = `trace ${summary.lastTraceId}`;
replaceChildren(el.codeAgentSummaryDetail, ...codeAgentSummaryRows(summary));
}
function currentCodeAgentStatusSummary() {
return classifyCodeAgentStatusSummary({
availability: state.codeAgentAvailability,
latestMessage: latestChatResult(),
live: state.liveSurface
});
}
function codeAgentSummaryRows(summary) {
return [
codeAgentSummaryRow("codeAgent.status", summary.codeAgentStatus, summary.tone),
codeAgentSummaryRow("provider/mode/backend", summary.providerModeBackend, "source"),
codeAgentSummaryRow("capabilityLevel", summary.capabilityLevel, summary.tone),
codeAgentSummaryRow("session", summary.sessionSummary, sessionSummaryTone(summary)),
codeAgentSummaryRow("readiness blockers", summary.blockersLabel, summary.readinessBlockers.length > 0 ? "blocked" : "source"),
codeAgentSummaryRow("last traceId", summary.lastTraceId, "source"),
codeAgentSummaryRow("当前部署 revision", summary.deploymentRevision, "source")
];
}
function codeAgentSummaryRow(label, value, tone) {
const row = document.createElement("div");
row.className = "code-agent-summary-row";
row.append(textSpan(label, "code-agent-summary-key"), textSpan(value, `code-agent-summary-value tone-${toneClass(tone)}`));
return row;
}
function sessionSummaryTone(summary) {
if (summary.kind === "long-lived-session") return "ok";
if (summary.kind === "blocked" || summary.kind === "skill-cli-api-blocked") return "blocked";
if (["read-only-session-tools", "stateless-one-shot", "fallback-text-chat-only", "degraded"].includes(summary.kind)) return "warn";
return "source";
}
function agentStatusLabel(status, result, labels) {
if (status !== "failed") return labels[status] ?? statusLabel(status);
const presentation = agentFailurePresentation(result?.error, { result });
return (
{
timeout: "等待超时",
provider: "Provider 不可用",
runner_busy: "Runner 忙碌",
session_blocked: "Session 受阻",
runner_blocked: "Runner 受阻",
api_error: "API 错误",
needs_config: "需要配置",
capability_unavailable: "能力未开放",
security_blocked: "安全阻断",
fallback: "仍是 fallback",
retryable: "可重试"
}[presentation.category] ?? labels.failed
);
}
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 textFallbackTitle(result) {
return `Code Agent 文本 fallback 回复 ${shortTime(result.updatedAt ?? new Date().toISOString())}`;
}
function classifyCodeAgentCompletion(result) {
if (isStructuredBlockedChatResult(result)) {
return {
status: "failed",
replied: false,
sourceKind: "BLOCKED",
title: agentFailurePresentation(structuredBlockedErrorFromResult(result), { result }).title
};
}
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)
};
}
if (isTextFallbackChatResult(result)) {
return {
status: "source",
replied: true,
sourceKind: "TEXT-FALLBACK",
title: textFallbackTitle(result)
};
}
return {
status: "failed",
replied: false,
sourceKind: "BLOCKED",
title: result?.status === "completed" && !result?.blocker ? "Code Agent 完成证据不足" : agentFailurePresentation(result?.error, { result }).title
};
}
function isStructuredBlockedChatResult(result) {
const error = result?.error && typeof result.error === "object" ? result.error : {};
return Boolean(
result &&
typeof result === "object" &&
(
result.blocker?.code ||
error.blocker?.code ||
result.capabilityLevel === "hwlab-api-control-blocked"
)
);
}
function structuredBlockedErrorFromResult(result) {
if (!isStructuredBlockedChatResult(result)) return null;
const error = result?.error && typeof result.error === "object" ? result.error : {};
const blocker = error.blocker ?? result?.blocker ?? {};
const code = normalizeErrorCode(error.code ?? blocker.code ?? "code_agent_blocked");
return {
...error,
code,
layer: error.layer ?? blocker.layer,
category: error.category ?? blocker.category,
blocker,
retryable: error.retryable ?? blocker.retryable,
userMessage: error.userMessage ?? blocker.userMessage,
message: error.message ?? blocker.summary ?? code,
traceId: error.traceId ?? result?.traceId,
route: error.route ?? blocker.route,
toolName: error.toolName ?? blocker.toolName
};
}
function failureMessage(result) {
const presentation = agentFailurePresentation(structuredBlockedErrorFromResult(result) ?? result?.error, { result });
if (presentation.category !== "unknown") {
return presentation.text;
}
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);
return codeAgentBlockedSummary(availability);
}
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 agentErrorFromHttpResponse(response, traceId) {
const payloadError = response.data?.error;
const code = response.timeout
? "client_timeout"
: normalizeErrorCode(
(payloadError && typeof payloadError === "object" ? payloadError.code : null) ??
(typeof payloadError === "string" ? payloadError : null) ??
response.data?.code ??
"request_failed"
);
const error = new Error(response.timeout
? `Code Agent 请求超过 ${response.timeoutMs}ms 未完成;已保留输入,可稍后重试。`
: payloadError?.userMessage || response.error || "Code Agent 请求失败");
error.traceId = response.data?.traceId || traceId;
error.code = code;
error.layer = payloadError && typeof payloadError === "object" ? payloadError.layer : undefined;
error.blocker = payloadError && typeof payloadError === "object" ? payloadError.blocker : undefined;
error.retryable = payloadError && typeof payloadError === "object" ? payloadError.retryable : response.timeout ? true : undefined;
error.userMessage = payloadError && typeof payloadError === "object" ? payloadError.userMessage : undefined;
error.missingConfig = payloadError && typeof payloadError === "object" ? payloadError.missingConfig : undefined;
error.route = payloadError && typeof payloadError === "object" ? payloadError.route : undefined;
error.toolName = payloadError && typeof payloadError === "object" ? payloadError.toolName : undefined;
error.timeoutMs = response.timeoutMs ?? (payloadError && typeof payloadError === "object" ? payloadError.timeoutMs : undefined);
error.providerStatus = payloadError && typeof payloadError === "object" ? payloadError.providerStatus : response.status;
error.category = payloadError && typeof payloadError === "object" ? payloadError.category : undefined;
return error;
}
function agentFailurePresentation(error, { result = null, traceId = null } = {}) {
const code = normalizeErrorCode(error?.code);
const message = String(error?.message ?? result?.error?.message ?? "").trim();
const structuredBlocker = error?.blocker ?? result?.error?.blocker ?? result?.blocker ?? null;
const userMessage = String(error?.userMessage ?? result?.error?.userMessage ?? structuredBlocker?.userMessage ?? "").trim();
const observedTraceId = result?.traceId || error?.traceId || traceId || null;
const traceSuffix = observedTraceId ? ` trace=${observedTraceId}` : "";
const timeoutMs = error?.timeoutMs ?? result?.error?.timeoutMs ?? timeoutMsFromText(message);
const providerStatus = error?.providerStatus ?? result?.error?.providerStatus;
if (isTimeoutFailure(code, message)) {
return {
category: "timeout",
title: "Code Agent 等待超时",
text: `Code Agent 请求${timeoutMs ? `超过 ${timeoutMs}ms ` : ""}未完成;输入已保留,可稍后重试或缩小问题范围。${traceSuffix}`
};
}
if (userMessage) {
const category = error?.category ?? result?.error?.category ?? structuredBlocker?.category ?? "runner_blocked";
return {
category,
title: titleForBlockerCategory(category, code),
text: `${userMessage}${traceSuffix}`
};
}
if (code === "session_busy") {
return {
category: "runner_busy",
title: "Code Agent Runner 忙碌",
text: `受控只读 runner 正在处理上一轮请求;输入已保留,稍后重试即可。${traceSuffix}`
};
}
if (["session_expired", "session_reuse_conflict"].includes(code)) {
return {
category: "session_blocked",
title: "Code Agent Session 受阻",
text: `当前 session 已过期或与 conversation 绑定不一致;输入已保留,可重新发送建立新的受控只读 session。${traceSuffix}`
};
}
if (["runner_unavailable", "tool_unavailable", "skills_unavailable", "security_blocked"].includes(code)) {
return {
category: "runner_blocked",
title: "Code Agent Runner 受阻",
text: `受控只读 runner 返回 ${code};本次不会 fallback 冒充 Codex session。${message ? `原因:${message}。` : ""}${traceSuffix}`
};
}
if (["invalid_params", "parse_error", "request_failed", "cloud_api_proxy_failed", "upstream_unavailable"].includes(code)) {
return {
category: "api_error",
title: "Code Agent API 错误",
text: `Code Agent API 请求失败;输入已保留,可稍后重试。${message ? `原因:${message}。` : ""}${traceSuffix}`
};
}
if (code === "provider_unavailable" || providerStatus) {
return {
category: "provider",
title: "Code Agent Provider 不可用",
text: providerStatus
? `Code Agent provider 返回 HTTP ${providerStatus};本次未生成回复,输入已保留,可稍后重试。${traceSuffix}`
: `Code Agent provider 暂不可用;本次未生成回复,输入已保留,可稍后重试。${traceSuffix}`
};
}
return {
category: "unknown",
title: "Code Agent 返回失败",
text: `Code Agent 调用失败:${message || "未知错误"}。请稍后重试或联系维护者补齐后端 provider。${traceSuffix}`
};
}
function titleForBlockerCategory(category, code) {
const value = String(category ?? "").trim();
if (value === "needs_config") return "Code Agent 需要配置";
if (value === "security_blocked" || code === "security_blocked") return "Code Agent 安全阻断";
if (value === "fallback" || code === "text_chat_only_fallback") return "Code Agent 仍是 fallback";
if (value === "timeout") return "Code Agent 等待超时";
if (value === "runner_busy") return "Code Agent Runner 忙碌";
if (value === "session_blocked") return "Code Agent Session 受阻";
if (value === "capability_unavailable") return "Code Agent 能力未开放";
if (value === "retryable") return "Code Agent 可重试";
return "Code Agent 服务受阻";
}
function normalizeErrorCode(code) {
const normalized = String(code ?? "").trim();
return normalized || "code_agent_failed";
}
function isTimeoutFailure(code, message) {
return ["client_timeout", "proxy_timeout", "cloud_api_proxy_timeout"].includes(code) ||
/\b(?:timeout|timed out|abort|aborted|超时|请求超过)\b/iu.test(message);
}
function timeoutMsFromText(message) {
const match = String(message ?? "").match(/(?:timed out after|timeout after|请求超时|请求超过)\s*(\d+)\s*ms/iu);
if (!match) return null;
const value = Number.parseInt(match[1], 10);
return Number.isInteger(value) && value > 0 ? value : null;
}
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 capabilityFacts = codeAgentCapabilityFactsPanel(message);
if (capabilityFacts) article.append(capabilityFacts);
const messageEvidence = messageEvidencePanel(message);
if (messageEvidence) article.append(messageEvidence);
return article;
}
function codeAgentCapabilityFactsPanel(message) {
const facts = codeAgentFactsFromMessage(message);
if (!facts) return null;
const section = document.createElement("section");
section.className = `code-agent-facts tone-border-${toneClass(facts.tone)}`;
section.dataset.codeAgentFactKind = facts.kind;
section.append(codeAgentFactsHeader(facts));
const grid = document.createElement("dl");
grid.className = "code-agent-fact-grid";
for (const row of facts.rows) {
const item = document.createElement("div");
item.className = "code-agent-fact-row";
item.append(textSpan(row.label, "code-agent-fact-key"), textSpan(row.value ?? "none", "code-agent-fact-value mono"));
grid.append(item);
}
section.append(grid);
if (facts.hwlabApiFacts.length > 0) {
const apiList = document.createElement("div");
apiList.className = "hwlab-api-facts";
apiList.append(textSpan("HWLAB API", "hwlab-api-facts-title"));
for (const fact of facts.hwlabApiFacts) {
const code = document.createElement("code");
code.className = "hwlab-api-fact-code";
code.textContent = compactHwlabApiFact(fact);
apiList.append(code);
}
section.append(apiList);
}
return section;
}
function codeAgentFactsHeader(facts) {
const header = document.createElement("div");
header.className = "code-agent-facts-head";
header.append(badge(facts.statusLabel, facts.tone));
header.append(textSpan(facts.summary, "code-agent-facts-summary"));
return header;
}
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("runner", message.runner?.kind),
recordField("workspace", message.workspace),
recordField("sandbox", message.sandbox),
recordField("capability", message.capabilityLevel),
recordField("session", sessionSummary(message.session)),
recordField("sessionMode", message.sessionMode),
recordField("sessionReuse", sessionReuseSummary(message.sessionReuse)),
recordField("implementation", message.implementationType),
recordField("limitations", limitationsSummary(message.runnerLimitations)),
recordField("codexStdio", codexStdioFeasibilitySummary(message.codexStdioFeasibility)),
recordField("longLivedGate", longLivedSessionGateSummary(message.longLivedSessionGate)),
recordField("toolCalls", toolCallsSummary(message.toolCalls)),
recordField("skills", skillsSummary(message.skills)),
recordField("runnerTrace", runnerTraceSummary(message.runnerTrace)),
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 toolCallsSummary(toolCalls) {
const summary = compactToolCallsSummary(toolCalls);
return summary === "none" ? null : summary;
}
function skillsSummary(skills) {
const summary = compactSkillsSummary(skills);
return summary === "none" ? null : summary;
}
function runnerTraceSummary(runnerTrace) {
const summary = compactRunnerTraceSummary(runnerTrace);
return summary === "none" ? null : summary;
}
function sessionReuseSummary(sessionReuse) {
if (!sessionReuse || typeof sessionReuse !== "object") return null;
const state = sessionReuse.reused ? "reused" : "new";
const status = sessionReuse.status ? `:${sessionReuse.status}` : "";
return `${state}:turn${sessionReuse.turn ?? "?"}${status}`;
}
function sessionSummary(session) {
if (!session || typeof session !== "object") return null;
const parts = [
session.sessionId,
session.status,
typeof session.turn === "number" ? `turn${session.turn}` : null,
session.lastTraceId ? `lastTrace=${session.lastTraceId}` : null
].filter(Boolean);
return parts.join(":");
}
function limitationsSummary(limitations) {
if (!Array.isArray(limitations) || limitations.length === 0) return null;
return limitations.slice(0, 3).join(",");
}
function longLivedSessionGateSummary(gate) {
if (!gate || typeof gate !== "object") return null;
const status = gate.status ?? "unknown";
const blockers = Array.isArray(gate.blockers)
? gate.blockers.map((blocker) => blocker?.code).filter(Boolean).slice(0, 2).join(",")
: "";
return blockers ? `${status}:${blockers}` : status;
}
function codexStdioFeasibilitySummary(feasibility) {
if (!feasibility || typeof feasibility !== "object") return null;
const blockers = Array.isArray(feasibility.blockers) ? feasibility.blockers.length : 0;
return `${feasibility.status ?? "unknown"}:${blockers}`;
}
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 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 formatTimestamp(value) {
const date = new Date(value);
if (Number.isNaN(date.getTime())) return "未知";
return shortTime(date.toISOString());
}
function tableCell(text, className, colspan = 1) {
const td = cell(text, className);
if (colspan > 1) td.colSpan = colspan;
return td;
}
function runtimeSummaryFrom(live) {
return live.health.data?.runtime ?? live.healthLive.data?.runtime ?? live.adapter.data?.runtime ?? live.restIndex.data?.runtime ?? null;
}
function messageFromPayload(payload) {
if (!payload || typeof payload !== "object") return "非 JSON 或空响应";
if (payload.error && typeof payload.error === "object") {
return payload.error.message || payload.error.code || payload.reason || payload.message || JSON.stringify(payload.error).slice(0, 160);
}
if (typeof payload.error === "string") {
return payload.message ? `${payload.error}${payload.message}` : payload.error;
}
return payload.code || payload.message || payload.reason || JSON.stringify(payload).slice(0, 160);
}
function normalizeBlockedAgentResult(payload, httpStatus, traceId, fallbackMessage) {
const { reply, ...safePayload } = payload;
const payloadError = payload.error && typeof payload.error === "object" ? payload.error : {};
const payloadErrorCode = payloadError.code ?? (typeof payload.error === "string" ? payload.error : null);
const providerStatus = payloadError.providerStatus ?? httpStatus;
const error = {
...payloadError,
code: payloadErrorCode ?? "provider_unavailable",
message: payloadError.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.error?.code === "security_blocked" ||
payload.error?.code === "tool_unavailable" ||
payload.error?.code === "skills_unavailable" ||
payload.availability?.status === "blocked"
)
);
}
function isHttpNon2xxStatus(value) {
return typeof value === "number" && Number.isInteger(value) && (value < 200 || value > 299);
}
function codeAgentAvailabilityFrom(live) {
return sanitizeCodeAgentAvailability(codeAgentAvailabilityFromLive(live));
}
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 sanitizeCodeAgentAvailability(availability) {
if (!availability || typeof availability !== "object") return availability;
if (availability.status !== "blocked") return availability;
return {
...availability,
blocker: "服务受阻",
summary: codeAgentBlockedSummary(availability)
};
}
function codeAgentStatusMessage(availability) {
if (availability?.longLivedSessionGate?.status === "pass" && availability?.codexStdioFeasibility?.canStartLongLivedCodexStdio === true) {
return {
role: "system",
title: "Code Agent 状态:Codex stdio 长会话可用",
text: "当前通道可创建和复用 repo-owned Codex stdio session,并暴露 cancel、reap、idle timeout 与 trace capture;硬件控制仍只走 cloud-api/HWLAB API/skill CLI。",
status: "source"
};
}
if (availability?.runner?.ready === true) {
return {
role: "system",
title: "Code Agent 状态:只读 session registry 可用,Codex stdio 受阻",
text: `pwd、skills、ls、rg --files 和 cat 会走 controlled-readonly-session-registry;它不是完整 Code Agent。${codeAgentBlockerDetail(availability)}`,
status: "source"
};
}
if (availability?.status === "blocked") {
return {
role: "system",
title: "Code Agent 状态:服务受阻",
text: codeAgentBlockedSummary(availability),
status: "blocked"
};
}
if (String(availability?.sourceKind ?? availability?.evidenceLevel ?? "").toUpperCase() === "SOURCE") {
return {
role: "system",
title: "Code Agent 状态:本地验证记录",
text: "当前只用于本地浏览器验证:可以检查发送、处理中、回复渲染和证据展示,但不会标记为实况完成。",
status: "source"
};
}
if (availability?.status === "available") {
return {
role: "system",
title: "Code Agent 状态:真实通道已接入",
text: "受控对话通道已接入;发送后只有真实完成的回复才会标记为已完成。",
status: "source"
};
}
return {
role: "system",
title: "Code Agent 状态:等待只读探测",
text: "正在确认服务状态;首屏不会宣称 Code Agent 可用,也不会把失败或静态内容当成真实回复。",
status: "source"
};
}
function codeAgentPromptText(availability) {
if (availability?.status === "blocked") {
return "可以继续输入并保留草稿;发送会走受控对话通道。服务恢复前会显示结构化失败,不会冒充真实 Codex 回复。";
}
return "请在底部输入中文消息并点击发送;完成态只来自真实完成的后端回复。";
}
function codeAgentControlSummary(availability) {
if (latestCompletedAgentMessage()) {
return state.conversationId
? `当前会话 ${state.conversationId};最近一次真实完成回复来自受控 Code Agent 接口,并带有可核验记录。`
: "最近一次真实完成回复来自受控 Code Agent 接口,并带有可核验记录。";
}
if (isSourceFixtureCompletedChatMessage(latestChatResult())) {
return "最近一次为本地验证回复;它只证明浏览器对话体验,不作为实况完成证据。";
}
if (availability?.status === "blocked") {
return codeAgentBlockedSummary(availability);
}
if (availability?.partialReady === true || availability?.runner?.ready === true) {
return `输入区会调用受控接口;当前只有只读 registry 或 text fallback,不能标成完整 Code Agent。${codeAgentBlockerDetail(availability)}`;
}
return "输入区会调用受控 Code Agent 接口;只有真实完成回复才显示为完成,不能因为只有会话编号就当成实况完成。";
}
function codeAgentBlockedSummary(availability) {
const structured = availability?.error?.userMessage ?? availability?.blocker?.userMessage;
if (structured) return structured;
const providerStatus = availability?.error?.providerStatus ?? availability?.providerStatus;
if (providerStatus) {
return `真实后端已接入,但当前上游响应 HTTP ${providerStatus};工作台保持只读和可重试,不会冒充真实 Code Agent 回复。`;
}
const blocker = codeAgentBlockerDetail(availability);
return `真实后端已接入,但 Code Agent 服务暂不可用;完整 Codex stdio Code Agent 仍受阻,工作台保持只读和可重试,不会冒充真实回复。${blocker}`;
}
function codeAgentBlockerDetail(availability) {
const blockers = Array.isArray(availability?.blockers)
? availability.blockers.map((blocker) => blocker?.code).filter(Boolean)
: Array.isArray(availability?.codexStdioFeasibility?.blockers)
? availability.codexStdioFeasibility.blockers.map((blocker) => blocker?.code).filter(Boolean)
: [];
if (blockers.length === 0 && availability?.reason) blockers.push(availability.reason);
return blockers.length > 0 ? ` blocker=${blockers.slice(0, 3).join(",")}。` : "";
}
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?.longLivedSessionGate?.status === "pass" && state.codeAgentAvailability?.codexStdioFeasibility?.canStartLongLivedCodexStdio === true) {
return "同源 API 可响应;Codex stdio 长会话 gate 已通过,支持真实 session 复用和 trace capture。";
}
if (state.codeAgentAvailability?.runner?.ready === true) {
return `同源 API 可响应;pwd/skills/ls/rg/cat 可走受控只读 session registry,但它不是 Codex stdio、不可写、非持久 session。${codeAgentBlockerDetail(state.codeAgentAvailability)}`;
}
if (state.codeAgentAvailability?.status === "blocked") {
return "同源 API 可响应;Code Agent 服务暂不可用,工作台保持只读和可重试。";
}
if (String(state.codeAgentAvailability?.sourceKind ?? state.codeAgentAvailability?.evidenceLevel ?? "").toUpperCase() === "SOURCE") {
return "同源 API 可响应;当前 Code Agent 回复来源是本地验证记录,仅用于浏览器验证,不是实况完成。";
}
if (state.codeAgentAvailability?.status === "available") {
return "同源 API 可响应;Code Agent 真实通道已接入,只有真实完成回复才显示为完成。";
}
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 isTextFallbackChatResult(result) {
const assistantReply = typeof result?.reply?.content === "string" ? result.reply.content.trim() : "";
return result?.status === "completed" &&
assistantReply.length > 0 &&
result?.provider === "openai-responses" &&
(result?.capabilityLevel === "text-chat-only" || result?.runner?.kind === "openai-responses-fallback") &&
result?.longLivedSessionGate?.status !== "pass";
}
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) {
if (isCodexRunnerCapableEvidence(value)) {
return true;
}
if (isReadOnlyRunnerPartialEvidence(value) || isTextFallbackChatResult(value)) {
return false;
}
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 isCodexRunnerCapableEvidence(value) {
return (
CODEX_RUNNER_CAPABLE_PROVIDERS.includes(String(value?.provider ?? "").trim().toLowerCase()) &&
value?.runner?.kind === "codex-mcp-stdio-runner" &&
value?.capabilityLevel === "long-lived-codex-stdio-session" &&
value?.session?.status === "idle" &&
typeof value?.session?.idleTimeoutMs === "number" &&
Boolean(value?.session?.lastTraceId) &&
value?.sessionMode === "codex-mcp-stdio-long-lived" &&
value?.implementationType === "repo-owned-codex-mcp-stdio-session" &&
Boolean(value?.sessionReuse) &&
value?.session?.longLivedSession === true &&
value?.session?.codexStdio === true &&
value?.runner?.codexStdio === true &&
value?.runner?.writeCapable === true &&
value?.runner?.durableSession === true &&
Boolean(value?.workspace || value?.runner?.workspace) &&
(value?.sandbox || value?.runner?.sandbox) === "workspace-write" &&
Array.isArray(value?.toolCalls) &&
value.toolCalls.some((tool) => tool?.status === "completed") &&
value?.longLivedSessionGate?.status === "pass" &&
Boolean(value?.runnerTrace) &&
Boolean(value?.skills)
);
}
function isReadOnlyRunnerPartialEvidence(value) {
return (
CODEX_READONLY_PARTIAL_PROVIDERS.includes(String(value?.provider ?? "").trim().toLowerCase()) &&
value?.runner?.kind === "hwlab-readonly-runner" &&
(value?.capabilityLevel === "read-only-session-tools" || value?.capabilityLevel === "read-only-tools") &&
value?.sessionMode === "controlled-readonly-session-registry" &&
value?.implementationType === "controlled-readonly-session-registry" &&
value?.runner?.codexStdio === false &&
value?.runner?.writeCapable === false &&
value?.runner?.durableSession === false &&
value?.longLivedSessionGate?.status === "blocked"
);
}
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 gateStatusLabel(value, statusKey) {
const explicit = String(value ?? "").trim();
const labels = {
pass: "通过",
blocked: "阻塞",
failed: "失败",
pending: "待验证",
info: "信息"
};
return labels[statusKey] ?? (["通过", "阻塞", "失败", "待验证", "信息"].includes(explicit) ? explicit : "阻塞");
}
function normalizeGateStatusKey(value) {
const text = String(value ?? "").trim().toLowerCase();
if (["通过", "pass", "passed", "ok", "ready"].includes(text)) return "pass";
if (["失败", "failed", "failure", "error"].includes(text)) return "failed";
if (["待验证", "pending", "loading", "unknown"].includes(text)) return "pending";
if (["信息", "info", "information"].includes(text)) return "info";
return "blocked";
}
function normalizeEvidenceList(value) {
const items = Array.isArray(value) ? value : [value];
const normalized = items.map(oneLine).filter(Boolean);
return normalized.length > 0 ? normalized : ["live 后端未返回 evidence 字段"];
}
function oneLine(value) {
return String(value ?? "").replace(/\s+/gu, " ").trim().slice(0, 240);
}
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 recordKindLabel(kind) {
return (
{
audit: "审计",
evidence: "证据",
operation: "操作",
record: "记录",
report: "报告",
trace: "轨迹"
}[String(kind ?? "").toLowerCase()] ?? statusLabel(kind)
);
}
function internalGatePathnames() {
return new Set(["/gate", "/diagnostics/gate"]);
}
function helpPathnames() {
return new Set(["/help"]);
}
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 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"));
}