Files
pikasTech-HWLAB/web/hwlab-cloud-web/app.mjs
T

4973 lines
193 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 { ensureWorkbenchAuth, initWorkbenchLogout } from "./auth.mjs";
import {
codeAgentRuntimePathFromMessage,
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";
import {
extractCodeAgentM3Evidence,
isCodeAgentM3SkillCompletion,
m3EvidenceRows,
m3EvidenceSummaryText
} from "./code-agent-m3-evidence.mjs";
const DEFAULT_API_TIMEOUT_MS = 4500;
const DEFAULT_CODE_AGENT_TIMEOUT_MS = 600000;
const DEFAULT_CODE_AGENT_SUBMIT_TIMEOUT_MS = 60000;
const DEFAULT_CODE_AGENT_CANCEL_TIMEOUT_MS = 30000;
const CODE_AGENT_TIMEOUT_STORAGE_KEY = "hwlab.workbench.codeAgentTimeoutMs.v1";
const TRACE_STREAM_FALLBACK_MS = 2500;
const TRACE_POLL_INTERVAL_MS = 1000;
const API_TIMEOUT_MS = resolveTimeoutMs("apiTimeoutMs", DEFAULT_API_TIMEOUT_MS, { min: 500, max: 30000 });
let CODE_AGENT_TIMEOUT_MS = resolveUserCodeAgentTimeoutMs();
const CODE_AGENT_SUBMIT_TIMEOUT_MS = resolveTimeoutMs("codeAgentSubmitTimeoutMs", DEFAULT_CODE_AGENT_SUBMIT_TIMEOUT_MS, { min: 5000, max: 120000 });
const CODE_AGENT_CANCEL_TIMEOUT_MS = resolveTimeoutMs("codeAgentCancelTimeoutMs", DEFAULT_CODE_AGENT_CANCEL_TIMEOUT_MS, { min: 5000, max: 120000 });
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: "待处理",
canceled: "已取消",
completed: "完成",
connected: "已连接",
degraded: "降级",
disabled: "禁用",
entry: "入口",
failed: "失败",
healthy: "健康",
live: "实况",
ok: "正常",
open: "未关闭",
pass: "通过",
pending: "等待",
running: "处理中",
timeout: "超时",
error: "错误",
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(["codex-stdio"]);
const CODEX_RUNNER_CAPABLE_PROVIDERS = Object.freeze(["codex-stdio"]);
const CODEX_APP_SERVER_RUNNER_KIND = "codex-app-server-stdio-runner";
const CODEX_APP_SERVER_SESSION_MODE = "codex-app-server-stdio-long-lived";
const CODEX_APP_SERVER_IMPLEMENTATION_TYPE = "repo-owned-codex-app-server-stdio-session";
const CODEX_APP_SERVER_PROTOCOL = "codex-app-server-jsonrpc-stdio";
const CODEX_STDIO_BLOCKER_MARKERS = Object.freeze([
"codex_cli_binary_missing",
"runner_lifecycle_missing",
"stdio_protocol_not_wired",
"codex_stdio_supervisor_disabled",
"not-codex-stdio",
"not-write-capable",
"process-local-session-registry"
]);
const UNTRUSTED_CODE_AGENT_PROVIDER_PATTERN = /\b(?:echo|mock|fixture|stub|sample)\b/iu;
const LAYOUT_STORAGE_KEY = "hwlab.workbench.layout.v1";
const LEFT_SIDEBAR_DEFAULT_WIDTH = 92;
const LEFT_SIDEBAR_MIN_WIDTH = 72;
const LEFT_SIDEBAR_MAX_WIDTH = 180;
const RIGHT_SIDEBAR_DEFAULT_WIDTH = 728;
const RIGHT_SIDEBAR_MIN_WIDTH = 560;
const RIGHT_SIDEBAR_MAX_WIDTH = 740;
const viewIds = new Set([...document.querySelectorAll("[data-view]")].map((view) => view.dataset.view));
let rpcSequence = 0;
const el = {
loginShell: byId("login-shell"),
shell: query("[data-app-shell]"),
logoutButton: byId("logout-button"),
leftSidebarToggle: byId("left-sidebar-toggle"),
leftSidebarResize: byId("left-sidebar-resize"),
rightSidebarResize: byId("right-sidebar-resize"),
routePath: byId("route-path"),
liveStatus: byId("live-status"),
liveDetail: byId("live-detail"),
liveBuildSummary: byId("live-build-summary"),
liveBuildToggle: byId("live-build-toggle"),
liveBuildLatest: byId("live-build-latest"),
liveBuildList: byId("live-build-list"),
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"),
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"),
codeAgentTimeout: byId("code-agent-timeout"),
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"),
m3FlowState: byId("m3-flow-state"),
m3ActionSummary: byId("m3-action-summary"),
m3Do1Target: byId("m3-do1-target"),
m3Di1Observed: byId("m3-di1-observed"),
m3TrustSummary: byId("m3-trust-summary"),
m3TraceSummary: byId("m3-trace-summary"),
m3RecordsJump: byId("m3-records-jump"),
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")
};
let activeWorkbenchDialog = null;
await ensureWorkbenchAuth({
loginShell: el.loginShell,
appShell: el.shell
});
const state = {
conversationId: null,
sessionId: null,
threadId: null,
codeAgentAvailability: null,
chatMessages: [],
traceStreams: new Map(),
traceDetailsOpen: new Map(),
canceledTraces: new Set(),
currentRequest: null,
sessionStatus: null,
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: {
leftSidebarCollapsed: false,
leftSidebarWidth: LEFT_SIDEBAR_DEFAULT_WIDTH,
leftDrag: null,
rightSidebarWidth: RIGHT_SIDEBAR_DEFAULT_WIDTH,
rightDrag: null
}
};
initRoutes();
initLayoutSizing();
initLeftSidebarToggle();
initLeftSidebarResize();
initRightSidebarResize();
initSideTabs();
initHardwareTabs();
initQuickActions();
initCodeAgentTimeoutControl();
initCommandBar();
initLiveBuildOverlay();
initWorkbenchLogout(el.logoutButton);
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 clampTimeoutMs(value, { min, max }) {
if (value === null || value === undefined) return null;
if (typeof value === "string" && value.trim() === "") return null;
const numeric = Number(value);
if (!Number.isFinite(numeric)) return null;
return Math.min(Math.max(Math.trunc(numeric), min), max);
}
function resolveUserCodeAgentTimeoutMs() {
let stored = null;
try {
stored = window.localStorage?.getItem(CODE_AGENT_TIMEOUT_STORAGE_KEY) ?? null;
} catch {
stored = null;
}
return clampTimeoutMs(stored, { min: 30000, max: 1200000 })
?? resolveTimeoutMs("codeAgentTimeoutMs", DEFAULT_CODE_AGENT_TIMEOUT_MS, { min: 30000, max: 1200000 });
}
function initCodeAgentTimeoutControl() {
syncCodeAgentTimeoutControl();
el.codeAgentTimeout.addEventListener("change", () => {
const next = clampTimeoutMs(el.codeAgentTimeout.value, { min: 30000, max: 1200000 }) ?? DEFAULT_CODE_AGENT_TIMEOUT_MS;
CODE_AGENT_TIMEOUT_MS = next;
try {
window.localStorage?.setItem(CODE_AGENT_TIMEOUT_STORAGE_KEY, String(next));
} catch {
// localStorage can be unavailable in hardened browser modes.
}
syncCodeAgentTimeoutControl();
renderCodeAgentSummary();
renderConversation();
});
}
function syncCodeAgentTimeoutControl() {
el.codeAgentTimeout.value = String(CODE_AGENT_TIMEOUT_MS);
el.codeAgentTimeout.title = `Code Agent 无新事件超过 ${formatTraceDuration(CODE_AGENT_TIMEOUT_MS)} 后才显示为超时`;
}
function initLiveBuildOverlay() {
el.liveBuildSummary.open = false;
el.liveBuildToggle.addEventListener("click", (event) => {
event.preventDefault();
el.liveBuildSummary.open = false;
openWorkbenchDialog({
title: "构建版本明细",
body: cloneLiveBuildList(),
restoreFocus: el.liveBuildToggle
});
});
}
function cloneLiveBuildList() {
const clone = el.liveBuildList.cloneNode(true);
clone.id = "";
clone.hidden = false;
clone.removeAttribute("hidden");
clone.tabIndex = 0;
clone.classList.remove("live-build-list-template");
return clone;
}
function openWorkbenchDialog({ title, body, restoreFocus = null }) {
closeWorkbenchDialog({ restoreFocus: false });
const layer = document.createElement("div");
layer.className = "workbench-dialog-layer";
const backdrop = document.createElement("button");
backdrop.type = "button";
backdrop.className = "workbench-dialog-backdrop";
backdrop.setAttribute("aria-label", "关闭弹窗");
const dialog = document.createElement("section");
dialog.className = "workbench-dialog";
dialog.setAttribute("role", "dialog");
dialog.setAttribute("aria-modal", "true");
dialog.setAttribute("aria-label", title);
const head = document.createElement("div");
head.className = "workbench-dialog-head";
const heading = document.createElement("strong");
heading.textContent = title;
const close = document.createElement("button");
close.type = "button";
close.className = "workbench-dialog-close";
close.textContent = "关闭";
close.setAttribute("aria-label", `关闭${title}`);
const content = document.createElement("div");
content.className = "workbench-dialog-content";
if (body) {
body.hidden = false;
body.removeAttribute("hidden");
content.append(body);
}
head.append(heading, close);
dialog.append(head, content);
layer.append(backdrop, dialog);
const closeDialog = () => closeWorkbenchDialog({ restoreFocus: true });
const keydown = (event) => {
if (event.key !== "Escape") return;
event.preventDefault();
closeDialog();
};
close.addEventListener("click", closeDialog);
backdrop.addEventListener("click", closeDialog);
document.addEventListener("keydown", keydown);
document.body.append(layer);
activeWorkbenchDialog = { layer, keydown, restoreFocus };
close.focus();
}
function closeWorkbenchDialog({ restoreFocus = true } = {}) {
if (!activeWorkbenchDialog) return;
const { layer, keydown, restoreFocus: target } = activeWorkbenchDialog;
document.removeEventListener("keydown", keydown);
layer.remove();
activeWorkbenchDialog = null;
if (restoreFocus && target && typeof target.focus === "function") {
target.focus();
}
}
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 leftSidebarWidth = clampLeftSidebarWidth(persisted?.leftSidebarWidth ?? LEFT_SIDEBAR_DEFAULT_WIDTH);
const rightSidebarWidth = clampRightSidebarWidth(persisted?.rightSidebarWidth ?? RIGHT_SIDEBAR_DEFAULT_WIDTH);
setLeftSidebarWidth(leftSidebarWidth, { persist: false });
setLeftSidebarCollapsed(persisted?.leftSidebarCollapsed === true, { 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 {
leftSidebarCollapsed: payload.leftSidebarCollapsed === true,
leftSidebarWidth: Number(payload.leftSidebarWidth),
rightSidebarWidth: Number(payload.rightSidebarWidth)
};
} catch {
return null;
}
}
function persistLayout() {
try {
window.localStorage?.setItem(
LAYOUT_STORAGE_KEY,
JSON.stringify({
version: 1,
leftSidebarCollapsed: state.layout.leftSidebarCollapsed,
leftSidebarWidth: state.layout.leftSidebarWidth,
rightSidebarWidth: state.layout.rightSidebarWidth
})
);
} catch {
// Browser storage can be unavailable; layout still works for the session.
}
}
function initLeftSidebarToggle() {
el.leftSidebarToggle.addEventListener("click", () => {
setLeftSidebarCollapsed(!state.layout.leftSidebarCollapsed);
});
}
function setLeftSidebarCollapsed(collapsed, options = {}) {
state.layout.leftSidebarCollapsed = collapsed === true;
el.shell.classList.toggle("is-left-sidebar-collapsed", state.layout.leftSidebarCollapsed);
el.leftSidebarToggle.setAttribute("aria-expanded", state.layout.leftSidebarCollapsed ? "false" : "true");
el.leftSidebarToggle.setAttribute(
"aria-label",
state.layout.leftSidebarCollapsed ? "展开左侧导航" : "折叠左侧导航"
);
el.leftSidebarToggle.title = state.layout.leftSidebarCollapsed ? "展开左侧导航" : "折叠左侧导航";
el.leftSidebarToggle.textContent = state.layout.leftSidebarCollapsed ? "展开" : "收起";
syncLeftSidebarResizeA11y();
if (options.persist !== false) persistLayout();
}
function setLeftSidebarWidth(width, options = {}) {
const clamped = clampLeftSidebarWidth(width);
state.layout.leftSidebarWidth = clamped;
el.shell.style.setProperty("--left-sidebar-width", `${clamped}px`);
syncLeftSidebarResizeA11y();
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 clampLeftSidebarWidth(value) {
const numeric = Number(value);
const width = Number.isFinite(numeric) ? numeric : LEFT_SIDEBAR_DEFAULT_WIDTH;
const bounds = leftSidebarResizeBounds();
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 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 leftSidebarResizeBounds() {
const styles = getComputedStyle(el.shell);
const cssMin = readCssPixel(styles.getPropertyValue("--left-sidebar-min-width"), LEFT_SIDEBAR_MIN_WIDTH);
const cssMax = readCssPixel(styles.getPropertyValue("--left-sidebar-max-width"), LEFT_SIDEBAR_MAX_WIDTH);
if (window.matchMedia("(max-width: 1240px)").matches) {
const fixedRailWidth = readCssPixel(styles.getPropertyValue("--rail-width"), LEFT_SIDEBAR_DEFAULT_WIDTH);
return { min: Math.round(fixedRailWidth), max: Math.round(fixedRailWidth) };
}
const rightWidth = state.layout.rightSidebarWidth;
const dynamicMax = window.innerWidth - rightWidth - 420 - 4;
return boundedResizeRange(cssMin, cssMax, dynamicMax);
}
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 = state.layout.leftSidebarCollapsed
? readCssPixel(styles.getPropertyValue("--rail-collapsed-width"), LEFT_SIDEBAR_DEFAULT_WIDTH)
: state.layout.leftSidebarWidth;
const dynamicMax = window.innerWidth - railWidth - 420 - 4;
return boundedResizeRange(cssMin, cssMax, dynamicMax);
}
function readCssPixel(value, fallback) {
const parsed = Number.parseFloat(String(value ?? "").trim());
return Number.isFinite(parsed) ? parsed : fallback;
}
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 syncLeftSidebarResizeA11y() {
const bounds = leftSidebarResizeBounds();
const disabled = !canResizeLeftSidebar();
el.leftSidebarResize.tabIndex = disabled ? -1 : 0;
el.leftSidebarResize.setAttribute("aria-disabled", disabled ? "true" : "false");
el.leftSidebarResize.setAttribute("aria-hidden", disabled ? "true" : "false");
el.leftSidebarResize.setAttribute("aria-valuemin", String(bounds.min));
el.leftSidebarResize.setAttribute("aria-valuemax", String(bounds.max));
el.leftSidebarResize.setAttribute("aria-valuenow", String(state.layout.leftSidebarWidth));
el.leftSidebarResize.setAttribute("aria-valuetext", `左侧导航宽度 ${state.layout.leftSidebarWidth} 像素`);
}
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 initLeftSidebarResize() {
window.addEventListener("resize", () => {
if (canResizeLeftSidebar()) setLeftSidebarWidth(state.layout.leftSidebarWidth, { persist: false });
syncLeftSidebarResizeA11y();
});
el.leftSidebarResize.addEventListener("pointerdown", (event) => {
if (!canResizeLeftSidebar()) return;
event.preventDefault();
el.leftSidebarResize.setPointerCapture?.(event.pointerId);
state.layout.leftDrag = {
pointerId: event.pointerId,
startX: event.clientX,
startWidth: state.layout.leftSidebarWidth
};
el.shell.classList.add("is-resizing");
});
el.leftSidebarResize.addEventListener("pointermove", (event) => {
const drag = state.layout.leftDrag;
if (!drag || drag.pointerId !== event.pointerId) return;
setLeftSidebarWidth(drag.startWidth + event.clientX - drag.startX, { persist: false });
});
el.leftSidebarResize.addEventListener("pointerup", finishLeftSidebarResize);
el.leftSidebarResize.addEventListener("pointercancel", finishLeftSidebarResize);
el.leftSidebarResize.addEventListener("keydown", (event) => {
if (!canResizeLeftSidebar()) return;
const keySteps = {
ArrowLeft: -8,
ArrowRight: 8,
PageUp: 24,
PageDown: -24
};
if (Object.hasOwn(keySteps, event.key)) {
event.preventDefault();
setLeftSidebarWidth(state.layout.leftSidebarWidth + keySteps[event.key]);
} else if (event.key === "Home") {
event.preventDefault();
setLeftSidebarWidth(leftSidebarResizeBounds().min);
} else if (event.key === "End") {
event.preventDefault();
setLeftSidebarWidth(leftSidebarResizeBounds().max);
}
});
}
function finishLeftSidebarResize(event) {
const drag = state.layout.leftDrag;
if (!drag || drag.pointerId !== event.pointerId) return;
state.layout.leftDrag = null;
el.shell.classList.remove("is-resizing");
persistLayout();
}
function canResizeLeftSidebar() {
return !state.layout.leftSidebarCollapsed && !window.matchMedia("(max-width: 1240px)").matches;
}
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 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 canResizeRightSidebar() {
return !window.matchMedia("(max-width: 1240px)").matches;
}
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));
}
el.m3RecordsJump.addEventListener("click", () => {
selectSideTab("records");
});
}
function initQuickActions() {
for (const button of document.querySelectorAll("[data-agent-quick-prompt]")) {
button.addEventListener("click", () => {
fillAgentQuickPrompt(button);
});
}
}
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 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();
await submitAgentMessage(value);
});
el.commandClear.addEventListener("click", () => {
for (const close of state.traceStreams.values()) close();
state.traceStreams.clear();
state.traceDetailsOpen.clear();
state.chatMessages = [];
state.conversationId = null;
state.sessionId = null;
state.threadId = null;
state.sessionStatus = null;
state.currentRequest = null;
state.canceledTraces.clear();
state.chatPending = false;
el.commandInput.value = "";
renderAgentChatStatus("idle");
renderCodeAgentSummary();
renderConversation();
renderDrafts();
renderRecords(state.liveSurface);
});
}
async function submitAgentMessage(value, options = {}) {
if (!value || state.chatPending) return;
const traceId = options.traceId ?? nextProtocolId("trc");
const activeConversationId = options.conversationId ?? state.conversationId ?? nextProtocolId("cnv");
const requestedSessionId = options.sessionId === undefined ? sessionIdForNextRequest() : options.sessionId;
const requestedThreadId = options.threadId === undefined ? threadIdForNextRequest() : options.threadId;
const pendingContinuity = pendingSessionContinuity({
conversationId: activeConversationId,
sessionId: requestedSessionId,
threadId: requestedThreadId,
retryOf: options.retryOf
});
state.conversationId = activeConversationId;
const userMessage = {
id: nextProtocolId("msg"),
role: "user",
title: `${options.retryOf ? "重试" : "用户"} ${shortTime(new Date().toISOString())}`,
text: value,
status: "sent",
traceId,
conversationId: activeConversationId,
sessionId: requestedSessionId,
threadId: requestedThreadId,
retryOf: options.retryOf ?? null,
createdAt: new Date().toISOString()
};
const pendingMessage = {
id: nextProtocolId("msg"),
role: "agent",
title: options.retryOf ? "Code Agent 重试中" : "Code Agent 处理中",
text: `正在处理这次 Code Agent 请求;复杂问题可能需要几分钟。页面会保留 trace/session 并等待后端返回成功、结构化 blocker 或真实超时;不会把旧 4500ms 轻量探测窗口当作最终失败。`,
status: "running",
traceId,
conversationId: activeConversationId,
sessionId: requestedSessionId,
threadId: requestedThreadId,
sessionContinuity: pendingContinuity,
retryInput: value,
retryOf: options.retryOf ?? null,
sourceKind: "PENDING",
createdAt: new Date().toISOString()
};
state.chatMessages.push(userMessage, pendingMessage);
state.chatPending = true;
state.currentRequest = {
traceId,
conversationId: activeConversationId,
sessionId: requestedSessionId,
threadId: requestedThreadId,
messageId: pendingMessage.id,
input: value,
lastActivityAt: Date.now(),
lastActivityIso: new Date().toISOString(),
traceEventCount: 0,
waitingFor: "agent/chat",
lastEventLabel: "request:submitted"
};
el.commandInput.value = "";
renderAgentChatStatus("running");
renderCodeAgentSummary();
renderConversation();
renderDrafts();
renderRecords(state.liveSurface);
const stopTraceStream = subscribeRunnerTrace(traceId, pendingMessage.id);
try {
const result = await sendAgentMessage(value, activeConversationId, traceId, requestedSessionId, requestedThreadId);
stopTraceStream();
if (state.canceledTraces.has(traceId)) return;
state.conversationId = result.conversationId || result.sessionId || state.conversationId;
state.sessionId = result.sessionId || result.session?.sessionId || state.sessionId || state.conversationId;
const resultThreadId = threadIdFrom(result);
if (resultThreadId) state.threadId = resultThreadId;
state.sessionStatus = result.sessionLifecycleStatus ?? result.sessionSummary?.status ?? result.session?.lifecycleStatus ?? result.session?.status ?? result.runnerTrace?.sessionLifecycleStatus ?? result.runnerTrace?.sessionStatus ?? result.status ?? state.sessionStatus;
const index = state.chatMessages.findIndex((message) => message.id === pendingMessage.id);
const sessionContinuity = completedSessionContinuity(result, pendingContinuity);
const resultBlockedError = structuredBlockedErrorFromResult(result);
const continuityBlocker = codeAgentContinuityBlocker(result, sessionContinuity);
const blockedError = resultBlockedError ?? continuityBlocker;
const completion = classifyCodeAgentCompletion(result, { blockedError });
const status = completion.status;
const m3Presentation = m3IoPresentation(result);
state.chatMessages[index] = {
...pendingMessage,
title: m3Presentation?.title ?? completion.title,
text: m3Presentation?.text ?? (completion.replied
? result.reply?.content || "Code Agent 没有返回文本。"
: blockedError
? failureMessage({ ...result, error: blockedError })
: result.status === "completed"
? untrustedCompletionMessage(result)
: failureMessage(result)),
status,
traceId: result.traceId,
conversationId: result.conversationId || result.sessionId || state.conversationId,
sessionId: result.sessionId || result.session?.sessionId || result.sessionReuse?.sessionId || state.sessionId,
threadId: resultThreadId || requestedThreadId || state.threadId,
sessionContinuity,
messageId: result.messageId,
provider: result.provider,
model: result.model,
backend: result.backend,
workspace: result.workspace,
sandbox: result.sandbox,
session: result.session,
sessionLifecycleStatus: result.sessionLifecycleStatus,
sessionLifecycle: result.sessionLifecycle,
sessionSummary: result.sessionSummary,
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,
conversationFacts: result.conversationFacts,
responseType: result.responseType,
m3Io: result.m3Io,
m3Evidence: extractCodeAgentM3Evidence(result),
capabilityLevel: result.capabilityLevel,
sourceKind: completion.sourceKind,
providerTrace: result.providerTrace,
blocker: result.blocker,
blockers: result.blockers,
updatedAt: result.updatedAt,
retryInput: value,
retryOf: options.retryOf ?? null,
error: blockedError ?? 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, state.chatMessages[index]);
renderCodeAgentSummary();
} catch (error) {
stopTraceStream();
if (state.canceledTraces.has(traceId)) return;
const index = state.chatMessages.findIndex((message) => message.id === pendingMessage.id);
const presentation = agentFailurePresentation(error, { traceId });
const failedStatus = errorStatusFromPresentation(presentation, error);
state.chatMessages[index] = {
...pendingMessage,
title: presentation.title,
text: presentation.text,
status: failedStatus,
traceId: error.traceId || traceId,
threadId: requestedThreadId,
sessionContinuity: failedSessionContinuity(pendingContinuity, error),
updatedAt: new Date().toISOString(),
retryInput: value,
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,
hardTimeoutMs: error.hardTimeoutMs,
idleMs: error.idleMs,
lastActivityAt: error.lastActivityAt,
waitingFor: error.waitingFor,
providerStatus: error.providerStatus,
missingConfig: error.missingConfig,
route: error.route,
toolName: error.toolName
},
runnerTrace: latestTraceSnapshot(traceId)
};
state.sessionStatus = state.chatMessages[index].runnerTrace?.sessionStatus ?? failedStatus;
el.commandInput.value = value;
renderAgentChatStatus(failedStatus, state.chatMessages[index]);
renderCodeAgentSummary();
} finally {
if (state.currentRequest?.traceId === traceId) {
state.currentRequest = null;
state.chatPending = false;
} else if (!state.currentRequest) {
state.chatPending = false;
}
stopTraceStream();
renderCodeAgentSummary();
renderConversation();
renderDrafts();
renderRecords(state.liveSurface);
}
}
function sessionIdForNextRequest() {
return isTerminalSessionStatus(state.sessionStatus) ? undefined : state.sessionId;
}
function threadIdForNextRequest() {
return isTerminalSessionStatus(state.sessionStatus) ? undefined : state.threadId;
}
function isTerminalSessionStatus(status) {
return ["failed", "interrupted", "timeout", "error", "canceled", "expired"].includes(String(status ?? "").toLowerCase());
}
function errorStatusFromPresentation(presentation, error) {
if (presentation.category === "timeout") return "timeout";
if (presentation.category === "canceled" || error?.code === "codex_stdio_canceled") return "canceled";
return "failed";
}
function pendingSessionContinuity({ conversationId, sessionId, threadId, retryOf = null } = {}) {
const hasReusableSession = Boolean(nonEmptyString(sessionId));
const hasReusableThread = Boolean(nonEmptyString(threadId));
const status = hasReusableSession || hasReusableThread
? "continued"
: retryOf ? "degraded" : "new";
return {
status,
tone: status === "continued" ? "ok" : status === "degraded" ? "warn" : "source",
label: status === "continued" ? "继续当前会话" : status === "degraded" ? "重试降级" : "新会话待建立",
summary: status === "continued"
? "本次请求会带上已保存的 conversation/session/thread 上下文。"
: status === "degraded"
? "上一轮未留下可复用 session/thread;重试会保留输入和 conversation,并等待后端建立新会话。"
: "这是当前浏览器会话中的首轮请求,等待后端分配 session/thread。",
requested: {
conversationId: nonEmptyString(conversationId),
sessionId: nonEmptyString(sessionId),
threadId: nonEmptyString(threadId),
retryOf: nonEmptyString(retryOf)
},
returned: {
conversationId: null,
sessionId: null,
threadId: null
},
missing: [],
changed: []
};
}
function completedSessionContinuity(result, pending = null) {
const lifecycle = result?.sessionSummary ?? result?.sessionLifecycle ?? null;
if (lifecycle?.degraded === true || lifecycle?.fallback === true) {
const requested = pending?.requested ?? {};
return {
status: "degraded",
tone: "warn",
label: lifecycle.label ?? "会话降级",
summary: lifecycle.userMessage ?? "当前响应为会话降级;输入已保留,可重新发送建立新会话。",
requested,
returned: {
conversationId: nonEmptyString(result?.conversationId),
sessionId: sessionIdFrom(result),
threadId: threadIdFrom(result)
},
missing: [],
changed: [],
reused: lifecycle.reused ?? null
};
}
const requested = pending?.requested ?? {};
const returned = {
conversationId: nonEmptyString(result?.conversationId),
sessionId: sessionIdFrom(result),
threadId: threadIdFrom(result)
};
const missing = [];
const changed = [];
if (!returned.conversationId) missing.push("conversationId");
if (requiresCodexSessionEvidence(result) && !returned.sessionId) missing.push("sessionId");
if (requiresCodexThreadEvidence(result) && !returned.threadId) missing.push("threadId");
if (requiresCodexProviderTrace(result) && !hasProviderTrace(result)) missing.push("providerTrace");
if (requested.conversationId && returned.conversationId && requested.conversationId !== returned.conversationId) changed.push("conversationId");
if (requested.sessionId && returned.sessionId && requested.sessionId !== returned.sessionId) changed.push("sessionId");
if (requested.threadId && returned.threadId && requested.threadId !== returned.threadId) changed.push("threadId");
const reused = result?.sessionReuse?.reused ?? result?.session?.reused ?? result?.runnerTrace?.sessionReused ?? null;
const requestedReusable = Boolean(requested.sessionId || requested.threadId);
const status = missing.length > 0 || changed.length > 0 || (requestedReusable && reused === false)
? "degraded"
: requestedReusable ? "continued" : "new";
const label = status === "continued" ? "继续当前会话" : status === "new" ? "新会话已建立" : "新会话/会话降级";
const summary = status === "continued"
? "后端确认复用当前 conversation/session/thread。"
: status === "new"
? "后端已为当前 conversation 建立新的 session/thread。"
: `后端未完整确认原 session/thread 复用;${continuityIssueText({ missing, changed, reused })}`;
return {
status,
tone: status === "continued" ? "ok" : status === "new" ? "source" : "warn",
label,
summary,
requested,
returned,
missing,
changed,
reused
};
}
function failedSessionContinuity(pending = null, error = null) {
const base = pending ?? pendingSessionContinuity();
const missing = [...(base.missing ?? [])];
if (!base.requested?.sessionId) missing.push("sessionId");
if (!base.requested?.threadId) missing.push("threadId");
const code = normalizeErrorCode(error?.code);
const label = code === "session_busy"
? "会话忙/请等待"
: code === "session_expired"
? "会话已过期"
: ["session_failed", "session_error", "session_timeout"].includes(code)
? "需新建会话"
: "会话受阻";
const summary = code === "session_busy"
? "上一轮仍在处理;输入已保留,请等待当前请求完成或取消后重试。"
: code === "session_expired"
? "当前 session 已过期;输入已保留,可重新发送建立新的 Code Agent session。"
: `本次请求没有完成可复用会话确认;输入已保留。${error?.code ? ` code=${error.code}` : ""}`;
return {
...base,
status: "blocked",
tone: "blocked",
label,
summary,
missing: [...new Set(missing)]
};
}
function continuityIssueText({ missing = [], changed = [], reused = null } = {}) {
const parts = [
missing.length ? `缺失 ${missing.join("、")}` : null,
changed.length ? `${changed.join("、")} 已变化` : null,
reused === false ? "sessionReuse=false" : null
].filter(Boolean);
return parts.length ? parts.join("") : "会话复用证据不足";
}
function codeAgentContinuityBlocker(result, continuity) {
if (
!result ||
result.status !== "completed" ||
isSourceFixtureCompletion(result) ||
isTextFallbackChatResult(result) ||
isCodeAgentM3SkillCompletion(result) ||
!isTrustedCodeAgentProvider(result.provider)
) {
return null;
}
const missing = [
...(!hasProviderTrace(result) ? ["providerTrace"] : []),
...(!sessionIdFrom(result) ? ["sessionId"] : []),
...(requiresCodexThreadEvidence(result) && !threadIdFrom(result) ? ["threadId"] : [])
];
if (missing.length === 0) return null;
return {
code: "code_agent_session_evidence_missing",
category: "session_blocked",
layer: "cloud-web",
retryable: true,
missingFields: missing,
traceId: result.traceId,
message: `completed response missing Code Agent session evidence: ${missing.join(", ")}`,
userMessage: `Code Agent completed 但会话证据缺失:${missing.join("、")};本次按会话降级处理,输入已保留,可重试建立新会话。`,
blocker: {
code: "code_agent_session_evidence_missing",
category: "session_blocked",
layer: "cloud-web",
retryable: true,
missingFields: missing,
traceId: result.traceId,
userMessage: `Code Agent completed 但会话证据缺失:${missing.join("、")};本次按会话降级处理,输入已保留,可重试建立新会话。`,
sessionContinuity: continuity
}
};
}
function hasProviderTrace(value) {
return Boolean(value?.providerTrace && typeof value.providerTrace === "object" && Object.keys(value.providerTrace).length > 0);
}
function requiresCodexProviderTrace(value) {
return isTrustedCodeAgentProvider(value?.provider) && !isSourceFixtureCompletion(value) && !isCodeAgentM3SkillCompletion(value);
}
function requiresCodexSessionEvidence(value) {
return isTrustedCodeAgentProvider(value?.provider) && !isCodeAgentM3SkillCompletion(value);
}
function requiresCodexThreadEvidence(value) {
return isCodexStdioLongLivedShape(value) && !isCodeAgentM3SkillCompletion(value);
}
function isCodexStdioLongLivedShape(value) {
return (
isTrustedCodeAgentProvider(value?.provider) ||
String(value?.runner?.kind ?? value?.runnerTrace?.runnerKind ?? "").trim() === CODEX_APP_SERVER_RUNNER_KIND ||
value?.runner?.codexStdio === true ||
value?.session?.codexStdio === true
) && (
String(value?.sessionMode ?? value?.session?.sessionMode ?? value?.runnerTrace?.sessionMode ?? "").trim() === CODEX_APP_SERVER_SESSION_MODE ||
String(value?.implementationType ?? value?.session?.implementationType ?? "").trim() === CODEX_APP_SERVER_IMPLEMENTATION_TYPE
);
}
function sessionIdFrom(value) {
return firstNonEmptyString(value?.sessionId, value?.session?.sessionId, value?.sessionReuse?.sessionId, value?.runner?.sessionId, value?.runnerTrace?.sessionId);
}
function threadIdFrom(value) {
return firstNonEmptyString(value?.threadId, value?.session?.threadId, value?.sessionReuse?.threadId, value?.providerTrace?.threadId, value?.runnerTrace?.threadId);
}
function firstNonEmptyString(...values) {
for (const value of values) {
const text = nonEmptyString(value);
if (text) return text;
}
return null;
}
function nonEmptyString(value) {
const text = String(value ?? "").trim();
return text ? text : null;
}
function subscribeRunnerTrace(traceId, messageId) {
if (typeof EventSource !== "function") return pollRunnerTrace(traceId, messageId);
const existing = state.traceStreams.get(traceId);
if (existing) existing();
let stopped = false;
let fallbackStop = null;
let firstEventSeen = false;
let fallbackTimer = null;
const source = new EventSource(`/v1/agent/chat/trace/${encodeURIComponent(traceId)}/stream`);
const close = () => {
stopped = true;
if (fallbackTimer) window.clearTimeout(fallbackTimer);
source.close();
if (fallbackStop) fallbackStop();
if (state.traceStreams.get(traceId) === close) state.traceStreams.delete(traceId);
};
const markEventSeen = () => {
firstEventSeen = true;
if (fallbackTimer) {
window.clearTimeout(fallbackTimer);
fallbackTimer = null;
}
};
const startPolling = () => {
if (stopped || fallbackStop) return;
source.close();
fallbackStop = pollRunnerTrace(traceId, messageId);
state.traceStreams.set(traceId, close);
};
fallbackTimer = window.setTimeout(() => {
if (!firstEventSeen) startPolling();
}, TRACE_STREAM_FALLBACK_MS);
source.addEventListener("snapshot", (event) => {
markEventSeen();
updateMessageTrace(messageId, parseTraceEventData(event.data));
});
source.addEventListener("runnerTrace", (event) => {
markEventSeen();
const payload = parseTraceEventData(event.data);
updateMessageTrace(messageId, payload?.snapshot ?? payload);
});
source.addEventListener("heartbeat", (event) => {
markEventSeen();
updateMessageTrace(messageId, parseTraceEventData(event.data), { quiet: true });
});
source.onerror = () => {
startPolling();
};
state.traceStreams.set(traceId, close);
return close;
}
function pollRunnerTrace(traceId, messageId) {
let stopped = false;
let timer = null;
const tick = async () => {
if (stopped) return;
try {
const response = await fetchJson(`/v1/agent/chat/trace/${encodeURIComponent(traceId)}`, {
timeoutMs: Math.min(API_TIMEOUT_MS, 3000),
timeoutName: "Code Agent trace"
});
if (response.ok) updateMessageTrace(messageId, response.data);
} catch {
// Keep polling; the main /v1/agent/chat request owns terminal error handling.
}
if (!stopped) timer = window.setTimeout(tick, TRACE_POLL_INTERVAL_MS);
};
tick();
const stop = () => {
stopped = true;
if (timer) window.clearTimeout(timer);
if (state.traceStreams.get(traceId) === stop) state.traceStreams.delete(traceId);
};
state.traceStreams.set(traceId, stop);
return stop;
}
function parseTraceEventData(data) {
try {
return data ? JSON.parse(data) : null;
} catch {
return null;
}
}
function updateMessageTrace(messageId, snapshot, options = {}) {
if (!snapshot || typeof snapshot !== "object" || !Array.isArray(snapshot.events)) return;
const index = state.chatMessages.findIndex((message) => message.id === messageId);
if (index < 0) return;
const current = state.chatMessages[index];
const runnerTrace = runnerTraceFromSnapshot(snapshot, current.runnerTrace);
const sessionId = snapshot.sessionId ?? runnerTrace.sessionId ?? current.sessionId;
const threadId = snapshot.threadId ?? runnerTrace.threadId ?? current.threadId;
noteCurrentRequestTraceActivity(snapshot);
state.chatMessages[index] = {
...current,
runnerTrace,
traceId: snapshot.traceId ?? current.traceId,
sessionId,
threadId,
updatedAt: snapshot.updatedAt ?? current.updatedAt
};
if (sessionId) state.sessionId = sessionId;
if (threadId) state.threadId = threadId;
if (runnerTrace.sessionStatus) state.sessionStatus = runnerTrace.sessionStatus;
if (state.currentRequest?.traceId === (snapshot.traceId ?? current.traceId) && sessionId) {
state.currentRequest.sessionId = sessionId;
}
if (state.currentRequest?.traceId === (snapshot.traceId ?? current.traceId) && threadId) {
state.currentRequest.threadId = threadId;
}
if (options.quiet !== true) {
renderCodeAgentSummary();
renderConversation();
renderRecords(state.liveSurface);
}
}
function noteCurrentRequestTraceActivity(snapshot) {
const request = state.currentRequest;
if (!request || request.traceId !== snapshot.traceId) return;
const eventCount = Number.isInteger(snapshot.eventCount) ? snapshot.eventCount : snapshot.events.length;
if (eventCount <= (request.traceEventCount ?? 0)) {
if (snapshot.waitingFor) request.waitingFor = snapshot.waitingFor;
return;
}
request.traceEventCount = eventCount;
request.lastActivityAt = Date.now();
request.lastActivityIso = new Date(request.lastActivityAt).toISOString();
request.lastTraceUpdatedAt = snapshot.updatedAt ?? request.lastTraceUpdatedAt;
request.waitingFor = snapshot.waitingFor ?? request.waitingFor;
request.lastEventLabel = snapshot.lastEvent?.label ?? snapshot.events.at(-1)?.label ?? request.lastEventLabel;
}
function latestTraceSnapshot(traceId) {
for (const message of [...state.chatMessages].reverse()) {
if (message.traceId === traceId && message.runnerTrace) return message.runnerTrace;
}
return null;
}
function runnerTraceFromSnapshot(snapshot, previous = null) {
return {
...(previous && typeof previous === "object" ? previous : {}),
traceId: snapshot.traceId,
runnerKind: snapshot.runnerKind ?? previous?.runnerKind,
workspace: snapshot.workspace ?? previous?.workspace,
sandbox: snapshot.sandbox ?? previous?.sandbox,
sessionMode: snapshot.sessionMode ?? previous?.sessionMode,
sessionId: snapshot.sessionId ?? previous?.sessionId,
threadId: snapshot.threadId ?? previous?.threadId,
turnId: snapshot.turnId ?? previous?.turnId,
sessionStatus: snapshot.sessionStatus ?? previous?.sessionStatus,
implementationType: snapshot.implementationType ?? previous?.implementationType,
startedAt: snapshot.startedAt ?? previous?.startedAt,
finishedAt: snapshot.finishedAt ?? previous?.finishedAt,
updatedAt: snapshot.updatedAt ?? previous?.updatedAt,
elapsedMs: snapshot.elapsedMs ?? previous?.elapsedMs,
waitingFor: snapshot.waitingFor ?? previous?.waitingFor,
events: snapshot.events,
eventLabels: snapshot.eventLabels ?? snapshot.events.map((item) => item.label).filter(Boolean),
lastEvent: snapshot.lastEvent ?? snapshot.events.at(-1) ?? null,
outputTruncated: snapshot.outputTruncated === true,
valuesPrinted: false
};
}
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.routePath.textContent = "当前浏览器会话只保存任务草稿,硬件动作需通过受控后端流程。";
renderConversation();
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;未完成前不标为通过。";
renderLiveBuilds(null);
renderCodeAgentSummary();
}
async function sendAgentMessage(message, conversationId, traceId = nextProtocolId("trc"), requestedSessionId = sessionIdForNextRequest(), requestedThreadId = threadIdForNextRequest()) {
const sessionId = requestedSessionId || undefined;
const threadId = requestedThreadId || undefined;
const response = await fetchJson("/v1/agent/chat", {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-Trace-Id": traceId,
"Prefer": "respond-async",
"X-HWLAB-Short-Connection": "1"
},
timeoutMs: CODE_AGENT_SUBMIT_TIMEOUT_MS,
timeoutName: "Code Agent",
body: JSON.stringify({
message,
conversationId,
sessionId,
threadId,
traceId,
shortConnection: true,
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;
}
if (response.status === 202 || response.data?.shortConnection === true) {
return waitForAgentMessageResult(traceId, response.data);
}
return {
...response.data,
traceId: response.data?.traceId || traceId
};
}
async function waitForAgentMessageResult(traceId, accepted = {}) {
const startedAt = Date.now();
const resultPath = accepted?.resultUrl || `/v1/agent/chat/result/${encodeURIComponent(traceId)}`;
while (true) {
const activity = readActivityRef(() => state.currentRequest?.traceId === traceId ? state.currentRequest : null, startedAt);
const idleMs = Math.max(0, Date.now() - activity.lastActivityAt);
if (idleMs >= CODE_AGENT_TIMEOUT_MS) {
const error = new Error(`Code Agent 超过 ${CODE_AGENT_TIMEOUT_MS}ms 无新事件`);
error.code = "client_trace_idle_timeout";
error.timeoutMs = CODE_AGENT_TIMEOUT_MS;
error.idleMs = idleMs;
error.lastActivityAt = activity.lastActivityIso;
error.waitingFor = activity.waitingFor;
error.traceId = traceId;
throw error;
}
const response = await fetchJson(resultPath, {
timeoutMs: Math.min(API_TIMEOUT_MS, 3000),
timeoutName: "Code Agent result"
});
if (response.ok && response.status === 200 && response.data?.status && response.data.status !== "running") {
return {
...response.data,
traceId: response.data.traceId || traceId
};
}
if (!response.ok && response.status && response.status !== 404) {
const error = agentErrorFromHttpResponse(response, traceId);
throw error;
}
await wait(Math.min(TRACE_POLL_INTERVAL_MS, 1000));
}
}
function wait(ms) {
return new Promise((resolve) => window.setTimeout(resolve, ms));
}
async function loadLiveSurface() {
const projectId = gateSummary.topology.projectId;
const [healthLive, liveBuilds, restIndex, m3Control, m3Status, health, adapter, audit, evidence] = await Promise.all([
fetchJson("/health/live"),
fetchJson("/v1/live-builds"),
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,
liveBuilds,
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,
activityRef = null,
...fetchOptions
} = options;
const controller = new AbortController();
let timer = null;
let timeoutDetail = null;
const startedAt = Date.now();
const activityTimeout = Boolean(activityRef);
const scheduleTimeout = () => {
const activity = readActivityRef(activityRef, startedAt);
const idleMs = Math.max(0, Date.now() - activity.lastActivityAt);
const remainingMs = timeoutMs - idleMs;
if (remainingMs <= 0) {
timeoutDetail = { ...activity, idleMs };
controller.abort();
return;
}
timer = window.setTimeout(scheduleTimeout, Math.max(1, remainingMs));
};
scheduleTimeout();
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";
const timeoutState = timeoutDetail ?? readActivityRef(activityRef, startedAt);
const idleMs = timedOut ? Math.max(0, Date.now() - timeoutState.lastActivityAt) : undefined;
return {
ok: false,
path,
timeout: timedOut,
timeoutMs,
idleMs,
lastActivityAt: timedOut ? timeoutState.lastActivityIso : undefined,
waitingFor: timedOut ? timeoutState.waitingFor : undefined,
error: timedOut
? activityTimeout
? `${timeoutName} 超过 ${timeoutMs}ms 无新事件`
: `${timeoutName} 请求超时 ${timeoutMs}ms`
: `请求失败:${error.message}`
};
} finally {
if (timer) window.clearTimeout(timer);
}
}
function readActivityRef(activityRef, fallbackMs = Date.now()) {
const ref = typeof activityRef === "function" ? activityRef() : activityRef;
const lastActivityAt = Number(ref?.lastActivityAt);
const safeLastActivityAt = Number.isFinite(lastActivityAt) && lastActivityAt > 0 ? lastActivityAt : fallbackMs;
return {
lastActivityAt: safeLastActivityAt,
lastActivityIso: typeof ref?.lastActivityIso === "string" ? ref.lastActivityIso : new Date(safeLastActivityAt).toISOString(),
waitingFor: typeof ref?.waitingFor === "string" ? ref.waitingFor : null,
lastEventLabel: typeof ref?.lastEventLabel === "string" ? ref.lastEventLabel : null
};
}
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");
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
};
if (!m3ControlCanOperate()) {
state.m3Control.operation = {
status: "blocked",
action,
traceId,
command: body,
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,
command: body,
observedAt: new Date().toISOString()
};
renderM3ControlStatus();
renderDrafts();
renderRecords(state.liveSurface);
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();
renderLiveBuilds(live.liveBuilds);
renderAgentChatStatus(deriveAgentChatStatus());
renderCodeAgentSummary();
renderHardwareStatus(state.m3Control.status);
renderWiringList(state.m3Control.status ?? runtimeSummary);
renderRecords(live);
renderM3ControlStatus();
renderConversation();
renderDrafts();
}
function renderLiveBuilds(response) {
const payload = response?.ok ? response.data : null;
const services = Array.isArray(payload?.services) ? payload.services : [];
const latest = payload?.latest ?? null;
if (latest?.build?.createdAt) {
el.liveBuildLatest.textContent = [
`最新镜像构建时间:${formatBeijingTime(latest.build.createdAt)}`,
latest.name || latest.serviceId || "未知服务",
`tag ${latest.image?.tag || "unknown"}`,
`commit ${shortToken(latest.commit?.id)}`,
`revision ${shortToken(latest.revision)}`,
`来源 ${liveBuildSourceLabel(latest.build?.metadataSource)}`
].join(" · ");
} else {
el.liveBuildLatest.textContent = "最新镜像构建时间:构建时间不可用";
}
if (!response) {
replaceChildren(
el.liveBuildList,
liveBuildUnavailableRow("等待实时元数据聚合返回。")
);
return;
}
if (!response.ok) {
replaceChildren(
el.liveBuildList,
liveBuildUnavailableRow(`构建时间不可用:${response.error || "实时元数据聚合不可用"}`)
);
return;
}
if (!services.length) {
replaceChildren(
el.liveBuildList,
liveBuildUnavailableRow("构建时间不可用:当前实时元数据未返回微服务明细。")
);
return;
}
replaceChildren(el.liveBuildList, ...services.map(liveBuildRow));
}
function liveBuildRow(service) {
const row = document.createElement("div");
row.className = `live-build-row live-build-row-${toneClass(service.status)}`;
const header = document.createElement("div");
header.className = "live-build-row-head";
header.append(
textSpan(service.name || service.serviceId || "未知服务", "live-build-service"),
textSpan(liveBuildTimeText(service), "live-build-time")
);
const meta = document.createElement("div");
meta.className = "live-build-meta";
meta.append(
textSpan(`tag ${service.image?.tag || "unknown"}`),
textSpan(`commit ${shortToken(service.commit?.id)}`),
textSpan(`revision ${shortToken(service.revision)}`),
textSpan(`来源 ${liveBuildSourceLabel(service.build?.metadataSource)}`)
);
const desired = liveBuildDesiredStateText(service);
if (desired) meta.append(textSpan(desired));
const reasonText = liveBuildReasonText(service);
if (reasonText) {
row.append(header, meta, textSpan(reasonText, "live-build-reason"));
} else {
row.append(header, meta);
}
return row;
}
function liveBuildUnavailableRow(reason) {
return liveBuildRow({
serviceId: "live-build-metadata",
name: "实时元数据",
kind: "hwlab",
status: "unavailable",
build: {
createdAt: null,
unavailableReason: reason
},
image: {
tag: "unknown"
},
commit: {
id: "unknown"
},
revision: "unknown"
});
}
function liveBuildTimeText(service) {
if (service.kind === "external") return "外部镜像";
const createdAt = service.build?.createdAt;
return createdAt ? formatBeijingTime(createdAt) : "构建时间不可用";
}
function liveBuildReasonText(service) {
if (service.kind === "external") return "外部镜像或非 HWLAB 构建产物,不参与最新 HWLAB 构建时间计算。";
if (service.build?.liveHealthMissingReason) return service.build.liveHealthMissingReason;
if (service.build?.unavailableReason) return service.build.unavailableReason;
if (service.error?.message) return `构建时间不可用:${service.error.message}`;
return "";
}
function liveBuildSourceLabel(source) {
const text = String(source ?? "").trim();
if (text.includes("artifact-catalog")) return "artifact catalog";
if (text.includes("deploy")) return "deploy metadata";
if (text.includes("runtime-env") || text.includes("health")) return "live health";
if (text.includes("external")) return "external image";
if (!text || text === "unavailable") return "不可用";
return text;
}
function liveBuildDesiredStateText(service) {
if (!service.desiredState || service.kind === "external") return "";
const parts = [];
if (Number.isFinite(Number(service.desiredState.replicas))) {
parts.push(`replicas ${Number(service.desiredState.replicas)}`);
}
if (service.desiredState.healthPath) parts.push(service.desiredState.healthPath);
return parts.length ? `desired ${parts.join(" / ")}` : "";
}
function formatBeijingTime(value) {
const date = new Date(value);
if (Number.isNaN(date.getTime())) return "构建时间不可用";
const parts = Object.fromEntries(new Intl.DateTimeFormat("zh-CN", {
timeZone: "Asia/Shanghai",
year: "numeric",
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
hour12: false
}).formatToParts(date).map((part) => [part.type, part.value]));
return `${parts.year}-${parts.month}-${parts.day} ${parts.hour}:${parts.minute}:${parts.second} 北京时间`;
}
function shortToken(value) {
const text = String(value ?? "unknown").trim() || "unknown";
return text.length > 12 ? text.slice(0, 12) : text;
}
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 renderConversation() {
const introMessages = [
{
role: "system",
title: "界面模式",
text: "这里是用户工作台:可以整理任务、查看资源、核对接线和可信记录。对话会发送到受控 Code Agent 后端;页面不会直接发送硬件变更。",
status: "source"
},
codeAgentStatusMessage(state.codeAgentAvailability)
];
replaceChildren(el.conversationList, ...[...introMessages, ...state.chatMessages].map(messageCard));
}
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"],
["链路状态", m3LinkStatus(status, patch), m3LinkTone(status, patch)],
["链路", `${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)],
["接线盘连接", 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}`, [
["在线", String(Boolean(gateway.online)), gateway.online ? "live" : "blocked"],
["sessionId", gateway.sessionId ?? "未验证", gateway.sessionId ? "source" : "blocked"],
["更新时间", gateway.lastSeenAt ?? status.observedAt ?? "未验证", gateway.online ? "live" : "source"],
["来源", gateway.sourceKind ?? "UNVERIFIED", sourceKindTone(gateway.sourceKind)],
["阻塞原因", 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, [
["在线", 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"],
["接线状态", m3LinkStatus(status, patch), m3LinkTone(status, patch)],
["连接已激活", 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"],
["更新时间", patch.lastSyncAt ?? status.observedAt ?? "未验证", patch.connectionActive ? "live" : "source"],
["阻塞原因", patch.lastError ?? "none", patch.lastError ? "blocked" : "source"],
["来源", patch.sourceKind ?? "UNVERIFIED", sourceKindTone(patch.sourceKind)]
])
];
}
function m3LinkStatus(status, patch = status?.patchPanel ?? {}) {
if (status?.status === "error") return "error";
if (status?.status === "unverified") return "unverified";
if (status?.status === "live" && status?.trust?.durableStatus === "green" && patch.connectionActive === true) return "live";
if (patch.connectionActive === true) return "blocked";
return status?.status === "blocked" ? "blocked" : "unverified";
}
function m3LinkTone(status, patch) {
const linkStatus = m3LinkStatus(status, patch);
if (linkStatus === "live") return "live";
if (linkStatus === "error" || linkStatus === "blocked") return "blocked";
return "source";
}
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";
renderM3FlowSummary({ label, tone, operation, contract, controlReady, pending });
}
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: "选择 DO1 true/false 后点击“写入 DO1”;再点击“读取 DI1”核对 res_boxsimu_2:DI1。最近 trace 与 operation 可在“可信记录”页签追踪。",
tone: controlReady ? "source" : "blocked"
},
{
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} / action=${operationActionLabel(operation.action)} / DO1目标=${m3OperationDo1Target(operation)} / DI1观测=等待返回 / 正在通过 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 ioValues = ` / DO1目标=${m3OperationDo1Target(operation)} / DI1观测=${m3OperationDi1Observed(operation, {
fallback: m3StatusPortValue(state.m3Control.status, M3_TRUSTED_ROUTE.toResourceId, M3_TRUSTED_ROUTE.toPort)
})}`;
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}${ioValues}${blocker}${evidence}`;
}
function renderM3FlowSummary({ label, tone, operation, contract, controlReady, pending }) {
const status = state.m3Control.status;
const do1Live = m3StatusPortValue(status, M3_TRUSTED_ROUTE.fromResourceId, M3_TRUSTED_ROUTE.fromPort);
const di1Live = m3StatusPortValue(status, M3_TRUSTED_ROUTE.toResourceId, M3_TRUSTED_ROUTE.toPort);
const do1Target = m3OperationDo1Target(operation, { fallback: el.m3ValueSelect.value });
const di1Observed = m3OperationDi1Observed(operation, { fallback: di1Live });
const actionLabel = operation
? `${operationActionLabel(operation.action)} / ${statusLabel(operation.status)}`
: controlReady
? "可执行:写入 DO1 后读取 DI1"
: "等待 cloud-api readiness";
el.m3FlowState.textContent = label;
el.m3FlowState.className = `state-tag tone-${toneClass(tone)}`;
el.m3ActionSummary.textContent = pending
? `正在执行 ${operationActionLabel(operation?.action)}trace 已生成。`
: actionLabel;
el.m3Do1Target.textContent = operation?.action === "di.read" && do1Live === null
? `本次只读取;当前选择 ${el.m3ValueSelect.value}`
: do1Target;
el.m3Di1Observed.textContent = di1Observed;
el.m3TrustSummary.textContent = m3TrustSummaryText(operation, contract);
el.m3TraceSummary.textContent = m3TraceSummaryText(operation, status);
}
function m3OperationDo1Target(operation, { fallback = null } = {}) {
if (operation?.action === "do.write") {
const value = operation.command?.value ?? operation.value ?? operation.target?.value ?? operation.result?.value;
return value === undefined || value === null ? "DO1 目标值等待返回" : `res_boxsimu_1:DO1=${String(value)}`;
}
if (fallback !== null && fallback !== undefined && fallback !== "") return `待写入选择=${String(fallback)}`;
return "本次未写入 DO1";
}
function m3OperationDi1Observed(operation, { fallback = null } = {}) {
const value = operation?.result?.targetReadback?.value ?? operation?.readback?.value ?? (operation?.action === "di.read" ? operation?.result?.value : undefined);
if (value !== undefined && value !== null) return `res_boxsimu_2:DI1=${String(value)}`;
if (fallback !== null && fallback !== undefined && fallback !== "") return `res_boxsimu_2:DI1=${String(fallback)}`;
if (operation?.status === "running") return "等待 cloud-api 返回";
return "待读取";
}
function m3StatusPortValue(status, resourceId, port) {
const portState = findM3Box(status, resourceId)?.ports?.[port];
return portState?.value === undefined || portState?.value === null ? null : portState.value;
}
function m3TrustSummaryText(operation, contract) {
if (operation?.evidenceState) {
const trusted = operation.evidenceState.status === "green";
const durable = operation.evidenceState.durable === true;
const blocker = operation.evidenceState.blocker ?? operation.trustBlocker?.code ?? operation.blocker?.code ?? "无";
return `trusted=${String(trusted)}durable=${String(durable)}${trusted ? "可信记录完整" : `阻塞原因=${blocker}`}`;
}
const trust = contract?.readiness?.trust ?? contract?.trustReadiness;
if (trust?.status) {
return `trusted=${String(trust.trustedEvidenceReady === true)}durable=${String(trust.durableStatus?.durable === true)};状态=${trust.status}`;
}
return m3ControlBlockedReason(contract);
}
function m3TraceSummaryText(operation, status) {
const ids = [
recordField("operation", operation?.operationId ?? status?.trust?.operationId),
recordField("trace", operation?.traceId ?? status?.trust?.traceId),
recordField("audit", operation?.auditId ?? status?.trust?.auditId),
recordField("evidence", operation?.evidenceId ?? status?.trust?.evidenceId)
].filter(Boolean);
return ids.length ? `最近 ${ids.join(" / ")}` : "最近 trace:无";
}
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 === "running" ? "pending" : ["failed", "timeout", "canceled", "error"].includes(message.status) ? "blocked" : "source"),
recordField("conversation", message.conversationId ?? state.conversationId),
recordField("trace", message.traceId),
recordField("session", message.sessionId ?? sessionSummary(message.session)),
recordField("thread", message.threadId ?? threadIdFrom(message)),
recordField("message", message.messageId),
message.error?.message ? `失败原因=${safeFailureReason(message.error.message)}` : null
].filter(Boolean).join(" / "),
tone: message.status === "running" ? "pending" : ["failed", "timeout", "canceled", "error"].includes(message.status) ? "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),
`DO1目标=${m3OperationDo1Target(controlOperation)}`,
`DI1观测=${m3OperationDi1Observed(controlOperation, {
fallback: m3StatusPortValue(state.m3Control.status, M3_TRUSTED_ROUTE.toResourceId, M3_TRUSTED_ROUTE.toPort)
})}`,
`trusted=${String(controlOperation.evidenceState?.status === "green")}`,
`durable=${String(controlOperation.evidenceState?.durable === true)}`,
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: "后端失败",
timeout: "等待超时",
canceled: "已取消",
error: "请求错误",
blocked: "服务受阻"
};
el.agentChatStatus.textContent = agentStatusLabel(status, result, labels);
el.agentChatStatus.className = `state-tag tone-${toneClass(agentStatusTone(status, result))}`;
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("sessionId/status", summary.sessionIdStatus, sessionSummaryTone(summary)),
codeAgentSummaryRow("会话提示", summary.sessionLifecycleHint, sessionSummaryTone(summary)),
codeAgentSummaryRow("workspace", summary.workspace, "source"),
codeAgentSummaryRow("sandbox", summary.sandbox, "source"),
codeAgentSummaryRow("runnerKind", summary.runnerKind, summary.kind === "long-lived-session" ? "ok" : "source"),
codeAgentSummaryRow("protocol", summary.protocol, summary.runtimePathTone),
codeAgentSummaryRow("implementationType", summary.implementationType, summary.runtimePathTone),
codeAgentSummaryRow("providerTrace.command", summary.providerTraceCommand, summary.runtimePathTone),
codeAgentSummaryRow("providerTrace.terminalStatus", summary.providerTraceTerminalStatus, summary.runtimePathTone),
codeAgentSummaryRow("运行路径语义", summary.runtimePathLabel, summary.runtimePathTone),
codeAgentSummaryRow("toolCalls", summary.toolCalls, summary.toolCalls === "none" ? "source" : summary.tone),
codeAgentSummaryRow("skills", summary.skills, summary.skills === "none" ? "source" : summary.tone),
codeAgentSummaryRow("conversation facts", conversationFactsSummary(summary.latestMessage?.conversationFacts), summary.latestMessage?.conversationFacts ? "source" : "blocked"),
codeAgentSummaryRow("fact traces", conversationFactTracesSummary(summary.latestMessage?.conversationFacts), summary.latestMessage?.conversationFacts ? "source" : "blocked"),
codeAgentSummaryRow("runnerTrace", summary.runnerTrace, "source"),
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 === "completed" && result?.m3Io?.type === "m3_io_result") {
return result.m3Io.trust?.trusted === true ? "M3 IO 可信回复" : "M3 IO 受控结果";
}
if (status !== "failed") return labels[status] ?? statusLabel(status);
if (result?.m3Io?.type === "m3_io_blocker") return "M3 IO 阻塞";
const presentation = agentFailurePresentation(result?.error, { result });
return (
{
timeout: "等待超时",
provider: "Provider 不可用",
runner_busy: "Runner 忙碌",
session_blocked: "Session 受阻",
canceled: "已取消",
runner_blocked: "Runner 受阻",
api_error: "API 错误",
needs_config: "需要配置",
capability_unavailable: "能力未开放",
security_blocked: "安全阻断",
fallback: "仍是 fallback",
retryable: "可重试"
}[presentation.category] ?? labels.failed
);
}
function agentStatusTone(status, result) {
if (status === "completed" && result?.m3Io?.type === "m3_io_result") {
return result.m3Io.trust?.trusted === true ? "dev-live" : "warn";
}
if (status === "completed") return "dev-live";
if (["failed", "blocked", "timeout", "canceled", "error"].includes(status)) return "blocked";
if (status === "running") return "pending";
return "source";
}
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, { blockedError = null } = {}) {
if (result?.status === "canceled") {
return {
status: "canceled",
replied: false,
sourceKind: "BLOCKED",
title: "Code Agent 已取消"
};
}
if (result?.status === "timeout") {
return {
status: "timeout",
replied: false,
sourceKind: "BLOCKED",
title: "Code Agent 无新事件超时"
};
}
if (result?.status === "error") {
return {
status: "failed",
replied: false,
sourceKind: "BLOCKED",
title: "Code Agent 返回错误"
};
}
if (result?.m3Io?.type === "m3_io_blocker") {
return {
status: "failed",
replied: false,
sourceKind: "BLOCKED",
title: "M3 IO 阻塞"
};
}
if (isStructuredBlockedChatResult(result)) {
return {
status: "failed",
replied: false,
sourceKind: "BLOCKED",
title: agentFailurePresentation(structuredBlockedErrorFromResult(result), { result }).title
};
}
if (blockedError) {
return {
status: "failed",
replied: false,
sourceKind: "BLOCKED",
title: agentFailurePresentation(blockedError, { result }).title
};
}
if (isRealCompletedChatResult(result)) {
return {
status: "completed",
replied: true,
sourceKind: result?.m3Io?.type === "m3_io_result" && result.m3Io.trust?.trusted !== true ? "CONTROLLED" : "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 ||
result.m3Io?.type === "m3_io_blocker" ||
result.m3Io?.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 m3Blocker = result?.m3Io?.blocker ?? {};
const code = normalizeErrorCode(error.code ?? m3Blocker.code ?? blocker.code ?? "code_agent_blocked");
const selectedBlocker = result?.m3Io?.type === "m3_io_blocker" && m3Blocker.code ? m3Blocker : blocker;
return {
...error,
code,
layer: error.layer ?? selectedBlocker.layer,
category: error.category ?? selectedBlocker.category,
blocker: selectedBlocker,
retryable: error.retryable ?? selectedBlocker.retryable,
userMessage: error.userMessage ?? selectedBlocker.userMessage,
message: error.message ?? selectedBlocker.summary ?? selectedBlocker.zh ?? selectedBlocker.message ?? code,
traceId: error.traceId ?? result?.traceId,
route: error.route ?? selectedBlocker.route,
toolName: error.toolName ?? selectedBlocker.toolName
};
}
function m3IoPresentation(result) {
const m3 = result?.m3Io && typeof result.m3Io === "object" ? result.m3Io : null;
if (!m3) return null;
const do1 = m3.do1?.targetValue === undefined || m3.do1?.targetValue === null ? "未产生/不可证明" : String(m3.do1.targetValue);
const di1 = m3.di1?.observedValue === undefined || m3.di1?.observedValue === null ? "未产生/不可证明" : String(m3.di1.observedValue);
const fields = `traceId=${m3.trace?.traceId ?? result?.traceId ?? "null"}operationId=${m3.operation?.operationId ?? "null"}`;
const trust = `trusted=${m3.trust?.trusted === true ? "true" : "false"}durable=${m3.trust?.durable === true ? "true" : "false"}`;
const path = m3.path?.summary ?? "Code Agent -> Skill CLI -> HWLAB API";
const wiring = m3.wiring?.label ?? "res_boxsimu_1:DO1 -> hwlab-patch-panel -> res_boxsimu_2:DI1";
if (m3.type === "m3_io_blocker") {
const blocker = m3.blocker ?? {};
const reason = blocker.userMessage ?? blocker.zh ?? blocker.message ?? blocker.code ?? "M3 控制链路仍受阻";
return {
title: "M3 IO 阻塞",
text: `M3 IO 阻塞:${reason}。\nDO1 目标值:${do1}DI1 观测值:${di1}。\n路径:${path}。接线:${wiring}。\n排查字段:${fields}blocker=${blocker.code ?? "unknown"}。\n可信状态:${trust};未伪造 DEV-LIVE,也未使用 OpenAI fallback 冒充执行。`
};
}
return {
title: "M3 IO 结果",
text: `M3 IO 结果:DO1 目标值=${do1}DI1 观测值=${di1}。\n路径:${path}。接线:${wiring}。\n排查字段:${fields}auditId=${m3.operation?.auditId ?? "null"}evidenceId=${m3.operation?.evidenceId ?? "null"}。\n可信状态:${trust};这是受控操作返回,不是 M3 DEV-LIVE 验收结论。`
};
}
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.hardTimeoutMs = payloadError && typeof payloadError === "object" ? payloadError.hardTimeoutMs : undefined;
error.idleMs = response.idleMs ?? (payloadError && typeof payloadError === "object" ? payloadError.idleMs : undefined);
error.lastActivityAt = response.lastActivityAt ?? (payloadError && typeof payloadError === "object" ? payloadError.lastActivityAt : undefined);
error.waitingFor = response.waitingFor ?? (payloadError && typeof payloadError === "object" ? payloadError.waitingFor : 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 hardTimeoutMs = error?.hardTimeoutMs ?? result?.error?.hardTimeoutMs;
const idleMs = error?.idleMs ?? result?.error?.idleMs;
const lastActivityAt = error?.lastActivityAt ?? result?.error?.lastActivityAt;
const waitingFor = error?.waitingFor ?? result?.error?.waitingFor;
const providerStatus = error?.providerStatus ?? result?.error?.providerStatus;
if (code === "codex_stdio_hard_timeout") {
return {
category: "timeout",
title: "Code Agent 达到硬性上限",
text: `Code Agent 达到硬性总时长上限${hardTimeoutMs ? ` ${hardTimeoutMs}ms` : ""};输入已保留,可稍后重试或缩小问题范围。${traceSuffix}`
};
}
if (isTimeoutFailure(code, message)) {
const detail = [
typeof idleMs === "number" ? `idleMs=${idleMs}` : null,
lastActivityAt ? `lastActivityAt=${lastActivityAt}` : null,
waitingFor ? `waitingFor=${waitingFor}` : null
].filter(Boolean).join("");
return {
category: "timeout",
title: "Code Agent 无新事件超时",
text: `Code Agent ${timeoutMs ? `超过 ${timeoutMs}ms ` : ""}无新事件;输入已保留,可稍后重试或缩小问题范围。${detail ? ` ${detail}。` : ""}${traceSuffix}`
};
}
if (code === "codex_stdio_canceled" || code === "session_canceled") {
return {
category: "canceled",
title: "Code Agent 已取消",
text: `本次 Codex stdio 请求已取消;输入、sessionId 和 traceId 已保留,可重试上一条消息。${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: "会话忙/请等待",
text: `Codex stdio runner 正在处理上一轮请求;输入已保留,请等待当前请求完成或取消后重试。${traceSuffix}`
};
}
if (code === "session_expired") {
return {
category: "session_blocked",
title: "会话已过期",
text: `当前 session 已过期;输入已保留,可重新发送建立新的 Codex stdio session。${traceSuffix}`
};
}
if (["session_reuse_conflict", "session_failed", "session_interrupted", "session_canceled", "session_timeout", "session_error"].includes(code)) {
return {
category: "session_blocked",
title: "Code Agent Session 受阻",
text: `当前 session 已过期、失败、中断或与 conversation 绑定不一致;输入已保留,可重新发送建立新的 Codex stdio session。${traceSuffix}`
};
}
if ([
"runner_unavailable",
"tool_unavailable",
"skills_unavailable",
"security_blocked",
"codex_stdio_blocked",
"codex_stdio_failed",
"codex_stdio_protocol_blocked",
"codex_stdio_empty_response",
"codex_stdio_command_probe_failed",
"external_network_blocked",
"network_tool_unavailable",
"network_timeout"
].includes(code)) {
return {
category: "runner_blocked",
title: "Code Agent Runner 受阻",
text: `Codex stdio 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 === "canceled" || code === "codex_stdio_canceled") return "Code Agent 已取消";
if (value === "runner_busy") return "Code Agent Runner 忙碌";
if (value === "session_blocked") return "Code Agent Session 受阻";
if (value === "runner_blocked") return "Code Agent Runner 受阻";
if (value === "api_error") return "Code Agent API 错误";
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", "codex_stdio_timeout", "codex_stdio_idle_timeout", "codex_stdio_hard_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 pendingContext = messagePendingContextPanel(message);
if (pendingContext) article.append(pendingContext);
const sessionContext = messageSessionContinuityPanel(message);
if (sessionContext) article.append(sessionContext);
const m3Evidence = messageM3EvidencePanel(message);
if (m3Evidence) article.append(m3Evidence);
const runtimePath = messageRuntimePathPanel(message);
if (runtimePath) article.append(runtimePath);
const tracePanel = messageTracePanel(message);
if (tracePanel) article.append(tracePanel);
const actions = messageActionsPanel(message);
if (actions) article.append(actions);
return article;
}
function messageActionsPanel(message) {
if (message.role !== "agent") return null;
const actions = [];
if (message.status === "running") {
actions.push(actionButton("取消当前请求", "cancel", () => cancelAgentMessage(message.id), "取消当前 in-flight Codex stdio 请求"));
if (message.retryInput) {
actions.push(actionButton("重试上一条", "retry", () => restartRunningAgentMessage(message.id), "先取消当前 trace,再用同一输入重新发送"));
}
}
if (canRetryTerminalAgentMessage(message)) {
actions.push(actionButton("重试上一条", "retry", () => retryAgentMessage(message.id), "保留 conversation/trace 记录并重新发送上一条输入"));
}
if (message.traceId) {
actions.push(actionButton("回放 trace", "trace", () => replayAgentTrace(message.id), "从 runnerTrace store 重新读取真实事件"));
}
if (actions.length === 0 && !message.traceReplayStatus) return null;
const panel = document.createElement("div");
panel.className = "message-actions";
panel.append(...actions);
if (message.traceReplayStatus) {
panel.append(textSpan(message.traceReplayStatus, "message-action-status"));
}
return panel;
}
function actionButton(label, action, onClick, title) {
const button = document.createElement("button");
button.type = "button";
button.className = `message-action message-action-${action}`;
button.textContent = label;
button.title = title;
button.addEventListener("click", onClick);
return button;
}
function canRetryTerminalAgentMessage(message) {
return Boolean(
message?.retryInput &&
["failed", "timeout", "error", "canceled"].includes(String(message.status ?? "").toLowerCase())
);
}
async function restartRunningAgentMessage(messageId) {
const message = state.chatMessages.find((item) => item.id === messageId);
if (!message?.retryInput || message.status !== "running") return;
await cancelAgentMessage(messageId);
const canceled = state.chatMessages.find((item) => item.id === messageId);
if (canceled?.status !== "canceled" || state.chatPending) {
setAgentActionStatus(messageId, "取消未确认,当前 trace 仍在等待后端;已保留输入,可等待真实超时或后端结果后再重试。");
return;
}
await submitAgentMessage(message.retryInput, {
conversationId: canceled.conversationId || state.conversationId || undefined,
sessionId: isTerminalSessionStatus(canceled.session?.status ?? canceled.runnerTrace?.sessionStatus ?? canceled.status)
? undefined
: canceled.sessionId,
threadId: isTerminalSessionStatus(canceled.session?.status ?? canceled.runnerTrace?.sessionStatus ?? canceled.status)
? undefined
: canceled.threadId ?? threadIdFrom(canceled),
retryOf: canceled.traceId
});
}
async function retryAgentMessage(messageId) {
const message = state.chatMessages.find((item) => item.id === messageId);
if (!message?.retryInput) return;
if (state.chatPending) {
setAgentActionStatus(messageId, "当前仍有请求处理中;请先取消当前请求,或等待真实超时/后端结果后再重试。");
return;
}
await submitAgentMessage(message.retryInput, {
conversationId: message.conversationId || state.conversationId || undefined,
sessionId: isTerminalSessionStatus(message.session?.status ?? message.runnerTrace?.sessionStatus ?? message.status)
? undefined
: message.sessionId,
threadId: isTerminalSessionStatus(message.session?.status ?? message.runnerTrace?.sessionStatus ?? message.status)
? undefined
: message.threadId ?? threadIdFrom(message),
retryOf: message.traceId
});
}
function setAgentActionStatus(messageId, text) {
const index = state.chatMessages.findIndex((message) => message.id === messageId);
if (index < 0) return;
state.chatMessages[index] = {
...state.chatMessages[index],
traceReplayStatus: text,
updatedAt: new Date().toISOString()
};
renderConversation();
}
async function cancelAgentMessage(messageId) {
const index = state.chatMessages.findIndex((message) => message.id === messageId);
if (index < 0) return;
const message = state.chatMessages[index];
const traceId = message.traceId;
const sessionId = message.sessionId || message.runnerTrace?.sessionId || state.currentRequest?.sessionId || state.sessionId;
const threadId = message.threadId || message.runnerTrace?.threadId || state.currentRequest?.threadId || state.threadId;
const response = await cancelAgentRequest({
traceId,
conversationId: message.conversationId || state.conversationId,
sessionId,
threadId
});
const payload = response.data ?? {};
const runnerTrace = payload.runnerTrace ?? latestTraceSnapshot(traceId);
if (response.ok && payload.canceled === true) {
state.canceledTraces.add(traceId);
stopRunningTraceStream(traceId);
state.chatPending = false;
state.currentRequest = null;
state.sessionId = payload.sessionId || sessionId || state.sessionId;
state.sessionStatus = payload.session?.status ?? runnerTrace?.sessionStatus ?? "canceled";
state.chatMessages[index] = {
...message,
title: "Code Agent 已取消",
text: payload.userMessage || "当前 Codex stdio 请求已取消;输入、sessionId、traceId 和最后 trace event 已保留,可重试上一条消息。",
status: "canceled",
sessionId: payload.sessionId || sessionId || message.sessionId,
threadId: payload.threadId || threadId || message.threadId,
sessionContinuity: failedSessionContinuity(message.sessionContinuity, { code: "codex_stdio_canceled" }),
session: payload.session ?? message.session,
runnerTrace,
traceReplayStatus: lastTraceEventLabel(runnerTrace),
updatedAt: new Date().toISOString(),
error: payload.error ?? {
code: "codex_stdio_canceled",
category: "canceled",
retryable: true,
message: "user canceled current Code Agent request"
}
};
if (message.retryInput) el.commandInput.value = message.retryInput;
} else {
state.chatMessages[index] = {
...message,
title: "Code Agent 取消受阻",
text: payload.error?.userMessage || response.error || "当前请求没有可取消的 in-flight Codex stdio session;输入和 trace 已保留。",
status: "failed",
threadId,
sessionContinuity: failedSessionContinuity(message.sessionContinuity, { code: "cancel_blocked" }),
runnerTrace,
traceReplayStatus: lastTraceEventLabel(runnerTrace),
updatedAt: new Date().toISOString(),
error: payload.error ?? {
code: "cancel_blocked",
category: "cancel_blocked",
retryable: true,
message: response.error || "cancel blocked"
}
};
if (message.retryInput) el.commandInput.value = message.retryInput;
}
renderAgentChatStatus(state.chatMessages[index].status, state.chatMessages[index]);
renderCodeAgentSummary();
renderConversation();
renderDrafts();
renderRecords(state.liveSurface);
}
function stopRunningTraceStream(traceId) {
const id = nonEmptyString(traceId);
if (!id) return;
const close = state.traceStreams.get(id);
if (typeof close === "function") close();
state.traceStreams.delete(id);
}
async function cancelAgentRequest({ traceId, conversationId, sessionId, threadId }) {
return fetchJson("/v1/agent/chat/cancel", {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-Trace-Id": traceId,
"Prefer": "respond-async"
},
timeoutMs: CODE_AGENT_CANCEL_TIMEOUT_MS,
timeoutName: "Code Agent cancel",
body: JSON.stringify({
traceId,
conversationId,
sessionId,
threadId
})
});
}
async function replayAgentTrace(messageId) {
const index = state.chatMessages.findIndex((message) => message.id === messageId);
if (index < 0) return;
const message = state.chatMessages[index];
if (!message.traceId) return;
const response = await fetchJson(`/v1/agent/chat/trace/${encodeURIComponent(message.traceId)}`, {
timeoutMs: Math.min(API_TIMEOUT_MS, 5000),
timeoutName: "Code Agent trace replay"
});
if (response.ok) {
const runnerTrace = runnerTraceFromSnapshot(response.data, message.runnerTrace);
state.chatMessages[index] = {
...message,
runnerTrace,
threadId: runnerTrace.threadId ?? message.threadId,
traceReplayStatus: `已回放 ${runnerTrace.events?.length ?? 0} 个真实 trace event${lastTraceEventLabel(runnerTrace)}`,
updatedAt: response.data?.updatedAt ?? new Date().toISOString()
};
} else {
state.chatMessages[index] = {
...message,
traceReplayStatus: response.error || "trace 回放失败",
updatedAt: new Date().toISOString()
};
}
renderCodeAgentSummary();
renderConversation();
renderRecords(state.liveSurface);
}
function lastTraceEventLabel(runnerTrace) {
const event = runnerTrace?.lastEvent ?? (Array.isArray(runnerTrace?.events) ? runnerTrace.events.at(-1) : null);
if (!event) return "lastEvent=none";
return `lastEvent=${event.label ?? `${event.type ?? "event"}:${event.status ?? "observed"}`}`;
}
function messageSessionContinuityPanel(message) {
if (message.role !== "agent" || message.status === "running") return null;
const continuity = message.sessionContinuity ?? completedSessionContinuity(message, pendingSessionContinuity({
conversationId: message.conversationId,
sessionId: message.sessionId,
threadId: message.threadId,
retryOf: message.retryOf
}));
if (!continuity) return null;
const fields = [
["状态", continuity.label],
["conversation", continuity.returned?.conversationId ?? message.conversationId],
["session", continuity.returned?.sessionId ?? message.sessionId ?? "未确认"],
["thread", continuity.returned?.threadId ?? message.threadId ?? "未确认"],
["lifecycle", message.sessionLifecycleStatus ?? message.sessionSummary?.status ?? message.session?.lifecycleStatus],
["提示", message.sessionSummary?.userMessage ?? message.sessionLifecycle?.userMessage],
["requestedSession", continuity.requested?.sessionId],
["requestedThread", continuity.requested?.threadId],
["retryOf", continuity.requested?.retryOf ?? message.retryOf],
["missing", continuity.missing?.join("、")],
["changed", continuity.changed?.join("、")]
];
const list = document.createElement("div");
list.className = "message-session-grid";
for (const [label, value] of fields) {
if (value === undefined || value === null || value === "") continue;
const item = document.createElement("div");
item.className = "message-session-row";
item.append(textSpan(label, "message-session-key"), textSpan(value, "message-session-value mono"));
if (label === "conversation" || label === "session" || label === "thread") {
item.append(copyButton(value, label));
}
list.append(item);
}
return messageCompactDetails({
className: "message-session-context",
tone: continuity.tone,
badgeLabel: continuity.label,
badgeTone: continuity.tone,
summary: continuity.summary,
dialogTitle: "会话明细",
body: list
});
}
function messagePendingContextPanel(message) {
if (message.status !== "running") return null;
const continuity = message.sessionContinuity ?? pendingSessionContinuity({
conversationId: message.conversationId,
sessionId: message.sessionId,
threadId: message.threadId,
retryOf: message.retryOf
});
const fields = [
["状态", continuity.label],
["traceId", message.traceId],
["conversation", message.conversationId],
["session", message.sessionId ?? message.runnerTrace?.sessionId ?? "等待后端分配"],
["thread", message.threadId ?? message.runnerTrace?.threadId ?? "等待后端分配"],
["sessionStatus", message.runnerTrace?.sessionStatus],
["lifecycle", message.runnerTrace?.sessionLifecycleStatus],
["activityTimeout", `${formatTraceDuration(CODE_AGENT_TIMEOUT_MS)} 无新事件`]
];
const section = document.createElement("section");
section.className = "message-pending-context tone-border-pending";
const header = document.createElement("div");
header.className = "message-pending-head";
header.append(badge("处理中", "pending"), textSpan(continuity.summary, "message-pending-summary"));
const list = document.createElement("div");
list.className = "message-pending-grid";
for (const [label, value] of fields) {
if (value === undefined || value === null || value === "") continue;
const item = document.createElement("div");
item.className = "message-pending-row";
item.append(textSpan(label, "message-pending-key"), textSpan(value, "message-pending-value mono"));
if (label === "traceId" || label === "conversation" || label === "session") {
item.append(copyButton(value, label));
}
list.append(item);
}
section.append(header, list);
return section;
}
function messageM3EvidencePanel(message) {
const evidence = message.m3Evidence ?? extractCodeAgentM3Evidence(message);
if (!evidence) return null;
const section = document.createElement("section");
section.className = `message-m3-evidence tone-border-${toneClass(evidence.verdict.tone)}`;
section.dataset.m3Verdict = evidence.verdict.key;
section.append(m3EvidenceHeader(evidence));
const rows = document.createElement("div");
rows.className = "message-m3-rows";
rows.append(...m3EvidenceRows(evidence).map(m3EvidenceRowElement));
section.append(rows);
section.append(m3EvidenceRawDetails(evidence, message));
return section;
}
function messageRuntimePathPanel(message) {
if (message.role !== "agent") return null;
const runtimePath = codeAgentRuntimePathFromMessage(message);
if (!runtimePath) return null;
const rows = document.createElement("div");
rows.className = "message-runtime-grid";
for (const row of runtimePath.rows) {
const item = document.createElement("div");
item.className = `message-runtime-row${row.missing ? " message-runtime-row-missing" : ""}`;
item.append(textSpan(row.label, "message-runtime-key"), textSpan(row.value, "message-runtime-value mono"));
if (!row.missing && (row.label === "providerTrace.command" || row.label === "providerTrace.terminalStatus" || row.label === "protocol")) {
item.append(copyButton(row.value, row.label));
}
rows.append(item);
}
if (runtimePath.missingFields.length > 0) {
const item = document.createElement("div");
item.className = "message-runtime-row message-runtime-row-missing message-runtime-row-wide";
item.append(
textSpan("缺失字段", "message-runtime-key"),
textSpan(runtimePath.missingFields.join(", "), "message-runtime-value mono")
);
rows.append(item);
}
const details = messageCompactDetails({
className: "message-runtime-path",
tone: runtimePath.tone,
badgeLabel: runtimePath.label,
badgeTone: runtimePath.tone,
summary: runtimePath.summary,
dialogTitle: "运行路径明细",
body: rows
});
details.dataset.runtimePathKind = runtimePath.kind;
return details;
}
function messageCompactDetails({ className, tone, badgeLabel, badgeTone, summary, dialogTitle, body }) {
const section = document.createElement("section");
section.className = `${className} message-compact-details tone-border-${toneClass(tone)}`;
const trigger = document.createElement("button");
trigger.type = "button";
trigger.className = "message-compact-summary";
trigger.setAttribute("aria-haspopup", "dialog");
trigger.append(
badge(badgeLabel, badgeTone),
textSpan(summary, "message-compact-summary-text")
);
trigger.addEventListener("click", () => {
openWorkbenchDialog({
title: dialogTitle,
body,
restoreFocus: trigger
});
});
section.append(trigger);
return section;
}
function m3EvidenceHeader(evidence) {
const header = document.createElement("div");
header.className = "message-m3-head";
header.append(
badge(evidence.verdict.label, evidence.verdict.tone),
textSpan(m3EvidenceSummaryText(evidence), "message-m3-summary")
);
return header;
}
function m3EvidenceRowElement(row) {
const item = document.createElement("div");
item.className = `message-m3-row tone-border-${toneClass(row.tone)}`;
item.append(textSpan(row.label, "message-m3-key"));
const value = row.href ? safeLink(row.href, row.value, "message-m3-value mono") : textSpan(row.value, "message-m3-value mono");
item.append(value);
if (row.copyable) {
item.append(copyButton(row.value, row.label));
}
return item;
}
function m3EvidenceRawDetails(evidence, message) {
const details = document.createElement("details");
details.className = "message-m3-raw";
const summary = document.createElement("summary");
summary.textContent = "原始字段";
const pre = document.createElement("pre");
pre.textContent = boundedJson({
responseType: message.responseType,
m3Io: message.m3Io ?? null,
toolCall: evidence.source ?? null,
providerTrace: message.providerTrace ?? null,
runnerTrace: message.runnerTrace ?? null,
blocker: message.blocker ?? null,
blockers: message.blockers ?? null
}, 6000);
details.append(summary, pre);
return details;
}
function boundedJson(value, maxLength = 6000) {
let text = "";
try {
text = JSON.stringify(value, null, 2);
} catch {
text = String(value ?? "");
}
return text.length > maxLength ? `${text.slice(0, maxLength)}\n...字段已截断` : text;
}
function messageTracePanel(message) {
const trace = message.runnerTrace && typeof message.runnerTrace === "object" ? message.runnerTrace : null;
if (!trace && !message.traceId) return null;
const details = document.createElement("details");
details.className = "message-trace";
const traceUiKey = messageTraceUiKey(message);
const storedOpen = traceUiKey ? state.traceDetailsOpen.get(traceUiKey) : undefined;
details.open = typeof storedOpen === "boolean" ? storedOpen : defaultTraceDetailsOpen(message);
if (traceUiKey) {
details.addEventListener("toggle", () => {
state.traceDetailsOpen.set(traceUiKey, details.open);
});
}
const summary = document.createElement("summary");
summary.className = "message-meta";
summary.textContent = runnerTraceHeadline(message, trace);
const list = document.createElement("ol");
list.className = "message-trace-events";
const events = Array.isArray(trace?.events) ? trace.events : [];
const rows = traceDisplayRows(trace, events);
if (events.length === 0) {
const item = document.createElement("li");
item.textContent = `trace=${message.traceId ?? "pending"};等待后端事件。`;
list.append(item);
} else {
const toolbar = messageTraceToolbar(message, trace, events, rows);
list.dataset.traceMode = "all";
renderTraceEventList(list, rows);
details.append(summary, toolbar, list);
return details;
}
details.append(summary, list);
return details;
}
function messageTraceUiKey(message) {
const traceId = nonEmptyString(message.traceId);
const messageId = nonEmptyString(message.id ?? message.messageId);
if (!traceId && !messageId) return null;
return `${messageId ?? "message"}:${traceId ?? "trace"}`;
}
function defaultTraceDetailsOpen(message) {
return ["running", "completed", "source", "failed", "timeout", "canceled", "error"].includes(message.status);
}
function messageTraceToolbar(message, trace, events, rows) {
const toolbar = document.createElement("div");
toolbar.className = "message-trace-toolbar";
const count = textSpan(messageTraceCountText(events.length, rows.length), "message-trace-count");
toolbar.append(count);
toolbar.append(traceActionButton("复制 JSON", () => copyTextToClipboard(messageTraceJson(message, trace, events)), "复制完整 trace JSON"));
toolbar.append(traceActionButton("下载 trace", () => downloadTraceJson(message, trace, events), "下载完整 trace JSON 文件"));
return toolbar;
}
function messageTraceCountText(rawTotal, displayTotal) {
return `显示全部 ${displayTotal} / 原始 ${rawTotal}`;
}
function renderTraceEventList(list, rows) {
list.replaceChildren();
for (const row of rows) {
const item = document.createElement("li");
item.className = `message-trace-row tone-border-${toneClass(row.tone)}`;
const header = document.createElement("div");
header.className = "message-trace-line";
header.textContent = row.header;
item.append(header);
if (row.body) {
const body = document.createElement("pre");
body.className = "message-trace-body";
body.textContent = row.body;
item.append(body);
}
list.append(item);
}
}
function traceDisplayRows(trace, events) {
const rows = [];
let noisyRun = [];
const flushNoisyRun = () => {
if (noisyRun.length === 0) return;
const row = traceNoiseSummaryRow(trace, noisyRun);
if (row) rows.push(row);
noisyRun = [];
};
for (const event of events) {
if (isNoisyTraceEvent(event)) {
noisyRun.push(event);
continue;
}
flushNoisyRun();
const row = traceDisplayRow(trace, event);
if (!row) continue;
rows.push(row);
}
flushNoisyRun();
return rows.length > 0 ? rows : events.map((event) => traceDisplayRow(trace, event, { includeNoise: true })).filter(Boolean);
}
function traceNoiseSummaryRow(trace, events) {
const visibleEvents = events.filter(Boolean);
if (visibleEvents.length === 0) return null;
const last = visibleEvents.at(-1);
const clock = traceClock(last.createdAt);
const total = formatTraceDuration(traceRelativeMs(trace, last));
const assistantChunks = visibleEvents.filter((event) => isAssistantChunkTraceEvent(event));
const label = assistantChunks.length === visibleEvents.length
? `assistant stream x${visibleEvents.length}`
: `trace noise x${visibleEvents.length}`;
return {
seq: last.seq ?? null,
tone: "source",
header: `${clock} total=${total} stream ${label}`,
body: traceNoiseSummaryBody(visibleEvents, assistantChunks)
};
}
function traceNoiseSummaryBody(events, assistantChunks) {
if (assistantChunks.length > 0) {
const text = assistantChunks
.map((event) => cleanTraceText(event.chunk))
.join("")
.trim();
const waitingFor = events.at(-1)?.waitingFor ? `waiting=${events.at(-1).waitingFor}` : null;
const recent = text ? compactTraceTextTail(text, 900) : null;
return [
`compressed=${assistantChunks.length} assistant chunks`,
waitingFor,
recent ? `recent=${recent}` : null
].filter(Boolean).join("\n");
}
const labels = new Map();
for (const event of events) {
const label = String(event?.label ?? `${event?.type ?? "event"}:${event?.status ?? "observed"}`);
labels.set(label, (labels.get(label) ?? 0) + 1);
}
return [...labels.entries()]
.slice(0, 6)
.map(([label, count]) => `${readableTraceLabel({ label })} x${count}`)
.join("\n");
}
function compactTraceTextTail(text, maxLength) {
const clean = cleanTraceText(text);
if (clean.length <= maxLength) return clean;
return `...${clean.slice(-maxLength)}`;
}
function traceDisplayRow(trace, event, options = {}) {
if (!options.includeNoise && isNoisyTraceEvent(event)) return null;
const clock = traceClock(event.createdAt);
const total = formatTraceDuration(traceRelativeMs(trace, event));
const status = traceStatusToken(event);
const label = readableTraceLabel(event);
const meta = [
status,
event.errorCode ? `error=${event.errorCode}` : null,
typeof event.timeoutMs === "number" ? `timeout=${formatTraceDuration(event.timeoutMs)}` : null,
typeof event.idleMs === "number" ? `idle=${formatTraceDuration(event.idleMs)}` : null,
event.outputSummary ? `out=${compactTraceSize(event.outputSummary)}` : null
].filter(Boolean).join(" ");
const body = traceDisplayBody(event);
return {
seq: event.seq ?? null,
tone: traceEventTone(event),
header: `${clock} total=${total}${meta ? ` ${meta}` : ""}${label ? ` ${label}` : ""}`,
body
};
}
function isNoisyTraceEvent(event) {
const label = String(event?.label ?? "");
if (isAssistantChunkTraceEvent(event)) return true;
if (/token_count|outputDelta:chunk/iu.test(label)) return true;
if (event?.type === "event" && !event.outputSummary && !event.message && !event.errorCode) return true;
if (event?.label === "request:accepted" && !event.message) return true;
return false;
}
function isAssistantChunkTraceEvent(event) {
const label = String(event?.label ?? "");
return /assistant:chunk/iu.test(label) ||
(event?.type === "assistant_message" && event?.status === "chunk");
}
function readableTraceLabel(event) {
const label = String(event?.label ?? `${event?.type ?? "event"}:${event?.status ?? "observed"}`);
if (label === "request:accepted-short-connection") return "submit short-connection";
if (label === "request:accepted") return "request accepted";
if (label.startsWith("tool:codex-app-server.thread/start+turn/start")) return "codex turn start";
if (label.startsWith("tool:codex-app-server.thread/resume+turn/start")) return "codex turn resume";
if (label.startsWith("item/commandExecution/outputDelta")) return "cmd output";
if (label === "turn:waiting:first_assistant_token") return "waiting first assistant token";
if (label.startsWith("turn:completed")) return "turn completed";
if (label.startsWith("result:completed")) return "result ready";
if (label.startsWith("result:failed")) return "result failed";
if (label.startsWith("timeout:")) return "timeout";
if (label.startsWith("cancel:")) return "cancel";
return label
.replace(/^tool:/u, "")
.replace(/^assistant:/u, "assistant ")
.replace(/:completed:completed$/u, " completed")
.replace(/:completed$/u, " completed")
.replace(/:started$/u, " started");
}
function traceDisplayBody(event) {
const lines = [
event.promptSummary,
event.outputSummary,
event.message,
event.chunk && !isNoisyTraceEvent(event) ? event.chunk : null,
event.waitingFor && event.status !== "completed" ? `waiting=${event.waitingFor}` : null
]
.map((value) => cleanTraceText(value))
.filter(Boolean);
return lines.join("\n").slice(0, 1400);
}
function traceStatusToken(event) {
const status = String(event?.status ?? "");
if (status === "completed" || status === "succeeded") return "ok";
if (["failed", "blocked", "error", "timeout", "canceled"].includes(status) || event?.errorCode) return "fail";
if (["started", "running", "accepted"].includes(status)) return "run";
return status || "event";
}
function traceEventTone(event) {
const status = traceStatusToken(event);
if (status === "ok") return "ok";
if (status === "fail") return "blocked";
if (status === "run") return "warn";
return "source";
}
function traceClock(value) {
const date = new Date(value);
return Number.isNaN(date.getTime()) ? "--:--:--" : date.toISOString().slice(11, 19);
}
function traceRelativeMs(trace, event) {
if (typeof event?.elapsedMs === "number") return event.elapsedMs;
const start = Date.parse(trace?.startedAt ?? trace?.createdAt ?? "");
const current = Date.parse(event?.createdAt ?? "");
return Number.isNaN(start) || Number.isNaN(current) ? 0 : Math.max(0, current - start);
}
function formatTraceDuration(ms) {
const total = Math.max(0, Math.floor(Number(ms) || 0));
const seconds = Math.floor(total / 1000);
const hh = String(Math.floor(seconds / 3600)).padStart(2, "0");
const mm = String(Math.floor((seconds % 3600) / 60)).padStart(2, "0");
const ss = String(seconds % 60).padStart(2, "0");
return `${hh}:${mm}:${ss}`;
}
function compactTraceSize(value) {
const length = String(value ?? "").length;
if (length >= 1000) return `${(length / 1000).toFixed(length >= 10000 ? 0 : 1)}k`;
return String(length);
}
function cleanTraceText(value) {
const text = String(value ?? "").trim();
if (!text) return "";
return text
.replace(/\s*\/\s*/gu, " / ")
.replace(/[ \t]{2,}/gu, " ")
.replace(/\n{3,}/gu, "\n\n");
}
function traceActionButton(label, onClick, title) {
const button = document.createElement("button");
button.type = "button";
button.className = "message-trace-action";
button.textContent = label;
button.title = title;
button.addEventListener("click", onClick);
return button;
}
function messageTraceJson(message, trace, events) {
return JSON.stringify({
traceId: trace?.traceId ?? message.traceId ?? null,
messageId: message.id ?? null,
eventCount: events.length,
lastEvent: trace?.lastEvent ?? events.at(-1) ?? null,
events
}, null, 2);
}
function downloadTraceJson(message, trace, events) {
const traceId = String(trace?.traceId ?? message.traceId ?? "trace")
.replace(/[^A-Za-z0-9_.-]+/gu, "-")
.replace(/^-|-$/gu, "")
.slice(0, 96) || "trace";
const blob = new Blob([messageTraceJson(message, trace, events)], { type: "application/json" });
const href = URL.createObjectURL(blob);
const anchor = document.createElement("a");
anchor.href = href;
anchor.download = `${traceId}.json`;
document.body.append(anchor);
anchor.click();
anchor.remove();
window.setTimeout(() => URL.revokeObjectURL(href), 0);
}
function runnerTraceHeadline(message, trace) {
const count = Array.isArray(trace?.events) ? trace.events.length : 0;
const last = trace?.lastEvent?.label ?? trace?.eventLabels?.at?.(-1) ?? "等待事件";
const elapsed = typeof trace?.elapsedMs === "number" ? ` / ${trace.elapsedMs}ms` : "";
const waiting = trace?.waitingFor ? ` / 等待 ${trace.waitingFor}` : "";
const thread = trace?.threadId ?? message.threadId ? ` / thread=${trace?.threadId ?? message.threadId}` : "";
return `运行 trace ${trace?.traceId ?? message.traceId ?? "pending"} / events=${count} / last=${last}${elapsed}${waiting}${thread}`;
}
function traceEventMeta(event) {
return [
event.toolName ? `tool=${event.toolName}` : null,
event.waitingFor ? `waiting=${event.waitingFor}` : null,
typeof event.timeoutMs === "number" ? `timeoutMs=${event.timeoutMs}` : null,
typeof event.hardTimeoutMs === "number" ? `hardTimeoutMs=${event.hardTimeoutMs}` : null,
typeof event.idleMs === "number" ? `idleMs=${event.idleMs}` : null,
event.lastActivityAt ? `lastActivityAt=${event.lastActivityAt}` : null,
typeof event.elapsedMs === "number" ? `${event.elapsedMs}ms` : null,
event.errorCode ? `error=${event.errorCode}` : null,
event.outputSummary,
event.message
].filter(Boolean).join(" / ");
}
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 conversationFactsSummary(conversationFacts) {
if (!conversationFacts || typeof conversationFacts !== "object") return "none";
return [
recordField("turns", conversationFacts.turnCount),
recordField("sessionId", conversationFacts.sessionId),
recordField("status", conversationFacts.sessionStatus),
recordField("workspace", conversationFacts.workspace),
recordField("mode", conversationFacts.sessionMode),
conversationFacts.valuesRedacted === true ? "valuesRedacted=true" : null
].filter(Boolean).join(" / ") || "none";
}
function conversationFactTracesSummary(conversationFacts) {
if (!conversationFacts || typeof conversationFacts !== "object") return "none";
const traces = Array.isArray(conversationFacts.traceIds) ? conversationFacts.traceIds.filter(Boolean) : [];
return traces.length ? traces.slice(-4).join(",") : conversationFacts.latestTraceId ?? "none";
}
function conversationFactSkillsSummary(conversationFacts) {
const skills = conversationFacts?.latestSkills;
if (!skills || typeof skills !== "object") return "none";
const names = Array.isArray(skills.names) ? skills.names.filter(Boolean).slice(0, 6).join(",") : "";
return [
skills.status ?? "unknown",
recordField("count", skills.totalCount ?? skills.count),
names ? `items=${names}` : null
].filter(Boolean).join(":") || "none";
}
function conversationFactToolsSummary(conversationFacts) {
const toolCalls = Array.isArray(conversationFacts?.recentToolCalls) ? conversationFacts.recentToolCalls : [];
if (toolCalls.length === 0) return "none";
return toolCalls
.map((toolCall) => [toolCall.name ?? "tool", toolCall.status ? `(${toolCall.status})` : null].filter(Boolean).join(""))
.slice(-5)
.join(",");
}
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.threadId ? `thread=${session.threadId}` : null,
session.lastTraceId ? `lastTrace=${session.lastTraceId}` : null
].filter(Boolean);
return parts.join(":");
}
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 badge(text, tone = text) {
const span = document.createElement("span");
span.className = `badge tone-${toneClass(tone)}`;
span.textContent = text ?? "无";
return span;
}
function copyButton(value, label = "value") {
const button = document.createElement("button");
button.className = "copy-chip";
button.type = "button";
button.textContent = "复制";
button.title = `复制 ${label}`;
button.addEventListener("click", async () => {
await copyTextToClipboard(value);
button.textContent = "已复制";
window.setTimeout(() => {
button.textContent = "复制";
}, 1200);
});
return button;
}
async function copyTextToClipboard(value) {
const text = String(value ?? "");
if (navigator.clipboard?.writeText) {
await navigator.clipboard.writeText(text);
return;
}
const input = document.createElement("textarea");
input.value = text;
input.setAttribute("readonly", "");
input.style.position = "fixed";
input.style.left = "-9999px";
document.body.append(input);
input.select();
document.execCommand("copy");
input.remove();
}
function safeLink(href, text, className) {
const anchor = document.createElement("a");
anchor.href = href;
anchor.textContent = text ?? href;
anchor.rel = "noreferrer";
if (className) anchor.className = className;
return anchor;
}
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 状态:Codex stdio gate 未通过",
text: `同源服务可响应,但完整 Codex stdio 长会话仍未通过 gate;不会用本地技能发现、shell/file 快捷命令或文本 fallback 冒充回复。${codeAgentBlockerDetail(availability)}`,
status: "blocked"
};
}
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 状态:等待 Codex stdio 探测",
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 `输入区会调用受控接口;完整 Codex stdio 长会话尚未可用时只返回结构化失败,不能用本地 shortcut 或 text fallback 冒充回复。${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 = codeAgentBlockerCodes(availability);
if (blockers.length === 0 && availability?.reason) blockers.push(availability.reason);
if (blockers.length === 0) return "";
const labels = blockers.slice(0, 3).map(codeAgentBlockerChineseLabel);
return ` 阻塞:${labels.join("")}。证据 code=${blockers.slice(0, 3).join(",")}。`;
}
function codeAgentBlockerCodes(availability) {
const values = [
...(Array.isArray(availability?.blockers) ? availability.blockers.map((blocker) => blocker?.code) : []),
...(Array.isArray(availability?.blockerCodes) ? availability.blockerCodes : []),
...(Array.isArray(availability?.longLivedSessionGate?.blockers) ? availability.longLivedSessionGate.blockers.map((blocker) => blocker?.code) : []),
...(Array.isArray(availability?.codexStdioFeasibility?.blockers) ? availability.codexStdioFeasibility.blockers.map((blocker) => blocker?.code) : []),
...(Array.isArray(availability?.runtimeContract?.stdioProtocol?.missingTools) && availability.runtimeContract.stdioProtocol.missingTools.length > 0 ? ["stdio_protocol_not_wired"] : []),
availability?.runtimeContract?.binary?.status === "missing" ? "codex_cli_binary_missing" : null,
availability?.runtimeContract?.lifecycleSupervisor?.status === "blocked" ? "runner_lifecycle_missing" : null
].filter(Boolean).map((code) => String(code));
return [...new Set(values)];
}
function codeAgentBlockerChineseLabel(code) {
return {
codex_cli_binary_missing: "未找到受控 Codex CLI binary",
codex_cli_not_executable: "Codex CLI 无法执行 --version",
codex_cli_native_dependency_missing: "Codex CLI native 依赖缺失",
runner_lifecycle_missing: "缺少 repo-owned lifecycle supervisor",
stdio_protocol_not_wired: "Codex stdio 协议尚未接入",
codex_stdio_supervisor_disabled: "Codex stdio supervisor 未启用",
codex_stdio_blocked_readonly_session_available: "旧 partial runner 不能作为 Codex stdio",
controlled_readonly_not_long_lived_stdio: "非 Codex stdio 长会话",
openai_responses_fallback_not_session: "文本 fallback 不是长会话",
one_shot_runner_not_long_lived: "一次性执行不是可复用 session",
provider_token_boundary: "token 边界未配置",
codex_home_missing: "CODEX_HOME 不存在或不可读",
codex_home_write_blocked: "CODEX_HOME 不可写",
workspace_mount_missing: "工作区挂载不可读",
workspace_write_boundary_blocked: "workspace-write 沙箱不可写",
codex_stdio_egress_boundary: "DEV egress 边界不合规"
}[code] ?? code;
}
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 可响应;完整 Codex stdio 长会话仍受阻,本地 shortcut 与文本 fallback 不会冒充真实 Code Agent 回复。${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 (isCodeAgentM3SkillCompletion(value)) {
return true;
}
if (isCodexRunnerCapableEvidence(value)) {
return true;
}
if (CODEX_RUNNER_CAPABLE_PROVIDERS.includes(String(value?.provider ?? "").trim().toLowerCase())) {
return false;
}
if (isTextFallbackChatResult(value)) {
return false;
}
return (
isTrustedCodeAgentProvider(value?.provider) &&
Boolean(value?.model) &&
Boolean(value?.backend) &&
Boolean(value?.traceId) &&
Boolean(value?.conversationId || value?.sessionId) &&
hasProviderTrace(value) &&
Boolean(sessionIdFrom(value)) &&
(!requiresCodexThreadEvidence(value) || Boolean(threadIdFrom(value))) &&
!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_APP_SERVER_RUNNER_KIND &&
value?.capabilityLevel === "long-lived-codex-stdio-session" &&
value?.session?.status === "idle" &&
typeof value?.session?.idleTimeoutMs === "number" &&
Boolean(value?.session?.lastTraceId) &&
value?.sessionMode === CODEX_APP_SERVER_SESSION_MODE &&
value?.implementationType === CODEX_APP_SERVER_IMPLEMENTATION_TYPE &&
Boolean(value?.sessionReuse) &&
hasProviderTrace(value) &&
Boolean(sessionIdFrom(value)) &&
Boolean(threadIdFrom(value)) &&
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) &&
["workspace-write", "danger-full-access"].includes(value?.sandbox || value?.runner?.sandbox) &&
Array.isArray(value?.toolCalls) &&
value.toolCalls.some((tool) => tool?.status === "completed") &&
value?.longLivedSessionGate?.status === "pass" &&
value?.providerTrace?.protocol === CODEX_APP_SERVER_PROTOCOL &&
/\bcodex\s+app-server\s+--listen\s+stdio:\/\//u.test(String(value?.providerTrace?.command ?? "")) &&
["completed", "succeeded"].includes(String(value?.providerTrace?.terminalStatus ?? "").toLowerCase()) &&
Boolean(value?.runnerTrace) &&
Boolean(value?.skills)
);
}
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 === "canceled") return "canceled";
if (latest?.status === "timeout") return "timeout";
if (latest?.status === "error") return "error";
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" || ["failed", "timeout", "canceled", "error"].includes(latestChatResult()?.status)) 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 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"));
}