From 88e647e380634e78d0883c8aac8ac9c845a80439 Mon Sep 17 00:00:00 2001 From: Lyon <88232613+pikasTech@users.noreply.github.com> Date: Sat, 23 May 2026 00:24:27 +0800 Subject: [PATCH] feat: group Cloud Workbench trusted records Merge PR #146 after Code Queue rebase to main a444387 and validation. This improves the deployed workbench UI surface for trusted records with explicit DEV-LIVE/SOURCE/DRY-RUN/BLOCKED labels. It does not claim M3 DEV-LIVE and does not change PROD or secrets. --- internal/mvp-gate/summary.mjs | 29 ++- scripts/src/dev-cloud-workbench-smoke-lib.mjs | 54 ++++ web/hwlab-cloud-web/app.mjs | 244 +++++++++++++++--- web/hwlab-cloud-web/gate-summary.mjs | 132 +++++++++- web/hwlab-cloud-web/scripts/check.mjs | 41 +++ web/hwlab-cloud-web/styles.css | 34 +++ 6 files changed, 499 insertions(+), 35 deletions(-) diff --git a/internal/mvp-gate/summary.mjs b/internal/mvp-gate/summary.mjs index 1f12ac68..cb4de7fd 100644 --- a/internal/mvp-gate/summary.mjs +++ b/internal/mvp-gate/summary.mjs @@ -234,7 +234,28 @@ function summarizeEvidence(record) { sha256: record.sha256, serviceId: record.serviceId, environment: record.environment, - dryRun: record.metadata?.dryRun === true + traceId: record.metadata?.traceId ?? null, + dryRun: record.metadata?.dryRun === true, + createdAt: record.createdAt ?? null + }; +} + +function summarizeAuditEvent(event) { + return { + auditId: event.auditId, + traceId: event.traceId, + action: event.action, + targetType: event.targetType, + targetId: event.targetId, + projectId: event.projectId ?? null, + gatewaySessionId: event.gatewaySessionId ?? null, + operationId: event.operationId ?? null, + workerSessionId: event.workerSessionId ?? null, + serviceId: event.serviceId, + environment: event.environment, + outcome: event.outcome ?? null, + dryRun: event.metadata?.dryRun === true, + occurredAt: event.occurredAt }; } @@ -321,8 +342,12 @@ export function buildMvpGateSummary({ report, plan }) { capabilityId: operation.capabilityId, agentSessionId: operation.agentSessionId ?? null, workerSessionId: operation.workerSessionId ?? null, - environment: operation.environment + environment: operation.environment, + dryRun: operation.input?.dryRun === true, + requestedAt: operation.requestedAt ?? null, + updatedAt: operation.updatedAt ?? null })), + auditEvents: plan.auditEvents.map(summarizeAuditEvent), evidenceRecords: plan.evidenceRecords.map(summarizeEvidence), steps, realDevGate: { diff --git a/scripts/src/dev-cloud-workbench-smoke-lib.mjs b/scripts/src/dev-cloud-workbench-smoke-lib.mjs index b4dabf21..e65da0b7 100644 --- a/scripts/src/dev-cloud-workbench-smoke-lib.mjs +++ b/scripts/src/dev-cloud-workbench-smoke-lib.mjs @@ -73,6 +73,21 @@ const requiredWiringColumns = Object.freeze([ "trace/evidence" ]); +const requiredTrustedRecordTerms = Object.freeze([ + "Code Agent 对话记录", + "接线/operation", + "audit 记录", + "evidence 证据", + "trace/messageId", + "audit.event.query + SOURCE 回退", + "evidence.record.query + SOURCE 回退", + "conversationId、traceId 和 messageId", + "失败原因=", + "只读 audit 缺少 M3 patch-panel live report / operation / trace / evidence 绑定", + "只读 evidence 缺少 M3 patch-panel live report / operation / trace / audit 绑定", + "当前显示 SOURCE 回退,不能冒充 DEV-LIVE" +]); + const requiredHardwareStatusTerms = Object.freeze([ "hardware-source-status", "hardwareGroup", @@ -208,6 +223,11 @@ function runStaticSmoke() { evidence: requiredWiringColumns }); + addCheck(checks, blockers, "trusted-record-groups", trustedRecordGroups(files), "Trusted Records panel groups Code Agent, wiring/operation, audit, and evidence summaries with source labels and live-failure fallback.", { + blocker: "observability_blocker", + evidence: requiredTrustedRecordTerms + }); + addCheck(checks, blockers, "hardware-status-rich-structure", hardwareStatusStructureContract(files), "Right hardware status renders Gateway/Box, DI/DO/AI/AO/FREQ, patch-panel, and DO1 -> DI1 source-labeled link state.", { blocker: "contract_blocker", evidence: requiredHardwareStatusTerms @@ -512,6 +532,38 @@ function defaultTopologyIsM3Only() { ); } +function trustedRecordGroups({ html, app, styles }) { + const source = `${html}\n${app}\n${styles}`; + return ( + /id=["']records-list["']/u.test(html) && + /function\s+renderRecords\s*\(/u.test(app) && + /function\s+codeAgentRecordCards\s*\(/u.test(app) && + /function\s+operationRecordCards\s*\(/u.test(app) && + /function\s+auditRecordCards\s*\(/u.test(app) && + /function\s+evidenceRecordCards\s*\(/u.test(app) && + /function\s+auditCard\s*\(/u.test(app) && + /function\s+evidenceCard\s*\(/u.test(app) && + /function\s+recordGroup\s*\(/u.test(app) && + /function\s+recordField\s*\(/u.test(app) && + /function\s+sourceKindLabel\s*\(/u.test(app) && + /function\s+liveFailureCard\s*\(/u.test(app) && + /state\.liveSurface/u.test(app) && + /renderRecords\(state\.liveSurface\)/u.test(app) && + /traceId: error\.traceId \|\| traceId/u.test(app) && + /error\.traceId = traceId/u.test(app) && + requiredTrustedRecordTerms.every((term) => source.includes(term)) && + /\.record-group\s*\{/u.test(styles) && + /\.record-group-title\s*\{/u.test(styles) && + /\.record-group-list\s*\{/u.test(styles) && + /replaceChildren\(el\.recordsList, \.\.\.groups\)/u.test(app) && + /gateSummary\.operations\.map/u.test(app) && + /gateSummary\.auditEvents\.slice\(0,\s*4\)\.map/u.test(app) && + /gateSummary\.evidenceRecords\.map/u.test(app) && + /message\.status === "failed" \? "blocked" : "source"/u.test(app) && + !/message\.status === "completed"\s*\?\s*"dev-live"/u.test(app) + ); +} + function finalRouteFallback(app) { const body = app.match(/function\s+routeFromLocation\s*\(\)\s*\{([\s\S]*?)\n\}/u)?.[1] ?? ""; const returns = [...body.matchAll(/return\s+["']([^"']+)["']\s*;/gu)].map((match) => match[1]); @@ -591,6 +643,8 @@ function sourceEvidenceNotDevLive(source) { const m3 = gateSummary.milestones.find((item) => item.id === "M3"); return ( m3?.status === "blocked" && + gateSummary.operations.every((operation) => operation.dryRun === true) && + gateSummary.auditEvents.every((event) => event.dryRun === true) && gateSummary.evidenceRecords.every((record) => record.dryRun === true) && gateSummary.safety.allowNetwork === false && ["来源 SOURCE", "演练 DRY-RUN", "开发实况 DEV-LIVE", "阻塞 BLOCKED"].every((label) => source.includes(label)) diff --git a/web/hwlab-cloud-web/app.mjs b/web/hwlab-cloud-web/app.mjs index e9bbe545..35587c57 100644 --- a/web/hwlab-cloud-web/app.mjs +++ b/web/hwlab-cloud-web/app.mjs @@ -92,7 +92,8 @@ const state = { conversationId: null, codeAgentAvailability: null, chatMessages: [], - chatPending: false + chatPending: false, + liveSurface: null }; initRoutes(); @@ -220,12 +221,14 @@ function initCommandBar() { event.preventDefault(); const value = el.commandInput.value.trim(); if (!value || state.chatPending) return; + const traceId = nextProtocolId("trc"); const userMessage = { id: nextProtocolId("msg"), role: "user", title: `用户 ${shortTime(new Date().toISOString())}`, text: value, status: "sent", + traceId, createdAt: new Date().toISOString() }; const pendingMessage = { @@ -234,6 +237,7 @@ function initCommandBar() { title: "Code Agent 处理中", text: "正在调用真实 Code Agent / Codex 后端,请稍候。", status: "running", + traceId, createdAt: new Date().toISOString() }; state.chatMessages.push(userMessage, pendingMessage); @@ -242,9 +246,10 @@ function initCommandBar() { renderAgentChatStatus("running"); renderConversation(); renderDrafts(); + renderRecords(state.liveSurface); try { - const result = await sendAgentMessage(value, state.conversationId); + const result = await sendAgentMessage(value, state.conversationId, traceId); state.conversationId = result.conversationId || result.sessionId || state.conversationId; const index = state.chatMessages.findIndex((message) => message.id === pendingMessage.id); const status = result.status === "completed" ? "completed" : "failed"; @@ -278,6 +283,7 @@ function initCommandBar() { title: "发送失败", text: `发送失败:${error.message}`, status: "failed", + traceId: error.traceId || traceId, updatedAt: new Date().toISOString(), error: { message: error.message @@ -289,6 +295,7 @@ function initCommandBar() { state.chatPending = false; renderConversation(); renderDrafts(); + renderRecords(state.liveSurface); } }); @@ -300,6 +307,7 @@ function initCommandBar() { renderAgentChatStatus("idle"); renderConversation(); renderDrafts(); + renderRecords(state.liveSurface); }); } @@ -329,8 +337,7 @@ function renderProbePending() { renderMethodList(rpcReadMethods, "pending"); } -async function sendAgentMessage(message, conversationId) { - const traceId = nextProtocolId("trc"); +async function sendAgentMessage(message, conversationId, traceId = nextProtocolId("trc")) { const response = await fetchJson("/v1/agent/chat", { method: "POST", headers: { @@ -346,9 +353,14 @@ async function sendAgentMessage(message, conversationId) { }); if (!response.ok) { - throw new Error(response.error || "Code Agent 请求失败"); + const error = new Error(response.error || "Code Agent 请求失败"); + error.traceId = traceId; + throw error; } - return response.data; + return { + ...response.data, + traceId: response.data?.traceId || traceId + }; } async function loadLiveSurface() { @@ -452,6 +464,7 @@ function nextProtocolId(prefix) { } function renderLiveSurface(live) { + state.liveSurface = live; const coreProbes = [live.healthLive, live.restIndex, live.health, live.adapter]; const reachable = coreProbes.some((probe) => probe.ok); const runtimeSummary = runtimeSummaryFrom(live); @@ -904,33 +917,206 @@ function renderWiringList(summary = null) { } function renderRecords(live) { - const sourceCards = gateSummary.evidenceRecords.map((record) => - infoCard({ - title: record.evidenceId, - detail: `${recordKindLabel(record.kind)} / ${record.operationId} / ${record.dryRun ? "演练 DRY-RUN" : "未知来源"}`, - tone: record.dryRun ? "dry-run" : "source" + const 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) }) - ); + ]; - const liveRecords = live?.evidence.ok ? live.evidence.data?.records ?? [] : []; - const liveCards = liveRecords.map((record) => { - const m3Trusted = hasTrustedM3LiveEvidence(record); - return infoCard({ - title: record.evidenceId, - detail: [ - m3Trusted ? "开发实况 DEV-LIVE M3" : "阻塞 BLOCKED:只读 evidence 查询不是 M3 trusted loop", - recordKindLabel(record.kind ?? "record"), - record.operationId ?? "n/a", - record.serviceId ?? "未知服务" - ].join(" / "), - tone: m3Trusted ? "dev-live" : "blocked" - }); - }); - if (live && !live.evidence.ok) { - liveCards.push(infoCard({ title: "evidence.record.query", detail: live.evidence.error, tone: "blocked" })); + 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" + }) + ]; } - replaceChildren(el.recordsList, ...liveCards, ...sourceCards); + return messages.slice(-4).reverse().map((message) => + infoCard({ + title: `${roleLabel(message.role)} ${statusLabel(message.status)}`, + detail: [ + sourceKindLabel(message.status === "failed" ? "blocked" : "source"), + recordField("conversation", state.conversationId), + recordField("trace", message.traceId), + recordField("message", message.messageId), + message.error?.message ? `失败原因=${message.error.message}` : null + ].filter(Boolean).join(" / "), + tone: message.status === "failed" ? "blocked" : "source" + }) + ); +} + +function operationRecordCards() { + const wiring = gateSummary.topology.patchPanel; + const wiringCards = wiring.activeConnections.map((link) => + infoCard({ + title: `${link.fromResourceId}:${link.fromPort} -> ${link.toResourceId}:${link.toPort}`, + detail: [ + sourceKindLabel("source"), + recordField("patch-panel", wiring.serviceId), + `状态=${statusLabel(wiring.state)}`, + recordField("statusId", wiring.patchPanelStatusId) + ].filter(Boolean).join(" / "), + tone: "source" + }) + ); + const operationCards = gateSummary.operations.map((operation) => + infoCard({ + title: operation.operationId, + detail: [ + sourceKindLabel(operation.dryRun ? "dry-run" : "source"), + `状态=${statusLabel(operation.status)}`, + recordField("resource", operation.resourceId), + recordField("capability", operation.capabilityId), + recordField("worker", operation.workerSessionId) + ].filter(Boolean).join(" / "), + tone: operation.dryRun ? "dry-run" : "source" + }) + ); + return [...wiringCards, ...operationCards]; +} + +function auditRecordCards(live) { + const cards = []; + if (live) { + if (live.audit.ok) { + const liveEvents = live.audit.data?.events ?? []; + if (liveEvents.length === 0) { + cards.push(infoCard({ + title: "audit.event.query", + detail: "只读查询成功但无 M3 trusted loop audit;下方显示 SOURCE/DRY-RUN 回退摘要,不能冒充 DEV-LIVE。", + tone: "source" + })); + } else { + cards.push(...liveEvents.map((event) => auditCard(event, hasTrustedM3LiveEvidence(event) ? "dev-live" : "blocked"))); + } + } else { + cards.push(liveFailureCard("audit.event.query", live.audit.error)); + } + } else { + cards.push(infoCard({ + title: "audit.event.query", + detail: "等待同源只读查询;未完成前显示 SOURCE/DRY-RUN 回退摘要。", + tone: "source" + })); + } + + cards.push(...gateSummary.auditEvents.slice(0, 4).map((event) => auditCard(event, event.dryRun ? "dry-run" : "source"))); + return cards; +} + +function evidenceRecordCards(live) { + const cards = []; + if (live) { + if (live.evidence.ok) { + const liveRecords = live.evidence.data?.records ?? []; + if (liveRecords.length === 0) { + cards.push(infoCard({ + title: "evidence.record.query", + detail: "只读查询成功但无 M3 trusted loop evidence;下方显示 SOURCE/DRY-RUN 回退摘要,不能冒充 DEV-LIVE。", + tone: "source" + })); + } else { + cards.push(...liveRecords.map((record) => evidenceCard(record, hasTrustedM3LiveEvidence(record) ? "dev-live" : "blocked"))); + } + } 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) { + return infoCard({ + title: event.auditId, + detail: [ + sourceKindLabel(tone), + 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) { + return infoCard({ + title: record.evidenceId, + detail: [ + sourceKindLabel(tone), + tone === "blocked" ? "只读 evidence 缺少 M3 patch-panel live report / operation / trace / audit 绑定" : null, + recordKindLabel(record.kind ?? "record"), + recordField("operation", record.operationId), + recordField("trace", record.traceId ?? record.metadata?.traceId), + recordField("service", record.serviceId) + ].filter(Boolean).join(" / "), + tone + }); +} + +function liveFailureCard(title, reason) { + return infoCard({ + title, + detail: `阻塞 BLOCKED:开发实况查询失败,失败原因:${reason || "未知错误"};当前显示 SOURCE 回退,不能冒充 DEV-LIVE。`, + tone: "blocked" + }); +} + +function recordGroup({ title, meta, cards }) { + const section = document.createElement("section"); + section.className = "record-group"; + const header = document.createElement("div"); + header.className = "record-group-head"; + header.append(textSpan(title, "record-group-title"), textSpan(meta, "record-group-meta")); + const body = document.createElement("div"); + body.className = "record-group-list"; + body.append(...cards); + section.append(header, body); + return section; +} + +function recordField(label, value) { + if (value === undefined || value === null || value === "") return null; + return `${label}=${value}`; +} + +function sourceKindLabel(tone) { + return statusLabel(tone); } function renderDiagnostics(live) { diff --git a/web/hwlab-cloud-web/gate-summary.mjs b/web/hwlab-cloud-web/gate-summary.mjs index c9041364..ad1bfff7 100644 --- a/web/hwlab-cloud-web/gate-summary.mjs +++ b/web/hwlab-cloud-web/gate-summary.mjs @@ -596,7 +596,10 @@ export const gateSummary = { "capabilityId": "cap_m5-relay-set", "agentSessionId": null, "workerSessionId": null, - "environment": "dev" + "environment": "dev", + "dryRun": true, + "requestedAt": "2026-05-21T00:02:00.000Z", + "updatedAt": "2026-05-21T00:02:30.000Z" }, { "operationId": "op_m5-agent-0001", @@ -606,7 +609,124 @@ export const gateSummary = { "capabilityId": "cap_m5-relay-set", "agentSessionId": "agt_m5-0001", "workerSessionId": "wkr_m5-0001", - "environment": "dev" + "environment": "dev", + "dryRun": true, + "requestedAt": "2026-05-21T00:03:00.000Z", + "updatedAt": "2026-05-21T00:03:30.000Z" + } + ], + "auditEvents": [ + { + "auditId": "aud_m5-project-selected", + "traceId": "trc_m5-project", + "action": "project.select", + "targetType": "project", + "targetId": "proj_m5-mvp-e2e", + "projectId": "proj_m5-mvp-e2e", + "gatewaySessionId": null, + "operationId": null, + "workerSessionId": null, + "serviceId": "hwlab-cloud-api", + "environment": "dev", + "outcome": "succeeded", + "dryRun": true, + "occurredAt": "2026-05-21T00:00:00.000Z" + }, + { + "auditId": "aud_m5-gateway-started", + "traceId": "trc_m5-gateway", + "action": "gateway.session.start", + "targetType": "gateway_session", + "targetId": "gws_m5-0001", + "projectId": "proj_m5-mvp-e2e", + "gatewaySessionId": "gws_m5-0001", + "operationId": null, + "workerSessionId": null, + "serviceId": "hwlab-gateway", + "environment": "dev", + "outcome": "succeeded", + "dryRun": true, + "occurredAt": "2026-05-21T00:00:30.000Z" + }, + { + "auditId": "aud_m5-wiring-applied", + "traceId": "trc_m5-wiring", + "action": "wiring.apply", + "targetType": "wiring_config", + "targetId": "wir_m5-0001", + "projectId": "proj_m5-mvp-e2e", + "gatewaySessionId": "gws_m5-0001", + "operationId": null, + "workerSessionId": null, + "serviceId": "hwlab-patch-panel", + "environment": "dev", + "outcome": "succeeded", + "dryRun": true, + "occurredAt": "2026-05-21T00:01:00.000Z" + }, + { + "auditId": "aud_m5-direct-operation", + "traceId": "trc_m5-direct-0001", + "action": "hardware.operation.request", + "targetType": "hardware_operation", + "targetId": "op_m5-direct-0001", + "projectId": "proj_m5-mvp-e2e", + "gatewaySessionId": "gws_m5-0001", + "operationId": "op_m5-direct-0001", + "workerSessionId": null, + "serviceId": "hwlab-cloud-api", + "environment": "dev", + "outcome": "succeeded", + "dryRun": true, + "occurredAt": "2026-05-21T00:02:30.000Z" + }, + { + "auditId": "aud_m5-agent-session", + "traceId": "trc_m5-agent-session", + "action": "agent.session.start", + "targetType": "agent_session", + "targetId": "agt_m5-0001", + "projectId": "proj_m5-mvp-e2e", + "gatewaySessionId": null, + "operationId": null, + "workerSessionId": "wkr_m5-0001", + "serviceId": "hwlab-agent-mgr", + "environment": "dev", + "outcome": "succeeded", + "dryRun": true, + "occurredAt": "2026-05-21T00:02:45.000Z" + }, + { + "auditId": "aud_m5-agent-operation", + "traceId": "trc_m5-agent-0001", + "action": "hardware.operation.request", + "targetType": "hardware_operation", + "targetId": "op_m5-agent-0001", + "projectId": "proj_m5-mvp-e2e", + "gatewaySessionId": "gws_m5-0001", + "operationId": "op_m5-agent-0001", + "workerSessionId": "wkr_m5-0001", + "serviceId": "hwlab-agent-worker", + "environment": "dev", + "outcome": "succeeded", + "dryRun": true, + "occurredAt": "2026-05-21T00:03:30.000Z" + }, + { + "auditId": "aud_m5-cleanup", + "traceId": "trc_m5-cleanup", + "action": "worker.cleanup", + "targetType": "worker_session", + "targetId": "wkr_m5-0001", + "projectId": "proj_m5-mvp-e2e", + "gatewaySessionId": "gws_m5-0001", + "operationId": null, + "workerSessionId": "wkr_m5-0001", + "serviceId": "hwlab-agent-worker", + "environment": "dev", + "outcome": "succeeded", + "dryRun": true, + "occurredAt": "2026-05-21T00:05:00.000Z" } ], "evidenceRecords": [ @@ -620,7 +740,9 @@ export const gateSummary = { "sha256": "4cd5cc2c942501b55f7a32f4184e9a1c920b31f13110f44ec68ddc0ad7dc1e34", "serviceId": "hwlab-cloud-api", "environment": "dev", - "dryRun": true + "traceId": "trc_m5-direct-0001", + "dryRun": true, + "createdAt": "2026-05-21T00:02:40.000Z" }, { "evidenceId": "evi_m5-agent-0001", @@ -632,7 +754,9 @@ export const gateSummary = { "sha256": "96e7ffa99efd78b3d5d1b1dfd08be2f077f618dacf786b47f764794714c15c1c", "serviceId": "hwlab-agent-worker", "environment": "dev", - "dryRun": true + "traceId": "trc_m5-agent-0001", + "dryRun": true, + "createdAt": "2026-05-21T00:03:40.000Z" } ], "steps": [ diff --git a/web/hwlab-cloud-web/scripts/check.mjs b/web/hwlab-cloud-web/scripts/check.mjs index 3d1df502..25965015 100644 --- a/web/hwlab-cloud-web/scripts/check.mjs +++ b/web/hwlab-cloud-web/scripts/check.mjs @@ -38,6 +38,20 @@ const distContractScript = fs.readFileSync(path.resolve(rootDir, "scripts/dist-c const markedLicense = fs.readFileSync(path.resolve(rootDir, "third_party/marked/LICENSE"), "utf8"); const artifactPublisher = fs.readFileSync(path.resolve(repoRoot, "scripts/dev-artifact-publish.mjs"), "utf8"); const frontendSource = `${html}\n${app}\n${artifactPublisher}`; +const requiredTrustedRecordTerms = Object.freeze([ + "Code Agent 对话记录", + "接线/operation", + "audit 记录", + "evidence 证据", + "trace/messageId", + "audit.event.query + SOURCE 回退", + "evidence.record.query + SOURCE 回退", + "conversationId、traceId 和 messageId", + "失败原因=", + "只读 audit 缺少 M3 patch-panel live report / operation / trace / evidence 绑定", + "只读 evidence 缺少 M3 patch-panel live report / operation / trace / audit 绑定", + "当前显示 SOURCE 回退,不能冒充 DEV-LIVE" +]); const workbenchSmoke = runDevCloudWorkbenchStaticSmoke(); const helpSmokeCheck = workbenchSmoke.checks.find((check) => check.id === "help-md-contract"); @@ -92,6 +106,21 @@ assert.doesNotMatch(functionBody(app, "renderHardwareStatus"), /hardwareRow\(\s* assert.match(functionBody(app, "hardwareGroup"), /group\.rows\.map\(hardwareRow\)/u); assert.doesNotMatch(app, /sourceKind:\s*counts\s*\?\s*"DEV-LIVE"/); assert.doesNotMatch(app, /counts\s*&&\s*(?:liveLink|sourceLink|m3Link)[\s\S]{0,80}\?\s*"DEV-LIVE"/); +assert.match(app, /function renderRecords/); +assert.match(app, /function codeAgentRecordCards/); +assert.match(app, /function operationRecordCards/); +assert.match(app, /function auditRecordCards/); +assert.match(app, /function evidenceRecordCards/); +assert.match(app, /function auditCard/); +assert.match(app, /function evidenceCard/); +assert.match(app, /function recordGroup/); +assert.match(app, /function recordField/); +assert.match(app, /function sourceKindLabel/); +assert.match(app, /function liveFailureCard/); +assert.match(app, /state\.liveSurface/); +assert.match(app, /renderRecords\(state\.liveSurface\)/); +assert.match(app, /traceId: error\.traceId \|\| traceId/); +assert.match(app, /error\.traceId = traceId/); for (const hardwareTerm of [ "Gateway/Box 在线", "BOX-SIMU 端口", @@ -110,6 +139,8 @@ for (const hardwareTerm of [ assert.match(app, new RegExp(escapeRegExp(hardwareTerm)), `missing hardware term: ${hardwareTerm}`); } assert.match(app, /dataset\.sourceKind/); +assert.match(styles, /\.record-group\s*{/); +assert.match(styles, /\.record-group-title\s*{/); assert.match(styles, /\.hardware-section\s*{/); assert.match(styles, /\.hardware-row\s*{/); assert.match(html, /Gate \/ 诊断 \/ 验收/); @@ -198,6 +229,9 @@ assert.match(html, /id="unblock-list"/); assert.match(app, /DB live readiness/); assert.match(app, /res_boxsimu_1:DO1 -> res_boxsimu_2:DI1/); assert.match(app, /patch-panel DO1 -> DI1/); +for (const trustedRecordTerm of requiredTrustedRecordTerms) { + assert.match(app, new RegExp(escapeRegExp(trustedRecordTerm)), `missing trusted-record term: ${trustedRecordTerm}`); +} assert.match(functionBody(app, "renderWiringList"), /M3_TRUSTED_ROUTE\.fromResourceId/); assert.match(functionBody(app, "renderWiringList"), /M3_TRUSTED_ROUTE\.patchPanelServiceId/); assert.doesNotMatch(functionBody(app, "renderWiringList"), /activeConnections\.map/u); @@ -314,7 +348,14 @@ assert.doesNotMatch( assert.equal(gateSummary.m5DryRunTopology.patchPanel.patchPanelStatusId, "pps_m5-0001"); assert.equal(gateSummary.agent.agentServiceId, "hwlab-agent-mgr"); assert.equal(gateSummary.agent.workerServiceId, "hwlab-agent-worker"); +assert.equal(gateSummary.operations.length, 2); +assert.ok(gateSummary.operations.every((operation) => operation.dryRun === true)); +assert.ok(gateSummary.operations.every((operation) => operation.requestedAt && operation.updatedAt)); +assert.equal(gateSummary.auditEvents.length, 7); +assert.ok(gateSummary.auditEvents.every((event) => event.dryRun === true)); +assert.ok(gateSummary.auditEvents.every((event) => event.traceId && event.occurredAt)); assert.equal(gateSummary.evidenceRecords.length, 2); +assert.ok(gateSummary.evidenceRecords.every((record) => record.traceId && record.createdAt && record.dryRun === true)); assert.equal(gateSummary.safety.allowNetwork, false); runCloudWebM3ReadonlyContract(); diff --git a/web/hwlab-cloud-web/styles.css b/web/hwlab-cloud-web/styles.css index babaaf37..587a4059 100644 --- a/web/hwlab-cloud-web/styles.css +++ b/web/hwlab-cloud-web/styles.css @@ -448,6 +448,40 @@ h3 { gap: 8px; } +.record-group { + min-width: 0; + display: grid; + gap: 7px; + padding: 8px; + background: var(--surface); + border: 1px solid var(--line); +} + +.record-group-head { + min-width: 0; + display: grid; + gap: 2px; +} + +.record-group-title { + color: var(--text); + font-weight: 800; + overflow-wrap: anywhere; +} + +.record-group-meta { + color: var(--dim); + font-family: var(--mono); + font-size: 10px; + overflow-wrap: anywhere; +} + +.record-group-list { + min-height: 0; + display: grid; + gap: 7px; +} + .message-card, .info-card, .trace-item,