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.
This commit is contained in:
@@ -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: {
|
||||
|
||||
@@ -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))
|
||||
|
||||
+215
-29
@@ -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) {
|
||||
|
||||
@@ -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": [
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user