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 = 180000; 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 }); const CODE_AGENT_TIMEOUT_MS = resolveTimeoutMs("codeAgentTimeoutMs", DEFAULT_CODE_AGENT_TIMEOUT_MS, { min: 50, max: 300000 }); const CODE_AGENT_TRACE_PREVIEW_LIMIT = 14; 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"), liveBuildPopover: byId("live-build-popover"), liveBuildClose: byId("live-build-close"), 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"), taskList: byId("task-list"), gateSourceStatus: byId("gate-source-status"), gateStatusFilter: byId("gate-status-filter"), gateSearch: byId("gate-search"), gateRefresh: byId("gate-refresh"), gateReviewBody: byId("gate-review-body"), commandForm: byId("command-form"), commandInput: byId("command-input"), commandSend: byId("command-send"), commandClear: byId("command-clear"), hardwareSourceStatus: byId("hardware-source-status"), hardwareList: byId("hardware-list"), controlList: byId("control-list"), wiringBody: byId("wiring-body"), recordsList: byId("records-list"), m3ControlForm: byId("m3-control-form"), m3ControlStatus: byId("m3-control-status"), 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") }; await ensureWorkbenchAuth({ loginShell: el.loginShell, appShell: el.shell }); const state = { conversationId: null, sessionId: null, threadId: null, codeAgentAvailability: null, chatMessages: [], traceStreams: 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(); 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 initLiveBuildOverlay() { syncLiveBuildOverlayState(); el.liveBuildSummary.addEventListener("toggle", syncLiveBuildOverlayState); el.liveBuildClose.addEventListener("click", () => closeLiveBuildOverlay({ restoreFocus: true })); document.addEventListener("keydown", (event) => { if (event.key === "Escape" && el.liveBuildSummary.open) { event.preventDefault(); closeLiveBuildOverlay({ restoreFocus: true }); } }); document.addEventListener("click", (event) => { if (!el.liveBuildSummary.open || el.liveBuildSummary.contains(event.target)) return; closeLiveBuildOverlay(); }); } function syncLiveBuildOverlayState() { const expanded = el.liveBuildSummary.open === true; el.liveBuildToggle.setAttribute("aria-expanded", expanded ? "true" : "false"); el.liveBuildPopover.setAttribute("aria-hidden", expanded ? "false" : "true"); } function closeLiveBuildOverlay(options = {}) { if (!el.liveBuildSummary.open) return; el.liveBuildSummary.open = false; syncLiveBuildOverlayState(); if (options.restoreFocus === true) el.liveBuildToggle.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.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 请求;复杂问题可能需要 1-2 分钟。页面会保留 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(); renderTaskList(); renderHardwareStatus(null); renderDrafts(); renderM3ControlStatus(); renderWiringList(null); renderRecords(null); renderGateTable(); renderCodeAgentSummary(); } function renderProbePending() { el.liveStatus.textContent = "等待验证"; el.liveStatus.className = "tone-pending"; el.liveDetail.textContent = "正在验证 hwlab-cloud-api /health/live、/v1、/v1/agent/chat 与 /v1/m3/io;未完成前不标为通过。"; 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_TIMEOUT_MS, timeoutName: "Code Agent", activityRef: () => state.currentRequest, body: JSON.stringify({ message, conversationId, sessionId, threadId, traceId, projectId: gateSummary.topology.projectId }) }); if (!response.ok) { if (isBlockedAgentResponse(response.data, response.status)) { return normalizeBlockedAgentResult(response.data, response.status, traceId, response.error); } const error = agentErrorFromHttpResponse(response, traceId); throw error; } 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 renderTaskList() { const rows = [ { title: "1. 描述目标", detail: "例如要检查哪个 BOX-SIMU、哪个端口、期望看到什么信号。", tone: "source" }, { title: "2. 核对接线", detail: "打开右侧“接线”表,确认 res_boxsimu_1 与 res_boxsimu_2 的 DO/DI 对应关系,以及接线盘和轨迹/证据摘要。", tone: "source" }, { title: "3. 留存记录", detail: "在“可信记录”面板查看已有证据;新增硬件动作不从前端直接发起。", tone: "source" } ]; replaceChildren(el.taskList, ...rows.map(infoCard)); } function renderHardwareStatus(m3Status) { syncHardwareTabs(); const status = m3Status ?? sourceFallbackM3Status(); const sourceKind = status.sourceKind ?? "UNVERIFIED"; const trustedGreen = status.trust?.durableStatus === "green"; const statusTone = status.status === "live" && trustedGreen ? "dev-live" : status.status === "error" || status.status === "blocked" ? "blocked" : sourceKind === "DEV-LIVE" ? "dev-live" : "source"; el.hardwareSourceStatus.textContent = hardwareStatusLabel(status); el.hardwareSourceStatus.className = `state-tag tone-${toneClass(statusTone)}`; const groups = { overview: hardwareOverviewGroups(status), gateways: hardwareGatewayGroups(status), box1: hardwareBoxGroups(status, M3_TRUSTED_ROUTE.fromResourceId), box2: hardwareBoxGroups(status, M3_TRUSTED_ROUTE.toResourceId), patch: hardwarePatchPanelGroups(status) }; // Tab bodies are compact Key/Value groups; SOURCE fallback stays unverified. replaceChildren(el.hardwareList, ...(groups[state.m3Control.hardwareTab] ?? groups.overview)); } function hardwareGroup(group) { const article = document.createElement("article"); article.className = "hardware-section"; article.append(textSpan(group.title, "hardware-section-title")); const rows = document.createElement("div"); rows.className = "hardware-rows"; rows.append(...group.rows.map(hardwareRow)); article.append(rows); return article; } function hardwareRow(item) { const row = document.createElement("div"); row.className = `hardware-row tone-border-${toneClass(item.tone)}`; row.dataset.sourceKind = item.sourceKind; const head = document.createElement("div"); head.className = "hardware-row-head"; head.append(textSpan(item.label, "hardware-row-label"), badge(item.badgeLabel ?? hardwareEvidenceLabel(item.sourceKind), item.sourceKind)); row.append(head); row.append(textSpan(item.value, `hardware-row-value tone-${toneClass(item.tone)}`)); row.append(textSpan(item.detail, "hardware-row-detail")); return row; } function hardwareStatusLabel(status) { if (!status) return "未验证"; if (status.status === "live" && status.trust?.durableStatus === "green") return "实况可信"; if (status.status === "error") return "错误"; if (status.status === "blocked") return "实况受阻"; if (status.sourceKind === "DEV-LIVE") return "DEV-LIVE 状态"; return "未验证"; } function sourceFallbackM3Status() { const observedAt = new Date().toISOString(); return { status: "unverified", sourceKind: "SOURCE", observedAt, chain: { sourceGatewayId: "gwsimu_1", sourceGatewaySessionId: "gws_gwsimu_1", sourceResourceId: M3_TRUSTED_ROUTE.fromResourceId, sourceBoxId: "boxsimu_1", sourcePort: M3_TRUSTED_ROUTE.fromPort, targetGatewayId: "gwsimu_2", targetGatewaySessionId: "gws_gwsimu_2", targetResourceId: M3_TRUSTED_ROUTE.toResourceId, targetBoxId: "boxsimu_2", targetPort: M3_TRUSTED_ROUTE.toPort, patchPanelServiceId: M3_TRUSTED_ROUTE.patchPanelServiceId }, gateways: [ { id: "gwsimu_1", role: "source", online: false, sessionId: "gws_gwsimu_1", sourceKind: "SOURCE", lastSeenAt: observedAt, lastError: "等待 cloud-api /v1/m3/status 聚合读取" }, { id: "gwsimu_2", role: "target", online: false, sessionId: "gws_gwsimu_2", sourceKind: "SOURCE", lastSeenAt: observedAt, lastError: "等待 cloud-api /v1/m3/status 聚合读取" } ], boxes: [ sourceFallbackBox("boxsimu_1", M3_TRUSTED_ROUTE.fromResourceId, "gwsimu_1", "gws_gwsimu_1", "DO1", "output", observedAt), sourceFallbackBox("boxsimu_2", M3_TRUSTED_ROUTE.toResourceId, "gwsimu_2", "gws_gwsimu_2", "DI1", "input", observedAt) ], patchPanel: { serviceId: M3_TRUSTED_ROUTE.patchPanelServiceId, connectionActive: false, connectionVersion: null, wiringConfigId: gateSummary.topology.patchPanel.wiringConfigId ?? null, lastSyncAt: observedAt, lastError: "SOURCE 拓扑 fallback,未验证 DEV-LIVE 接线盘状态", sourceKind: "SOURCE" }, trust: { operationId: null, traceId: null, auditId: null, evidenceId: null, durableStatus: "blocked", blocker: "unverified_cloud_api_m3_status" }, blocker: { code: "unverified_cloud_api_m3_status", layer: "cloud-api", zh: "等待 cloud-api /v1/m3/status 聚合读取;SOURCE 拓扑不能冒充实况硬件状态。" } }; } function sourceFallbackBox(id, resourceId, gatewayId, gatewaySessionId, port, direction, observedAt) { return { id, resourceId, gatewayId, gatewaySessionId, online: false, sourceKind: "SOURCE", observedAt, ports: { [port]: { value: null, direction, source: "SOURCE fallback", sourceKind: "SOURCE", observedAt, lastError: "未验证" } } }; } function hardwareOverviewGroups(status) { const box1 = findM3Box(status, M3_TRUSTED_ROUTE.fromResourceId); const box2 = findM3Box(status, M3_TRUSTED_ROUTE.toResourceId); const do1 = box1?.ports?.DO1; const di1 = box2?.ports?.DI1; const patch = status.patchPanel ?? {}; const trust = status.trust ?? {}; return [ hardwareKvGroup("总览", [ ["状态", hardwareStatusLabel(status), statusTone(status)], ["观测时间", status.observedAt ?? "未验证", "source"], ["Gateway 在线", `${onlineCount(status.gateways)} / ${status.gateways?.length ?? 0}`, onlineCount(status.gateways) === 2 ? "live" : "blocked"], ["BOX 在线", `${onlineCount(status.boxes)} / ${status.boxes?.length ?? 0}`, onlineCount(status.boxes) === 2 ? "live" : "blocked"], ["链路状态", 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 未 ready(status=${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 未 ready(status=${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); 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); } async function cancelAgentRequest({ traceId, conversationId, sessionId, threadId }) { return fetchJson("/v1/agent/chat/cancel", { method: "POST", headers: { "Content-Type": "application/json", "X-Trace-Id": traceId }, timeoutMs: Math.min(API_TIMEOUT_MS, 5000), 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 section = document.createElement("section"); section.className = `message-session-context tone-border-${toneClass(continuity.tone)}`; const header = document.createElement("div"); header.className = "message-session-head"; header.append(badge(continuity.label, continuity.tone), textSpan(continuity.summary, "message-session-summary")); 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); } section.append(header, list); return section; } 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", `${CODE_AGENT_TIMEOUT_MS}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 section = document.createElement("section"); section.className = `message-runtime-path tone-border-${toneClass(runtimePath.tone)}`; section.dataset.runtimePathKind = runtimePath.kind; const header = document.createElement("div"); header.className = "message-runtime-head"; header.append( badge(runtimePath.label, runtimePath.tone), textSpan(runtimePath.summary, "message-runtime-summary") ); 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); } section.append(header, rows); 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"; details.open = ["running", "completed", "source", "failed", "timeout", "canceled", "error"].includes(message.status); 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, tracePreviewEvents(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, list); list.dataset.traceMode = rows.length > CODE_AGENT_TRACE_PREVIEW_LIMIT ? "tail" : "all"; renderTraceEventList(list, tracePreviewRows(rows)); details.append(summary, toolbar, list); return details; } details.append(summary, list); return details; } function messageTraceToolbar(message, trace, events, list) { const toolbar = document.createElement("div"); toolbar.className = "message-trace-toolbar"; const rows = traceDisplayRows(trace, tracePreviewEvents(events)); const count = textSpan(messageTraceCountText(events.length, rows.length, false), "message-trace-count"); toolbar.append(count); if (rows.length > CODE_AGENT_TRACE_PREVIEW_LIMIT) { let expanded = false; const toggle = traceActionButton("展开全部", () => { expanded = !expanded; renderTraceEventList(list, expanded ? rows : tracePreviewRows(rows)); list.classList.toggle("message-trace-events-full", expanded); list.dataset.traceMode = expanded ? "all" : "tail"; count.textContent = messageTraceCountText(events.length, rows.length, expanded); toggle.textContent = expanded ? "收起为最近 14" : "展开全部"; }, "在面板内查看完整 trace 时间线"); toolbar.append(toggle); } toolbar.append(traceActionButton("复制 JSON", () => copyTextToClipboard(messageTraceJson(message, trace, events)), "复制完整 trace JSON")); toolbar.append(traceActionButton("下载 trace", () => downloadTraceJson(message, trace, events), "下载完整 trace JSON 文件")); return toolbar; } function tracePreviewEvents(events) { return Array.isArray(events) ? events : []; } function tracePreviewRows(rows) { return rows.length > CODE_AGENT_TRACE_PREVIEW_LIMIT ? rows.slice(-CODE_AGENT_TRACE_PREVIEW_LIMIT) : rows; } function messageTraceCountText(total, displayTotal, expanded) { if (displayTotal <= CODE_AGENT_TRACE_PREVIEW_LIMIT || expanded) return `显示全部 ${displayTotal} / 总计 ${total}`; return `显示最近 ${CODE_AGENT_TRACE_PREVIEW_LIMIT} / 总计 ${total}`; } 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 = []; for (const event of events) { const row = traceDisplayRow(trace, event); if (!row) continue; rows.push(row); } return rows.length > 0 ? rows : events.map((event) => traceDisplayRow(trace, event, { includeNoise: true })).filter(Boolean); } 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 (/assistant:chunk|token_count|outputDelta:chunk/iu.test(label)) return true; if (event?.type === "assistant_message" && event?.status === "chunk") 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 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.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")); }