Merge pull request #265 from pikasTech/fix/workbench-first-viewport-cleanup-117-120

fix: clean workbench first viewport labels
This commit is contained in:
Lyon
2026-05-23 14:30:44 +08:00
committed by GitHub
5 changed files with 217 additions and 106 deletions
+82 -16
View File
@@ -72,6 +72,9 @@ const defaultHomepageForbiddenTerms = Object.freeze([
"验收",
"M0-M5",
"BLOCKED",
"SOURCE",
"DRY-RUN",
"DEV-LIVE",
"db-live",
"m3-hardware-loop-runtime",
"执行轨迹",
@@ -133,7 +136,7 @@ const requiredHardwareStatusTerms = Object.freeze([
"Patch Panel 连线",
"DO1 -> patch-panel -> DI1",
"AI/AO/FREQ",
"future capability",
"后续能力",
"SOURCE",
"DEV-LIVE",
"BLOCKED"
@@ -1109,14 +1112,12 @@ function beforeSection(source, viewId) {
}
function defaultHomepageHidesBackstage({ html }) {
const workspaceHtml = sectionSource(html, "workspace");
const visibleShellHtml = `${beforeSection(html, "help")}\n${workspaceHtml}`;
return defaultHomepageForbiddenTerms.every((term) => !visibleShellHtml.includes(term));
const firstViewportText = visibleTextFromHtml(defaultFirstViewportHtml(html));
return defaultHomepageForbiddenTerms.every((term) => !textHasForbiddenTerm(firstViewportText, term));
}
function defaultWorkspaceIsSanitized({ html, app }) {
const workspaceHtml = sectionSource(html, "workspace");
const defaultShell = `${beforeSection(html, "help")}\n${workspaceHtml}`;
const defaultShellText = visibleTextFromHtml(defaultFirstViewportHtml(html));
const userFacingBodies = [
functionBody(app, "codeAgentStatusMessage"),
functionBody(app, "codeAgentPromptText"),
@@ -1127,14 +1128,47 @@ function defaultWorkspaceIsSanitized({ html, app }) {
functionBody(app, "renderWiringList")
].join("\n");
return (
!/OPENAI_API_KEY|hwlab-code-agent-provider|Secret|secretRef|m3-route-required|waiting-for-dev-live/u.test(defaultShell) &&
!/OPENAI_API_KEY|hwlab-code-agent-provider|Secret|secretRef|m3-route-required|waiting-for-dev-live|BLOCKED 凭证缺口/u.test(userFacingBodies) &&
defaultHomepageForbiddenTerms.every((term) => !textHasForbiddenTerm(defaultShellText, term)) &&
!/OPENAI_API_KEY|hwlab-code-agent-provider|Secret|secretRef|m3-route-required|waiting-for-dev-live|BLOCKED 凭证缺口|SOURCE fixture|DEV-LIVE/u.test(userFacingBodies) &&
/服务受阻/u.test(functionBody(app, "renderAgentChatStatus")) &&
/Code Agent 服务暂不可用/u.test(functionBody(app, "codeAgentBlockedSummary")) &&
/等待可信记录/u.test(functionBody(app, "renderWiringList"))
);
}
function defaultFirstViewportHtml(html) {
return `${beforeSection(html, "help")}\n${rightSidebarDefaultHtml(html)}`;
}
function rightSidebarDefaultHtml(html) {
const start = html.search(/<aside\b[^>]*\bclass=["'][^"']*\bright-sidebar\b[^"']*["'][^>]*>/u);
if (start === -1) return "";
const wiringStart = html.slice(start).search(/<section\b[^>]*\bid=["']panel-wiring["'][^>]*>/u);
if (wiringStart !== -1) return html.slice(start, start + wiringStart);
const end = html.slice(start).search(/<\/aside>/u);
return end === -1 ? html.slice(start) : html.slice(start, start + end);
}
function visibleTextFromHtml(fragment) {
return fragment
.replace(/<script\b[\s\S]*?<\/script>/giu, " ")
.replace(/<style\b[\s\S]*?<\/style>/giu, " ")
.replace(/<[^>]+>/gu, " ")
.replace(/&nbsp;/gu, " ")
.replace(/&gt;/gu, ">")
.replace(/&lt;/gu, "<")
.replace(/&amp;/gu, "&")
.replace(/\s+/gu, " ")
.trim();
}
function textHasForbiddenTerm(text, term) {
if (/^[A-Za-z][A-Za-z0-9-]*$/u.test(term)) {
return new RegExp(`\\b${escapeRegExp(term)}\\b`, "u").test(text);
}
return text.includes(term);
}
function hasCompletePrimaryNavigation(html) {
const navLabelsPresent = ["工作台", "内部复核", "使用说明", "资源树"].every((label) => html.includes(`>${label}<`));
const legacyLabelsAbsent = incompletePrimaryNavLabels.every((label) => !html.includes(`>${label}<`));
@@ -1190,7 +1224,7 @@ function hardwareStatusStructureContract({ html, app, styles }) {
/function\s+hasTrustedM3LiveEvidence\s*\(/u.test(app) &&
/function\s+linkStatusLabel\s*\(/u.test(app) &&
/function\s+normalizePort\s*\(/u.test(app) &&
/Web 不提供硬件写 RPC;不会新增伪硬件写操作。/u.test(app) &&
/页面不直连模拟器,也不伪造硬件状态。/u.test(app) &&
/\.hardware-section\s*\{/u.test(styles) &&
/\.hardware-row\s*\{/u.test(styles)
);
@@ -1370,8 +1404,8 @@ function hasCodeAgentReadinessVisibility({ html, app }) {
blockedCodeAgentUiLabels.some((label) => app.includes(label)) &&
/provider_unavailable/u.test(app) &&
/OPENAI_API_KEY/u.test(app) &&
/只有真实 completed 回复才标 DEV-LIVE/u.test(app) &&
/不能因为只有 conversationId 标为开发实况/u.test(app) &&
/只有真实完成回复才显示为完成/u.test(app) &&
/不能因为只有会话编号就当成实况完成/u.test(app) &&
/isTrustedCodeAgentProvider/u.test(app) &&
/Boolean\(value\?\.model\)/u.test(app) &&
/Boolean\(value\?\.backend\)/u.test(app) &&
@@ -1492,8 +1526,8 @@ function m3FixtureCountsNotDevLive(app) {
/hasTrustedPatchPanelLiveReport/u.test(liveEvidenceBody) &&
/serviceId\s*!==\s*M3_TRUSTED_ROUTE\.patchPanelServiceId/u.test(patchPanelReportBody) &&
/trustedM3Link\(\[link\]\)/u.test(patchPanelReportBody) &&
/counts 不是 patch-panel live report/u.test(linkDetailBody) &&
/SOURCE fixture 包含目标接线/u.test(linkDetailBody)
/只读运行计数已返回,但还不能证明接线盘闭环/u.test(linkDetailBody) &&
/来源拓扑包含目标接线/u.test(linkDetailBody)
);
}
@@ -1869,7 +1903,7 @@ async function inspectLiveDom(url) {
browser = await chromium.launch({ headless: true });
const page = await browser.newPage({ viewport: { width: 1366, height: 768 } });
await page.goto(url, { waitUntil: "domcontentloaded", timeout: 8000 });
const dom = await page.evaluate(async () => {
const dom = await page.evaluate(async (forbiddenTerms) => {
const workspace = document.querySelector('[data-view="workspace"]');
const gate = document.querySelector('[data-view="gate"]');
const help = document.querySelector('[data-view="help"]');
@@ -1884,10 +1918,34 @@ async function inspectLiveDom(url) {
const box = element.getBoundingClientRect();
return style.visibility !== "hidden" && style.display !== "none" && box.width > 0 && box.height > 0;
};
const visibleTextInFirstViewport = () => {
const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT);
const chunks = [];
for (let node = walker.nextNode(); node; node = walker.nextNode()) {
const textContent = node.textContent?.replace(/\s+/gu, " ").trim();
if (!textContent) continue;
const parent = node.parentElement;
if (!parent) continue;
const style = getComputedStyle(parent);
if (style.visibility === "hidden" || style.display === "none") continue;
const box = parent.getBoundingClientRect();
const inViewport = box.width > 0 && box.height > 0 && box.bottom > 0 && box.right > 0 && box.top < window.innerHeight && box.left < window.innerWidth;
if (inViewport) chunks.push(textContent);
}
return chunks.join(" ").replace(/\s+/gu, " ").trim();
};
const textHasForbiddenTerm = (candidateText, term) => {
if (/^[A-Za-z][A-Za-z0-9-]*$/u.test(term)) {
const escaped = term.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
return new RegExp(`\\b${escaped}\\b`, "u").test(candidateText);
}
return candidateText.includes(term);
};
window.scrollTo(0, 240);
document.documentElement.scrollTop = 240;
document.body.scrollTop = 240;
await new Promise((resolve) => requestAnimationFrame(resolve));
const firstViewportText = visibleTextInFirstViewport();
const rootAfterScrollAttempt = {
windowScrollY: window.scrollY,
htmlScrollTop: document.documentElement.scrollTop,
@@ -1903,6 +1961,8 @@ async function inspectLiveDom(url) {
diagnosticsHidden: diagnostics ? diagnostics.hidden : null,
outerScrollLocked: document.documentElement.scrollHeight <= document.documentElement.clientHeight + 2,
rootAfterScrollAttempt,
firstViewportForbiddenPresent: forbiddenTerms.filter((term) => textHasForbiddenTerm(firstViewportText, term)),
firstViewportTextSample: firstViewportText.slice(0, 400),
labelsPresent: ["硬件资源", "Agent 对话", "工作清单", "可信记录", "使用说明"].every((label) => text.includes(label)),
coreControlsVisible: {
commandInput: visible("#command-input"),
@@ -1914,7 +1974,7 @@ async function inspectLiveDom(url) {
recordsTab: visible("#tab-records")
}
};
});
}, [...defaultHomepageForbiddenTerms]);
const pass =
dom.title === "HWLAB 云工作台" &&
dom.bodyOverflow === "hidden" &&
@@ -1927,12 +1987,18 @@ async function inspectLiveDom(url) {
dom.rootAfterScrollAttempt.windowScrollY === 0 &&
dom.rootAfterScrollAttempt.htmlScrollTop === 0 &&
dom.rootAfterScrollAttempt.bodyScrollTop === 0 &&
dom.firstViewportForbiddenPresent.length === 0 &&
dom.labelsPresent &&
Object.values(dom.coreControlsVisible).every(Boolean);
return {
status: pass ? "pass" : "blocked",
summary: pass ? "Browser DOM confirms the default workbench, core controls, and outer scroll lock." : "Browser DOM did not satisfy the workbench contract.",
evidence: [`title=${dom.title}`, `bodyOverflow=${dom.bodyOverflow}`, `htmlOverflow=${dom.htmlOverflow}`],
evidence: [
`title=${dom.title}`,
`bodyOverflow=${dom.bodyOverflow}`,
`htmlOverflow=${dom.htmlOverflow}`,
`firstViewportForbidden=${dom.firstViewportForbiddenPresent.join(",") || "none"}`
],
observations: dom
};
} catch (error) {
+63 -51
View File
@@ -17,7 +17,7 @@ const STATUS_LABELS = Object.freeze({
active: "活动",
available: "可用",
blocked_after_cloud_api: "cloud-api 后阻塞",
blocked: "阻塞 BLOCKED",
blocked: "待处理",
completed: "完成",
connected: "已连接",
degraded: "降级",
@@ -36,9 +36,9 @@ const STATUS_LABELS = Object.freeze({
recorded: "已记录",
sent: "已发送",
requires: "依赖",
source: "来源 SOURCE",
"dry-run": "演练 DRY-RUN",
"dev-live": "开发实况 DEV-LIVE"
source: "来源",
"dry-run": "演练",
"dev-live": "实况已验证"
});
const M3_TRUSTED_ROUTE = Object.freeze({
@@ -913,7 +913,7 @@ function renderUnblockList() {
},
{
title: "证据升级规则",
detail: "SOURCE、LOCAL、DRY-RUN 与 fixture 只能作为解释材料,不能升级为 DEV-LIVE 验收证据。",
detail: "源码、本地与演练记录只能作为解释材料,不能升级为实况验收证据。",
tone: "dry-run"
}
];
@@ -931,7 +931,7 @@ function renderHardwareStatus(summary) {
const patchTone = m3Live ? "live" : patchPanel.state === "active" ? "source" : patchPanel.state;
const linkTone = m3Live ? "live" : sourceLink || patchPanel.activeConnectionCount > 0 ? "degraded" : "blocked";
el.hardwareSourceStatus.textContent = m3Live ? "DEV-LIVE" : "SOURCE";
el.hardwareSourceStatus.textContent = m3Live ? "实况已验证" : "只读核对";
el.hardwareSourceStatus.className = `state-tag tone-${toneClass(m3Live ? "dev-live" : "source")}`;
replaceChildren(
@@ -943,8 +943,8 @@ function renderHardwareStatus(summary) {
label: "Gateway-SIMU",
value: counts ? `${counts.gatewaySessions ?? 0} 会话` : "离线查看",
detail: counts
? `${counts.gatewaySessions ?? 0} 个 Gateway 会话来自只读 runtime counts;不是 M3 DEV-LIVE 证据`
: `${gateSummary.gatewaySessionCount} 个 Gateway 会话来自 SOURCE fixture`,
? `${counts.gatewaySessions ?? 0} 个 Gateway 会话来自只读运行计数;仅用于资源核对`
: `${gateSummary.gatewaySessionCount} 个 Gateway 会话来自内置拓扑`,
sourceKind: runtimeCountsSourceKind,
tone: runtimeCountsTone
},
@@ -952,8 +952,8 @@ function renderHardwareStatus(summary) {
label: "BOX-SIMU",
value: counts ? `${counts.boxResources ?? 0} 资源` : "离线查看",
detail: counts
? `${counts.boxResources ?? 0} 个 BOX 资源来自只读 runtime counts;不是 M3 DEV-LIVE 证据`
: `${gateSummary.boxResourceCount} 个 BOX 资源来自 SOURCE fixture`,
? `${counts.boxResources ?? 0} 个 BOX 资源来自只读运行计数;仅用于资源核对`
: `${gateSummary.boxResourceCount} 个 BOX 资源来自内置拓扑`,
sourceKind: runtimeCountsSourceKind,
tone: runtimeCountsTone
}
@@ -964,22 +964,22 @@ function renderHardwareStatus(summary) {
rows: [
{
label: "DO1",
value: m3Live ? "verified" : "not wired",
detail: m3Live ? m3LiveDetail(liveM3Evidence) : "BLOCKED:没有 DO1 电平的 live M3 operation/trace/audit/evidence 证据。",
value: m3Live ? "已验证" : "待接线",
detail: m3Live ? m3LiveDetail(liveM3Evidence) : "尚未看到 DO1 电平的完整可信记录。",
sourceKind: m3Live ? "DEV-LIVE" : "BLOCKED",
tone: m3Live ? "live" : "blocked"
},
{
label: "DI1",
value: m3Live ? "verified" : "not wired",
detail: m3Live ? m3LiveDetail(liveM3Evidence) : "BLOCKED:没有 DI1 回读的 live M3 operation/trace/audit/evidence 证据。",
value: m3Live ? "已验证" : "待回读",
detail: m3Live ? m3LiveDetail(liveM3Evidence) : "尚未看到 DI1 回读的完整可信记录。",
sourceKind: m3Live ? "DEV-LIVE" : "BLOCKED",
tone: m3Live ? "live" : "blocked"
},
{
label: "AI/AO/FREQ",
value: "future capability",
detail: "当前未声明模拟量/频率闭环,按 future capability 处理。",
value: "后续能力",
detail: "当前未声明模拟量频率闭环,按后续能力处理。",
sourceKind: "SOURCE",
tone: "disabled"
}
@@ -992,8 +992,8 @@ function renderHardwareStatus(summary) {
label: "hwlab-patch-panel",
value: statusLabel(patchPanel.state),
detail: m3Live
? `DEV-LIVE patch-panel live report ${liveM3Evidence.patchPanelLiveReport.reportId ?? liveM3Evidence.patchPanelLiveReport.patchPanelStatusId} 已绑定 M3 route`
: `${patchPanel.activeConnectionCount} SOURCE fixture 接线;工作台只展示 required M3 route,未升级为 DEV-LIVE`,
? `接线盘实况记录 ${liveM3Evidence.patchPanelLiveReport.reportId ?? liveM3Evidence.patchPanelLiveReport.patchPanelStatusId} 已绑定目标链路`
: `${patchPanel.activeConnectionCount}内置接线记录;工作台只展示目标链路,未升级为实况验收`,
sourceKind: m3Live ? "DEV-LIVE" : "SOURCE",
tone: patchTone
},
@@ -1011,10 +1011,10 @@ function renderHardwareStatus(summary) {
rows: [
{
label: "硬件控制",
value: state.m3Control.contract?.status === "available" ? "cloud-api 受控" : "阻塞",
value: state.m3Control.contract?.status === "available" ? "受控路径" : "等待探测",
detail: state.m3Control.contract?.status === "available"
? "M3 控制只调用 same-origin /v1/m3/io,由 cloud-api 转发 gateway-simu/box-simu 并触发 patch-panelWeb 不直连 gateway/box-simu。Web 不提供硬件写 RPC;不会新增伪硬件写操作。"
: "M3 控制路径未就绪;Web 不提供直连 gateway/box-simu 或伪硬件状态写入。",
? "控制只通过受控后端执行,并经由 gateway-simubox-simu patch-panel页面不直连模拟器,也不伪造硬件状态。"
: "控制路径未就绪;页面不直连模拟器,也不伪造硬件状态。",
sourceKind: state.m3Control.contract?.status === "available" ? "SOURCE" : "BLOCKED",
tone: state.m3Control.contract?.status === "available" ? "source" : "blocked"
}
@@ -1040,7 +1040,7 @@ function hardwareRow(item) {
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.sourceKind, item.sourceKind));
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"));
@@ -1060,18 +1060,30 @@ function trustedM3Link(connections) {
function m3LinkDetail({ liveM3Evidence, sourceLink, patchPanel, counts }) {
if (hasTrustedM3LiveEvidence(liveM3Evidence)) {
return `DEV-LIVE M3${M3_TRUSTED_ROUTE.fromResourceId}:${M3_TRUSTED_ROUTE.fromPort} -> ${M3_TRUSTED_ROUTE.patchPanelServiceId} -> ${M3_TRUSTED_ROUTE.toResourceId}:${M3_TRUSTED_ROUTE.toPort}${m3LiveDetail(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 `BLOCKEDSOURCE fixture 包含目标接线 ${sourceLink.fromResourceId}:${sourceLink.fromPort} -> ${patchPanel.serviceId} -> ${sourceLink.toResourceId}:${sourceLink.toPort}但缺少 patch-panel live report 与 operation/trace/audit/evidence IDs`;
return `来源拓扑包含目标接线 ${sourceLink.fromResourceId}:${sourceLink.fromPort} -> ${patchPanel.serviceId} -> ${sourceLink.toResourceId}:${sourceLink.toPort}仍在等待完整可信记录`;
}
if (counts) {
return "BLOCKEDruntime counts 已返回,但 counts 不是 patch-panel live report,不能证明 M3 DEV-LIVE。";
return "只读运行计数已返回,但还不能证明接线盘闭环。";
}
if (patchPanel.activeConnectionCount > 0) {
return `SOURCE 仅看到非 M3 patch-panel fixture 接线;required M3 route ${M3_TRUSTED_ROUTE.fromResourceId}:${M3_TRUSTED_ROUTE.fromPort} -> ${M3_TRUSTED_ROUTE.patchPanelServiceId} -> ${M3_TRUSTED_ROUTE.toResourceId}:${M3_TRUSTED_ROUTE.toPort} BLOCKED,未冒充 live`;
return `当前只看到非目标接线;目标链路 ${M3_TRUSTED_ROUTE.fromResourceId}:${M3_TRUSTED_ROUTE.fromPort} -> ${M3_TRUSTED_ROUTE.patchPanelServiceId} -> ${M3_TRUSTED_ROUTE.toResourceId}:${M3_TRUSTED_ROUTE.toPort}在等待可信记录`;
}
return "BLOCKED当前没有 DO1 -> patch-panel -> DI1 的可信链路证据。";
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) {
@@ -1202,13 +1214,13 @@ function renderM3ControlStatus() {
? "执行中"
: operationSucceeded
? operation.evidenceState?.status === "green"
? "控制可达 / 证据 green"
: "控制可达 / 证据 blocked"
? "控制可达 / 记录完整"
: "控制可达 / 等待记录"
: operationBlocked
? "操作 BLOCKED"
? "操作待处理"
: contractAvailable
? "cloud-api 受控"
: "BLOCKED";
? "受控路径可用"
: "等待探测";
const tone = pending
? "dry-run"
: operationSucceeded
@@ -1244,16 +1256,16 @@ function controlRows() {
: [];
return [
{
title: "cloud-api 控制路径",
title: "控制路径",
detail: state.m3Control.contract?.status === "available"
? "按钮只调用 /v1/m3/iocloud-api 再通过 gateway-simu -> box-simu -> hwlab-patch-panel 执行 M3 DO/DI。"
: state.m3Control.contract?.blockedReason ?? "等待 /v1/m3/io 合同探测;不可用时不会直连 gateway/box-simu。",
? "按钮只走受控后端,再通过 gateway-simu -> box-simu -> hwlab-patch-panel 执行 M3 DO/DI。"
: "等待受控路径探测;不可用时不会直连 gateway/box-simu。",
tone: state.m3Control.contract?.status === "available" ? "source" : "blocked"
},
...operationCards,
{
title: "读取目标",
detail: `${M3_TRUSTED_ROUTE.toResourceId}:${M3_TRUSTED_ROUTE.toPort} 只能通过 /v1/m3/io 的 di.read 读取;不能从浏览器直连 box-simu。`,
detail: `${M3_TRUSTED_ROUTE.toResourceId}:${M3_TRUSTED_ROUTE.toPort} 只能通过受控路径读取;浏览器直连 box-simu。`,
tone: "source"
},
{
@@ -1263,7 +1275,7 @@ function controlRows() {
},
{
title: "只读探测",
detail: "技术复核探测保持 same-origin read-only,并放在二级页面。",
detail: "内部复核探测保持只读,并放在二级页面。",
tone: "source"
}
];
@@ -1328,9 +1340,9 @@ function renderWiringList(summary = null) {
tr.append(cell(M3_TRUSTED_ROUTE.toResourceId, "mono wrap"));
tr.append(cell(M3_TRUSTED_ROUTE.toPort, "mono"));
const status = document.createElement("td");
status.append(badge(m3Live ? "开发实况 DEV-LIVE" : "阻塞 BLOCKED", m3Live ? "dev-live" : "blocked"));
status.append(badge(m3Live ? "实况已验证" : "待可信记录", m3Live ? "dev-live" : "blocked"));
tr.append(status);
tr.append(cell(m3Live ? "DEV-LIVE" : "来源拓扑", "wrap"));
tr.append(cell(m3Live ? "实况可信记录" : "来源拓扑", "wrap"));
tr.append(cell(traceEvidence, "mono wrap"));
replaceChildren(el.wiringBody, tr);
@@ -2010,8 +2022,8 @@ function codeAgentStatusMessage(availability) {
if (String(availability?.sourceKind ?? availability?.evidenceLevel ?? "").toUpperCase() === "SOURCE") {
return {
role: "system",
title: "Code Agent 状态:SOURCE fixture",
text: "当前只用于本地浏览器 smoke:可以验证发送、处理中、回复渲染和 evidence 展示,但不会标记为 DEV-LIVE。",
title: "Code Agent 状态:本地验证记录",
text: "当前只用于本地浏览器验证:可以检查发送、处理中、回复渲染和证据展示,但不会标记为实况完成。",
status: "source"
};
}
@@ -2019,38 +2031,38 @@ function codeAgentStatusMessage(availability) {
return {
role: "system",
title: "Code Agent 状态:真实通道已接入",
text: "真实 /v1/agent/chat 后端已接入;发送后只有实际 completed 回复才会标记为 DEV-LIVE。",
text: "受控对话通道已接入;发送后只有真实完成的回复才会标记为已完成。",
status: "source"
};
}
return {
role: "system",
title: "Code Agent 状态:等待只读探测",
text: "正在读取 same-origin /v1 与 /health/live;首屏不会宣称 Code Agent 可用,也不会把失败或静态内容标成 DEV-LIVE。",
text: "正在确认服务状态;首屏不会宣称 Code Agent 可用,也不会把失败或静态内容当成真实回复。",
status: "source"
};
}
function codeAgentPromptText(availability) {
if (availability?.status === "blocked") {
return "可以继续输入并保留草稿;发送会走真实 /v1/agent/chat。服务恢复前会显示结构化失败,不会冒充真实 Codex 回复。";
return "可以继续输入并保留草稿;发送会走受控对话通道。服务恢复前会显示结构化失败,不会冒充真实 Codex 回复。";
}
return "请在底部输入中文消息并点击发送;完成态只来自真实 /v1/agent/chat completed 回复。";
return "请在底部输入中文消息并点击发送;完成态只来自真实完成的后端回复。";
}
function codeAgentControlSummary(availability) {
if (latestCompletedAgentMessage()) {
return state.conversationId
? `当前会话 ${state.conversationId};最近一次真实 completed 回复来自 cloud-api 受控 Code Agent 接口,并带有 provider/model/trace/conversation 证据`
: "最近一次真实 completed 回复来自 cloud-api 受控 Code Agent 接口,并带有 provider/model/trace/conversation 证据。";
? `当前会话 ${state.conversationId};最近一次真实完成回复来自受控 Code Agent 接口,并带有可核验记录`
: "最近一次真实完成回复来自受控 Code Agent 接口,并带有可核验记录。";
}
if (isSourceFixtureCompletedChatMessage(latestChatResult())) {
return "最近一次为 SOURCE fixture 回复;它证明本地浏览器对话 UX,不作为 DEV-LIVE 或 #143 完成证据。";
return "最近一次为本地验证回复;它证明浏览器对话体验,不作为实况完成证据。";
}
if (availability?.status === "blocked") {
return codeAgentBlockedSummary(availability);
}
return "输入区会调用 cloud-api 受控 Code Agent 接口;只有真实 completed 回复才标 DEV-LIVE,不能因为只有 conversationId 标为开发实况。";
return "输入区会调用受控 Code Agent 接口;只有真实完成回复才显示为完成,不能因为只有会话编号就当成实况完成。";
}
function codeAgentBlockedSummary(availability) {
@@ -2071,10 +2083,10 @@ function codeAgentAvailabilitySummary() {
return "同源 API 可响应;Code Agent 服务暂不可用,工作台保持只读和可重试。";
}
if (String(state.codeAgentAvailability?.sourceKind ?? state.codeAgentAvailability?.evidenceLevel ?? "").toUpperCase() === "SOURCE") {
return "同源 API 可响应;当前 Code Agent 回复来源是 SOURCE fixture,仅用于本地浏览器 smoke,不是 DEV-LIVE。";
return "同源 API 可响应;当前 Code Agent 回复来源是本地验证记录,仅用于浏览器验证,不是实况完成。";
}
if (state.codeAgentAvailability?.status === "available") {
return "同源 API 可响应;Code Agent 真实通道已接入,只有 completed 回复才标 DEV-LIVE。";
return "同源 API 可响应;Code Agent 真实通道已接入,只有真实完成回复才显示为完成。";
}
return "同源只读接口可响应。技术复核详情已放在二级页面。";
}
+15 -15
View File
@@ -211,35 +211,35 @@
<p class="eyebrow">运行界面</p>
<h2>硬件状态:BOX-SIMU / Gateway-SIMU / hwlab-patch-panel</h2>
</div>
<span class="state-tag tone-source" id="hardware-source-status">SOURCE</span>
<span class="state-tag tone-source" id="hardware-source-status">只读核对</span>
</div>
<div id="hardware-list" class="hardware-list" data-static-source-fallback="SOURCE">
<article class="hardware-section">
<span class="hardware-section-title">SOURCE 兜底状态</span>
<span class="hardware-section-title">只读资源概览</span>
<div class="hardware-rows">
<div class="hardware-row tone-border-source" data-source-kind="SOURCE">
<div class="hardware-row-head">
<span class="hardware-row-label">Gateway/Box SOURCE fixture</span>
<span class="state-tag tone-source">SOURCE</span>
<span class="hardware-row-label">Gateway/Box 资源</span>
<span class="state-tag tone-source">来源拓扑</span>
</div>
<span class="hardware-row-value">离线查看</span>
<span class="hardware-row-detail">等待 app.mjs 或同源只读 counts 覆盖;当前展示 SOURCE 拓扑摘要。</span>
<span class="hardware-row-detail">等待同源只读数据覆盖;当前展示内置拓扑摘要。</span>
</div>
<div class="hardware-row tone-border-source" data-source-kind="SOURCE">
<div class="hardware-row-head">
<span class="hardware-row-label">hwlab-patch-panel</span>
<span class="state-tag tone-source">SOURCE</span>
<span class="state-tag tone-source">来源拓扑</span>
</div>
<span class="hardware-row-value">来源拓扑</span>
<span class="hardware-row-value">目标接线</span>
<span class="hardware-row-detail">res_boxsimu_1:DO1 -> hwlab-patch-panel -> res_boxsimu_2:DI1 仅为源码兜底显示。</span>
</div>
<div class="hardware-row tone-border-blocked" data-source-kind="BLOCKED">
<div class="hardware-row-head">
<span class="hardware-row-label">DO1 -> patch-panel -> DI1</span>
<span class="state-tag tone-blocked">BLOCKED</span>
<span class="state-tag tone-blocked">待可信记录</span>
</div>
<span class="hardware-row-value">m3-route-required / waiting-for-dev-live</span>
<span class="hardware-row-detail">缺少 patch-panel live report 与 operation/trace/audit/evidence IDs,不能标为 DEV-LIVE</span>
<span class="hardware-row-value">等待可信记录</span>
<span class="hardware-row-detail">尚未看到完整可信记录,不能作为实况闭环证据</span>
</div>
</div>
</article>
@@ -297,7 +297,7 @@
<section class="side-panel" id="panel-wiring" role="tabpanel" aria-labelledby="tab-wiring" data-side-panel="wiring" hidden>
<div class="panel-title-row">
<h2>接线:hwlab-patch-panel</h2>
<span class="state-tag tone-source">来源 SOURCE 接线</span>
<span class="state-tag tone-source">来源拓扑接线</span>
</div>
<div class="table-wrap wiring-table-wrap">
<table class="wiring-table">
@@ -320,9 +320,9 @@
<td class="mono wrap">hwlab-patch-panel</td>
<td class="mono wrap">res_boxsimu_2</td>
<td class="mono">DI1</td>
<td><span class="state-tag tone-blocked">阻塞 BLOCKED</span></td>
<td class="wrap">来源拓扑 / required M3 route</td>
<td class="mono wrap">等待可信记录m3-route-required / waiting-for-dev-live</td>
<td><span class="state-tag tone-blocked">待可信记录</span></td>
<td class="wrap">来源拓扑</td>
<td class="mono wrap">等待可信记录</td>
</tr>
</tbody>
</table>
@@ -332,7 +332,7 @@
<section class="side-panel" id="panel-records" role="tabpanel" aria-labelledby="tab-records" data-side-panel="records" hidden>
<div class="panel-title-row">
<h2>可信记录</h2>
<span class="state-tag tone-source">来源 SOURCE + 只读查询</span>
<span class="state-tag tone-source">来源拓扑 + 只读查询</span>
</div>
<div id="records-list" class="compact-list"></div>
</section>
+46 -13
View File
@@ -191,17 +191,18 @@ for (const hardwareTerm of [
"Patch Panel 连线",
"DO1 -> patch-panel -> DI1",
"AI/AO/FREQ",
"future capability",
"后续能力",
"SOURCE",
"DEV-LIVE",
"BLOCKED",
"runtime counts;不是 M3 DEV-LIVE 证据",
"counts 不是 patch-panel live report",
"缺少 patch-panel live report 与 operation/trace/audit/evidence IDs",
"Web 不提供硬件写 RPC;不会新增伪硬件写操作。"
"只读运行计数;仅用于资源核对",
"只读运行计数已返回,但还不能证明接线盘闭环",
"仍在等待完整可信记录",
"页面不直连模拟器,也不伪造硬件状态。"
]) {
assert.match(app, new RegExp(escapeRegExp(hardwareTerm)), `missing hardware term: ${hardwareTerm}`);
}
assert.match(app, /function hardwareEvidenceLabel/);
assert.match(app, /dataset\.sourceKind/);
assert.match(styles, /\.record-group\s*{/);
assert.match(styles, /\.record-group-title\s*{/);
@@ -282,10 +283,9 @@ for (const helpTerm of [
]) {
assert.match(helpMarkdown, new RegExp(escapeRegExp(helpTerm)), `missing help term: ${helpTerm}`);
}
const workspaceHtml = sectionSource(html, "workspace");
const visibleShellHtml = `${beforeSection(html, "help")}\n${workspaceHtml}`;
for (const backendTerm of [/Gate/u, /诊断/u, /验收/u, /BLOCKED/u, /M0-M5/u, /执行轨迹/u, /OPENAI_API_KEY/u, /Secret/u, /secretRef/u, /hwlab-code-agent-provider/u, /m3-route-required/u, /waiting-for-dev-live/u]) {
assert.doesNotMatch(visibleShellHtml, backendTerm, `default workbench must not expose backend term ${backendTerm}`);
const defaultFirstViewportText = visibleTextFromHtml(defaultFirstViewportHtml(html));
for (const backendTerm of ["Gate", "诊断", "验收", "BLOCKED", "SOURCE", "DRY-RUN", "DEV-LIVE", "M0-M5", "执行轨迹", "OPENAI_API_KEY", "Secret", "secretRef", "hwlab-code-agent-provider", "m3-route-required", "waiting-for-dev-live"]) {
assert.equal(textHasForbiddenTerm(defaultFirstViewportText, backendTerm), false, `default workbench must not expose backend term ${backendTerm}`);
}
for (const fullNavLabel of ["工作台", "内部复核", "使用说明", "资源树"]) {
assert.match(html, new RegExp(`>${escapeRegExp(fullNavLabel)}<`), `missing full nav label ${fullNavLabel}`);
@@ -355,8 +355,8 @@ assert.match(html, />读取 DI1<\/button>/);
assert.match(app, /function runM3IoAction/);
assert.match(app, /function renderM3ControlStatus/);
assert.match(app, /frontendBypass=false/);
assert.match(app, /按钮只调用 \/v1\/m3\/io/);
assert.match(app, /Web 不直连 gateway\/box-simu/);
assert.match(app, /按钮只走受控后端/);
assert.match(app, /页面不直连模拟器,也不伪造硬件状态。/);
assert.match(styles, /\.m3-control-form\s*{/);
assert.match(app, /Code Agent 调用失败/);
assert.match(app, /normalizeBlockedAgentResult/);
@@ -370,8 +370,8 @@ assert.match(app, /latestCompletedAgentMessage/);
assert.match(app, /服务受阻/);
assert.match(app, /provider_unavailable/);
assert.match(app, /OPENAI_API_KEY/);
assert.match(app, /只有真实 completed 回复才标 DEV-LIVE/);
assert.match(app, /不能因为只有 conversationId 标为开发实况/);
assert.match(app, /只有真实完成回复才显示为完成/);
assert.match(app, /不能因为只有会话编号就当成实况完成/);
assert.match(app, /TRUSTED_CODE_AGENT_PROVIDERS/);
assert.match(app, /UNTRUSTED_CODE_AGENT_PROVIDER_PATTERN/);
assert.match(app, /isRealCompletedChatResult/);
@@ -558,6 +558,39 @@ function beforeSection(source, viewId) {
return index === -1 ? source : source.slice(0, index);
}
function defaultFirstViewportHtml(source) {
return `${beforeSection(source, "help")}\n${rightSidebarDefaultHtml(source)}`;
}
function rightSidebarDefaultHtml(source) {
const start = source.search(/<aside\b[^>]*\bclass=["'][^"']*\bright-sidebar\b[^"']*["'][^>]*>/u);
if (start === -1) return "";
const wiringStart = source.slice(start).search(/<section\b[^>]*\bid=["']panel-wiring["'][^>]*>/u);
if (wiringStart !== -1) return source.slice(start, start + wiringStart);
const end = source.slice(start).search(/<\/aside>/u);
return end === -1 ? source.slice(start) : source.slice(start, start + end);
}
function visibleTextFromHtml(fragment) {
return fragment
.replace(/<script\b[\s\S]*?<\/script>/giu, " ")
.replace(/<style\b[\s\S]*?<\/style>/giu, " ")
.replace(/<[^>]+>/gu, " ")
.replace(/&nbsp;/gu, " ")
.replace(/&gt;/gu, ">")
.replace(/&lt;/gu, "<")
.replace(/&amp;/gu, "&")
.replace(/\s+/gu, " ")
.trim();
}
function textHasForbiddenTerm(text, term) {
if (/^[A-Za-z][A-Za-z0-9-]*$/u.test(term)) {
return new RegExp(`\\b${escapeRegExp(term)}\\b`, "u").test(text);
}
return text.includes(term);
}
function functionBody(source, functionName) {
const match = source.match(new RegExp(`function\\s+${escapeRegExp(functionName)}\\s*\\([^)]*\\)\\s*\\{`, "u"));
if (!match) return "";
@@ -52,13 +52,13 @@ export function runM3ControlPanelGuard() {
for (const copy of [
"M3 IO 控制请求失败",
"等待 /v1/m3/io 合同探测;不可用时不会直连 gateway/box-simu。",
"不能从浏览器直连 box-simu",
"等待受控路径探测;不可用时不会直连 gateway/box-simu。",
"浏览器直连 box-simu",
"阻塞原因=",
"缺少 patch-panel live report 与 operation/trace/audit/evidence IDs",
"Web 不提供直连 gateway/box-simu 或伪硬件状态写入。",
"控制可达 / 证据 blocked",
"阻塞 BLOCKED"
"仍在等待完整可信记录",
"页面不直连模拟器,也不伪造硬件状态。",
"控制可达 / 等待记录",
"待可信记录"
]) {
assert.match(browserSource, new RegExp(escapeRegExp(copy), "u"), `missing Chinese blocked/error copy: ${copy}`);
}
@@ -87,8 +87,8 @@ function assertControlPanelDom(html) {
for (const header of ["源设备", "源端口", "接线盘", "目标设备", "目标端口", "状态", "证据来源", "trace/evidence"]) {
assert.match(wiringPanel, new RegExp(`<th>${escapeRegExp(header)}<\\/th>`, "u"), `wiring table missing ${header}`);
}
assert.match(wiringPanel, /阻塞 BLOCKED/u, "static wiring row must stay blocked");
assert.match(wiringPanel, /m3-route-required \/ waiting-for-dev-live/u, "static wiring row must require live evidence");
assert.match(wiringPanel, /待可信记录/u, "static wiring row must stay blocked");
assert.match(wiringPanel, /等待可信记录/u, "static wiring row must require live evidence");
}
function assertControlPanelSource(app) {
@@ -125,9 +125,9 @@ function assertControlPanelSource(app) {
assert.match(actionBody, /evidenceState\?\.status === "green" \? "DEV-LIVE" : "BLOCKED"/u, "M3 operation sourceKind must not claim DEV-LIVE unless evidence is green");
const controlRowsBody = functionBody(app, "controlRows");
assert.match(controlRowsBody, /按钮只调用 \/v1\/m3\/io/u, "control copy must name same-origin control route");
assert.match(controlRowsBody, /按钮只走受控后端/u, "control copy must describe the controlled backend route");
assert.match(controlRowsBody, /gateway-simu -> box-simu -> hwlab-patch-panel/u, "control copy must show backend-owned path");
assert.match(controlRowsBody, /di\.read/u, "control rows must describe DI read affordance");
assert.match(controlRowsBody, /受控路径读取/u, "control rows must describe DI read affordance");
const operationDetailBody = functionBody(app, "m3OperationDetail");
for (const field of ["operation", "trace", "audit", "evidence"]) {
@@ -187,7 +187,7 @@ function assertNoStaticDevLiveClaim({ html, app }) {
const controlPanel = elementSectionById(html, "panel-control");
const wiringPanel = elementSectionById(html, "panel-wiring");
assert.doesNotMatch(`${controlPanel}\n${wiringPanel}`, /tone-dev-live|开发实况 DEV-LIVE/u, "static M3 panels must not claim DEV-LIVE");
assert.match(`${controlPanel}\n${wiringPanel}`, /tone-blocked|阻塞 BLOCKED/u, "static M3 panels must keep blocked/source posture");
assert.match(`${controlPanel}\n${wiringPanel}`, /tone-blocked|待可信记录/u, "static M3 panels must keep blocked/source posture");
assert.doesNotMatch(
functionBody(app, "renderM3ControlStatus"),
/operationSucceeded[\s\S]{0,160}\?\s*"dev-live"\s*:\s*"degraded"[\s\S]{0,160}operation\.status === "succeeded"/u,