From d44390ddbd92181051281345e994538bfbb246a3 Mon Sep 17 00:00:00 2001 From: HWLAB Code Queue Date: Fri, 22 May 2026 12:08:40 +0000 Subject: [PATCH] fix: address Cloud Workbench feedback 117-120 --- scripts/src/dev-cloud-workbench-smoke-lib.mjs | 110 +++++++- web/hwlab-cloud-web/app.mjs | 237 ++++++++++-------- web/hwlab-cloud-web/help.md | 29 ++- web/hwlab-cloud-web/index.html | 125 +++++---- web/hwlab-cloud-web/scripts/check.mjs | 56 ++++- web/hwlab-cloud-web/styles.css | 67 +++-- 6 files changed, 421 insertions(+), 203 deletions(-) diff --git a/scripts/src/dev-cloud-workbench-smoke-lib.mjs b/scripts/src/dev-cloud-workbench-smoke-lib.mjs index ce9e60b2..5edb1d99 100644 --- a/scripts/src/dev-cloud-workbench-smoke-lib.mjs +++ b/scripts/src/dev-cloud-workbench-smoke-lib.mjs @@ -33,11 +33,44 @@ const chineseWorkbenchLabels = Object.freeze([ "HWLAB 云工作台", "硬件资源", "Agent 对话", - "执行轨迹", + "工作清单", "可信记录", + "内部复核", "使用说明" ]); +const defaultHomepageForbiddenTerms = Object.freeze([ + "Gate", + "诊断", + "验收", + "M0-M5", + "BLOCKED", + "db-live", + "m3-hardware-loop-runtime", + "执行轨迹" +]); + +const diagnosticsRequiredTerms = Object.freeze([ + "Gate / 诊断 / 验收", + "M0-M5", + "BLOCKED", + "DB live readiness", + "patch-panel DO1 -> DI1" +]); + +const incompletePrimaryNavLabels = Object.freeze(["台", "证", "诊", "帮"]); + +const requiredWiringColumns = Object.freeze([ + "源设备", + "源端口", + "接线盘", + "目标设备", + "目标端口", + "状态", + "证据来源", + "trace/evidence" +]); + const workbenchMarkers = Object.freeze([ "data-app-shell", "workbench-shell", @@ -46,7 +79,7 @@ const workbenchMarkers = Object.freeze([ "resource-tree", "center-workspace", "conversation-list", - "trace-list", + "task-list", "right-sidebar", "hardware-list", "command-form" @@ -129,6 +162,26 @@ function runStaticSmoke() { evidence: chineseWorkbenchLabels }); + addCheck(checks, blockers, "feedback-117-default-no-backstage", defaultHomepageHidesBackstage(files), "Default workbench hides Gate, diagnostics, acceptance, BLOCKED, and M0-M5 backstage information.", { + blocker: "runtime_blocker", + evidence: defaultHomepageForbiddenTerms + }); + + addCheck(checks, blockers, "feedback-118-complete-nav", hasCompletePrimaryNavigation(files.html), "Primary navigation uses complete Chinese labels and removes the left-rail trusted-records shortcut.", { + blocker: "contract_blocker", + evidence: ["工作台", "内部复核", "使用说明", "资源树"] + }); + + addCheck(checks, blockers, "feedback-119-gate-retains-diagnostics", gateRetainsDiagnostics(files.html, files.app), "Internal Gate page retains BLOCKED/M0-M5 diagnostics and the DB live plus patch-panel DO1 -> DI1 unblock path.", { + blocker: "contract_blocker", + evidence: diagnosticsRequiredTerms + }); + + addCheck(checks, blockers, "feedback-120-wiring-table", wiringTableContract(files.html), "Patch-panel wiring panel is a table with source, target, status, evidence source, and trace/evidence columns.", { + blocker: "contract_blocker", + evidence: requiredWiringColumns + }); + addCheck(checks, blockers, "outer-scroll-contract", hasScrollLockContract(files.styles), "Outer page scrolling is locked and internal workbench panes own scrolling.", { blocker: "contract_blocker", evidence: ["html/body overflow hidden", "workbench-shell 100vh/100dvh overflow hidden", ".view overflow auto"] @@ -221,7 +274,15 @@ function baseReport({ mode, status, checks, blockers, evidenceLevel, devLive, he return { status, task: "DC-DCSN-P0-2026-003", - refs: ["pikasTech/HWLAB#108", "pikasTech/HWLAB#99", "pikasTech/HWLAB#78"], + refs: [ + "pikasTech/HWLAB#108", + "pikasTech/HWLAB#99", + "pikasTech/HWLAB#78", + "pikasTech/HWLAB#117", + "pikasTech/HWLAB#118", + "pikasTech/HWLAB#119", + "pikasTech/HWLAB#120" + ], mode, url, generatedAt: new Date().toISOString(), @@ -299,14 +360,49 @@ function secondaryViewsAreNotDefault({ html, app }) { const unhiddenSecondaryViews = [...html.matchAll(/]*\bdata-view=["']([^"']+)["'][^>]*>/gu)] .filter((match) => match[1] !== "workspace") .filter((match) => !/\bhidden\b/u.test(match[0])); - const diagnosticsPanel = html.match(/]*\bdata-side-panel=["']diagnostics["'][^>]*>/u)?.[0] ?? ""; - return finalFallback === "workspace" && unhiddenSecondaryViews.length === 0 && /\bhidden\b/u.test(diagnosticsPanel); + return finalFallback === "workspace" && unhiddenSecondaryViews.length === 0; } function firstTagForDataView(html, view) { return html.match(new RegExp(`]*\\bdata-view=["']${escapeRegExp(view)}["'][^>]*>`, "u"))?.[0] ?? null; } +function sectionSource(source, viewId) { + const start = source.search(new RegExp(`]*\\bdata-view=["']${escapeRegExp(viewId)}["'][^>]*>`, "u")); + if (start === -1) return ""; + const next = source.slice(start + 1).search(/]*\bdata-view=["'][^"']+["'][^>]*>/u); + return next === -1 ? source.slice(start) : source.slice(start, start + 1 + next); +} + +function beforeSection(source, viewId) { + const index = source.search(new RegExp(`]*\\bdata-view=["']${escapeRegExp(viewId)}["'][^>]*>`, "u")); + return index === -1 ? source : source.slice(0, index); +} + +function defaultHomepageHidesBackstage({ html }) { + const workspaceHtml = sectionSource(html, "workspace"); + const visibleShellHtml = `${beforeSection(html, "help")}\n${workspaceHtml}`; + return defaultHomepageForbiddenTerms.every((term) => !visibleShellHtml.includes(term)); +} + +function hasCompletePrimaryNavigation(html) { + const navLabelsPresent = ["工作台", "内部复核", "使用说明", "资源树"].every((label) => html.includes(`>${label}<`)); + const legacyLabelsAbsent = incompletePrimaryNavLabels.every((label) => !html.includes(`>${label}<`)); + const noLeftSideRecordsShortcut = !/data-side-tab-jump=["']records["']/u.test(html); + return navLabelsPresent && legacyLabelsAbsent && noLeftSideRecordsShortcut; +} + +function gateRetainsDiagnostics(html, app) { + const gateHtml = sectionSource(html, "gate"); + const gateSource = `${gateHtml}\n${app}`; + return diagnosticsRequiredTerms.every((term) => gateSource.includes(term)); +} + +function wiringTableContract(html) { + const wiringPanel = html.match(/]*\bdata-side-panel=["']wiring["'][\s\S]*?<\/section>/u)?.[0] ?? ""; + return requiredWiringColumns.every((column) => wiringPanel.includes(`${column}`)); +} + 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]); @@ -323,7 +419,7 @@ function hasScrollLockContract(styles) { /body\s*>\s*\[data-app-shell\]\s*\{[^\}]*min-height:\s*0;/su.test(styles) && /\.workbench-shell\s*\{[^\}]*height:\s*100(?:d)?vh;[^\}]*overflow:\s*hidden;/su.test(styles) && /\.view\s*\{[^\}]*min-height:\s*0;[^\}]*overflow:\s*auto;/su.test(styles) && - /\.(?:resource-tree|compact-list|conversation-list|trace-list|hardware-list)[^\{]*\{[^\}]*min-height:\s*0;[^\}]*overflow:\s*auto;/su.test(styles) + /\.(?:resource-tree|compact-list|conversation-list|trace-list|task-list|hardware-list)[^\{]*\{[^\}]*min-height:\s*0;[^\}]*overflow:\s*auto;/su.test(styles) ); } @@ -499,7 +595,7 @@ async function inspectLiveDom(url) { gateHidden: gate ? gate.hidden : null, diagnosticsHidden: diagnostics ? diagnostics.hidden : null, outerScrollLocked: document.documentElement.scrollHeight <= document.documentElement.clientHeight + 2, - labelsPresent: ["硬件资源", "Agent 对话", "执行轨迹", "可信记录", "使用说明"].every((label) => text.includes(label)) + labelsPresent: ["硬件资源", "Agent 对话", "工作清单", "可信记录", "使用说明"].every((label) => text.includes(label)) }; }); const pass = diff --git a/web/hwlab-cloud-web/app.mjs b/web/hwlab-cloud-web/app.mjs index 1bc93c1e..9c4a1d7a 100644 --- a/web/hwlab-cloud-web/app.mjs +++ b/web/hwlab-cloud-web/app.mjs @@ -35,13 +35,6 @@ const STATUS_LABELS = Object.freeze({ blocked: "阻塞 BLOCKED" }); -const SOURCE_KIND_LABELS = Object.freeze({ - SOURCE: "来源 SOURCE", - "DRY-RUN": "演练 DRY-RUN", - "DEV-LIVE": "开发实况 DEV-LIVE", - BLOCKED: "阻塞 BLOCKED" -}); - const viewIds = new Set([...document.querySelectorAll("[data-view]")].map((view) => view.dataset.view)); let rpcSequence = 0; @@ -49,16 +42,17 @@ const el = { shell: query("[data-app-shell]"), explorerToggle: byId("explorer-toggle"), explorerStatus: byId("explorer-status"), - sourceStrip: byId("source-strip"), treeCount: byId("tree-count"), resourceTree: byId("resource-tree"), - blockerCount: byId("blocker-count"), - explorerBlockers: byId("explorer-blockers"), + capabilityCount: byId("capability-count"), + explorerCapabilities: byId("explorer-capabilities"), routePath: byId("route-path"), liveStatus: byId("live-status"), liveDetail: byId("live-detail"), conversationList: byId("conversation-list"), + taskList: byId("task-list"), traceList: byId("trace-list"), + unblockList: byId("unblock-list"), gateMetadata: byId("gate-metadata"), milestoneGrid: byId("milestone-grid"), healthBody: byId("health-body"), @@ -69,7 +63,7 @@ const el = { commandClear: byId("command-clear"), hardwareList: byId("hardware-list"), controlList: byId("control-list"), - wiringList: byId("wiring-list"), + wiringBody: byId("wiring-body"), recordsList: byId("records-list"), diagnosticsList: byId("diagnostics-list"), methodList: byId("method-list"), @@ -84,6 +78,7 @@ const state = { initRoutes(); initExplorerToggle(); initSideTabs(); +initQuickActions(); initCommandBar(); renderStaticWorkbench(); renderProbePending(); @@ -179,6 +174,14 @@ function initSideTabs() { } } +function initQuickActions() { + for (const button of document.querySelectorAll("[data-focus-command]")) { + button.addEventListener("click", () => { + el.commandInput.focus(); + }); + } +} + function selectSideTab(tabId) { for (const tab of document.querySelectorAll("[data-side-tab]")) { const active = tab.dataset.sideTab === tabId; @@ -212,15 +215,16 @@ function initCommandBar() { } function renderStaticWorkbench() { - el.explorerStatus.textContent = statusLabel(gateSummary.blocked ? "blocked" : gateSummary.gateStatus); - el.explorerStatus.className = `status-dot tone-${toneClass(gateSummary.blocked ? "blocked" : gateSummary.gateStatus)}`; - el.routePath.textContent = runtime.serviceRoute.join(" -> "); + el.explorerStatus.textContent = "只读工作区"; + el.explorerStatus.className = "status-dot tone-source"; + el.routePath.textContent = "当前浏览器会话只保存任务草稿,硬件动作需通过受控后端流程。"; - renderStateStrip(false); renderResourceTree(); - renderBlockerLists(); + renderCapabilityList(); renderConversation(); + renderTaskList(); renderTrace(); + renderUnblockList(); renderHardwareStatus(null); renderDrafts(); renderWiringList(); @@ -230,9 +234,9 @@ function renderStaticWorkbench() { } function renderProbePending() { - el.liveStatus.textContent = "探测中"; - el.liveStatus.className = "tone-probing"; - el.liveDetail.textContent = "正在从当前同源检查 /health/live、/v1 与只读 /json-rpc。"; + el.liveStatus.textContent = "只读模式"; + el.liveStatus.className = "tone-source"; + el.liveDetail.textContent = "可整理任务、查看资源、核对接线和可信记录;不会触发硬件变更。"; renderMethodList(rpcReadMethods, "pending"); } @@ -339,46 +343,26 @@ function nextProtocolId(prefix) { function renderLiveSurface(live) { const coreProbes = [live.healthLive, live.restIndex, live.health, live.adapter]; const reachable = coreProbes.some((probe) => probe.ok); - const healthStatus = live.health.data?.status ?? live.healthLive.data?.status ?? "degraded"; const runtimeSummary = runtimeSummaryFrom(live); const methods = methodsFrom(live); - const dbReady = dbReadyFrom(live); - const devLiveReady = reachable && healthStatus === "ok" && dbReady === true && !gateSummary.blocked; - const blocked = gateSummary.blocked || !devLiveReady; - el.liveStatus.textContent = devLiveReady ? "开发实况 DEV-LIVE 就绪" : reachable ? "阻塞 BLOCKED" : "降级 degraded"; - el.liveStatus.className = `tone-${toneClass(devLiveReady ? "dev-live" : reachable ? "blocked" : "degraded")}`; + el.liveStatus.textContent = reachable ? "只读连接可用" : "离线查看"; + el.liveStatus.className = `tone-${toneClass(reachable ? "dev-live" : "source")}`; el.liveDetail.textContent = reachable - ? `同源只读界面已响应;health=${healthStatus},db.ready=${String(dbReady)},DEV-LIVE=${devLiveReady ? "true" : "false"}。` - : `同源 API 不可用:${probeErrors(coreProbes).join(" | ")}`; + ? "同源只读接口可响应。技术复核详情已放在二级页面。" + : "同源 API 暂不可用,当前仍可使用本地草稿和已加载资源。"; - renderStateStrip(devLiveReady); renderHardwareStatus(runtimeSummary); renderRecords(live); renderDiagnostics(live); renderMethodList(methods.length > 0 ? methods.filter((method) => rpcReadMethods.includes(method)) : rpcReadMethods, reachable ? "live" : "degraded"); - - if (blocked) { - el.explorerStatus.textContent = "阻塞 BLOCKED"; - el.explorerStatus.className = "status-dot tone-blocked"; - } -} - -function renderStateStrip(devLiveReady) { - replaceChildren( - el.sourceStrip, - stateTag("SOURCE", `报告 ${shortHash(gateSummary.reportCommitId)}`), - stateTag("DRY-RUN", statusLabel(gateSummary.dryRun.status)), - stateTag("DEV-LIVE", devLiveReady ? "就绪" : "未声明"), - stateTag("BLOCKED", gateSummary.blocked ? `${gateSummary.blockers.length} 个未关闭` : "已清除") - ); } function renderResourceTree() { const items = [ { label: gateSummary.topology.projectId, - meta: `${gateSummary.environment} 项目`, + meta: "当前项目", tone: "source", children: [ ...gateSummary.topology.gateways.map((gateway) => ({ @@ -400,7 +384,7 @@ function renderResourceTree() { }, { label: gateSummary.agent.agentSessionId, - meta: `${gateSummary.agent.agentServiceId} / ${statusLabel(gateSummary.agent.agentStatus)}`, + meta: `${gateSummary.agent.agentServiceId} / Agent 会话`, tone: gateSummary.agent.agentStatus, children: [ { @@ -409,16 +393,6 @@ function renderResourceTree() { tone: gateSummary.agent.workerStatus } ] - }, - { - label: "诊断", - meta: "Gate 二级入口", - tone: "source", - children: gateSummary.milestones.map((milestone) => ({ - label: milestone.id, - meta: statusLabel(milestone.status), - tone: milestone.status - })) } ]; @@ -442,9 +416,26 @@ function treeGroup(item) { return details; } -function renderBlockerLists() { - el.blockerCount.textContent = String(gateSummary.blockers.length); - replaceChildren(el.explorerBlockers, ...gateSummary.blockers.map(blockerCard)); +function renderCapabilityList() { + const capabilities = [ + { + title: "任务草稿", + detail: "在底部输入区记录要交给 Agent 的任务,不会直接发送硬件请求。", + tone: "source" + }, + { + title: "资源查看", + detail: "查看 Gateway-SIMU、BOX-SIMU 与 hwlab-patch-panel 的只读资源状态。", + tone: "source" + }, + { + title: "接线核对", + detail: "在右侧接线表核对源端口、接线盘、目标端口与证据记录。", + tone: "source" + } + ]; + el.capabilityCount.textContent = String(capabilities.length); + replaceChildren(el.explorerCapabilities, ...capabilities.map(infoCard)); replaceChildren(el.gateBlockers, ...gateSummary.blockers.map(blockerCard)); } @@ -453,27 +444,43 @@ function renderConversation() { { role: "system", title: "界面模式", - text: "此界面面向 M3 可信闭环用户,展示路由、拓扑、执行轨迹与诊断,不发送硬件变更。" + text: "这里是用户工作台:可以整理任务、查看资源、核对接线和可信记录。页面不会发送硬件变更。" }, { role: "agent", - title: "实况验收状态", - text: userFacingSummary(gateSummary.devPreconditions.summary) + title: "建议起步", + text: "先描述要检查的设备、端口或现象;我会把它保存成当前浏览器会话的任务草稿。" }, - ...gateSummary.blockers.map((blocker) => ({ - role: "blocker", - title: blockerTitle(blocker), - text: userFacingSummary(blocker.summary) - })), { role: "user", - title: "可用操作", - text: "可在底部输入区编写指令草稿。草稿只保留在当前浏览器会话中,直到可信后端工作流就绪。" + title: "可查看内容", + text: "左侧是项目资源和常用能力;右侧提供硬件状态、接线表和可信记录。技术复核内容在二级页面。" } ]; replaceChildren(el.conversationList, ...messages.map(messageCard)); } +function renderTaskList() { + const rows = [ + { + title: "1. 描述目标", + detail: "例如要检查哪个 BOX-SIMU、哪个端口、期望看到什么信号。", + tone: "source" + }, + { + title: "2. 核对接线", + detail: "打开右侧“接线”表,确认源设备、接线盘、目标设备和 trace/evidence 是否匹配。", + tone: "source" + }, + { + title: "3. 留存记录", + detail: "在“可信记录”面板查看已有证据;新增硬件动作不从前端直接发起。", + tone: "source" + } + ]; + replaceChildren(el.taskList, ...rows.map(infoCard)); +} + function renderTrace() { replaceChildren( el.traceList, @@ -490,6 +497,27 @@ function renderTrace() { ); } +function renderUnblockList() { + const rows = [ + { + title: "DB live readiness", + detail: "先让 cloud-api /health/live 返回 db.ready=true、db.connected=true,再进入 Agent runtime 调度。", + tone: "blocked" + }, + { + title: "patch-panel DO1 -> DI1", + detail: "再由 hwlab-patch-panel 建立 res_boxsimu_1:DO1 -> res_boxsimu_2:DI1 的活动链路,并附带可信记录。", + tone: "blocked" + }, + { + title: "证据升级规则", + detail: "SOURCE、LOCAL、DRY-RUN 与 fixture 只能作为解释材料,不能升级为 DEV-LIVE 验收证据。", + tone: "dry-run" + } + ]; + replaceChildren(el.unblockList, ...rows.map(infoCard)); +} + function renderHardwareStatus(summary) { const counts = summary?.counts ?? {}; const cards = [ @@ -515,7 +543,7 @@ function renderHardwareStatus(summary) { title: "硬件写入界面", value: "已禁用", meta: "Web 不提供硬件写 RPC", - tone: "blocked" + tone: "source" } ]; replaceChildren(el.hardwareList, ...cards.map(statusCard)); @@ -525,17 +553,17 @@ function controlRows() { return [ { title: "硬件操作", - detail: "此 Web 工作台不开放硬件写入。阻塞清除后仍需通过 patch-panel 管控工作流执行。", - tone: "blocked" + detail: "此 Web 工作台不开放硬件写入;需要硬件动作时交由受控后端流程处理。", + tone: "source" }, { title: "Agent 自动化", - detail: "当前仅作为可信轨迹与演练 DRY-RUN 证据展示;实况调度仍受 DB 就绪状态阻塞。", - tone: "dry-run" + detail: "输入区用于整理 Agent 任务草稿,不会直接调用 cloud-api 变更方法。", + tone: "source" }, { title: "只读探测", - detail: "允许同源 /health/live、/v1 与只读 /json-rpc 诊断。", + detail: "技术复核探测仍保持 same-origin read-only,并放在二级页面。", tone: "source" } ]; @@ -564,21 +592,33 @@ function renderDrafts() { function renderWiringList() { const patchPanel = gateSummary.topology.patchPanel; - const cards = [ - infoCard({ - title: patchPanel.serviceId, - detail: `${patchPanel.patchPanelStatusId} / ${statusLabel(patchPanel.state)} / ${patchPanel.environment}`, - tone: patchPanel.state - }), - ...patchPanel.activeConnections.map((link) => - infoCard({ - title: `${link.fromResourceId}:${link.fromPort}`, - detail: `接线到 ${link.toResourceId}:${link.toPort}`, - tone: "source" - }) - ) - ]; - replaceChildren(el.wiringList, ...cards); + const wiringStep = gateSummary.steps.find((step) => step.kind === "wiring"); + const traceEvidence = [wiringStep?.id, patchPanel.patchPanelStatusId].filter(Boolean).join(" / "); + const rows = patchPanel.activeConnections.map((link) => { + const tr = document.createElement("tr"); + tr.append(cell(link.fromResourceId, "mono wrap")); + tr.append(cell(link.fromPort, "mono")); + tr.append(cell(patchPanel.serviceId, "mono wrap")); + tr.append(cell(link.toResourceId, "mono wrap")); + tr.append(cell(link.toPort, "mono")); + const status = document.createElement("td"); + status.append(badge(statusLabel(patchPanel.state), patchPanel.state)); + tr.append(status); + tr.append(cell(`${patchPanel.environment} / SOURCE`, "wrap")); + tr.append(cell(traceEvidence || "无", "mono wrap")); + return tr; + }); + + if (rows.length === 0) { + const tr = document.createElement("tr"); + const td = document.createElement("td"); + td.colSpan = 8; + td.textContent = "当前没有接线记录。"; + tr.append(td); + rows.push(tr); + } + + replaceChildren(el.wiringBody, ...rows); } function renderRecords(live) { @@ -760,13 +800,6 @@ function infoCard(item) { return article; } -function stateTag(label, detail) { - const span = document.createElement("span"); - span.className = `state-tag tone-${toneClass(label)}`; - span.textContent = `${sourceKindLabel(label)} ${detail}`; - return span; -} - function statusLight(tone) { const span = document.createElement("span"); span.className = `tree-light tone-bg-${toneClass(tone)}`; @@ -818,14 +851,6 @@ function methodsFrom(live) { return []; } -function dbReadyFrom(live) { - return live.healthLive.data?.db?.ready ?? live.health.data?.db?.ready ?? live.restIndex.data?.db?.ready ?? null; -} - -function probeErrors(probes) { - return probes.filter((probe) => !probe.ok).map((probe) => `${probe.method ?? probe.path}:${probe.error}`); -} - function messageFromPayload(payload) { if (!payload || typeof payload !== "object") return "非 JSON 或空响应"; return payload.error?.message || payload.error?.code || payload.message || JSON.stringify(payload).slice(0, 160); @@ -837,10 +862,6 @@ function statusLabel(value) { return STATUS_LABELS[normalized] ?? key; } -function sourceKindLabel(value) { - return SOURCE_KIND_LABELS[String(value ?? "").trim().toUpperCase()] ?? statusLabel(value); -} - function roleLabel(role) { return ( { diff --git a/web/hwlab-cloud-web/help.md b/web/hwlab-cloud-web/help.md index 93c1c880..66816990 100644 --- a/web/hwlab-cloud-web/help.md +++ b/web/hwlab-cloud-web/help.md @@ -1,32 +1,39 @@ # 云工作台内部使用说明 -本页面说明当前 Cloud Workbench 的前端区域与状态含义。默认 `/` 仍进入工作台;本说明只通过工作台内部的 `#help` 入口打开。 +本页面说明当前 Cloud Workbench 的前端区域与状态含义。默认 `/` 仍进入工作台;本说明只通过工作台内部的 `#help` 入口打开,不作为默认页。 ## 左侧资源与功能导航 -- 活动栏:`台` 回到默认工作台,`证` 跳到右侧可信记录,`诊` 打开 Gate / 诊断二级入口,`帮` 打开本说明,`树` 展开或收起资源树。 -- 资源树:展示当前项目、Gateway-SIMU、BOX-SIMU、`hwlab-patch-panel`、Agent manager / worker 与 M0-M5 Gate 状态。它只解释来源状态,不提供硬件写操作。 -- 来源条:展示 `SOURCE`、`DRY-RUN`、`DEV-LIVE`、`BLOCKED` 的当前解释。`LOCAL` 只表示浏览器本地草稿,不等同于 DEV-LIVE。 +- 活动栏使用完整中文标签:`工作台` 回到默认工作台,`内部复核` 打开 Gate / 诊断 / 验收二级页面,`使用说明` 打开本说明,`资源树` 展开或收起资源树。 +- 左侧不再放独立的可信记录跳转;可信记录保留在右侧子面板。 +- 资源树只展示当前项目、Gateway-SIMU、BOX-SIMU、`hwlab-patch-panel`、Agent manager / worker 等用户可理解资源,不把 M0-M5 Gate 状态放在默认首屏。 +- 常用能力区提供任务草稿、资源查看与接线核对入口。`LOCAL` 只表示浏览器本地草稿,不等同于 DEV-LIVE。 -## 中间 Agent 对话与 trace +## 中间 Agent 对话与工作清单 -- Agent 对话区显示工作台范围、当前验收摘要、阻塞项和可执行的本地草稿动作。 -- 执行轨迹区展示 M0-M5 计划步骤、依赖关系和输出数量,用于复核流程状态。 +- Agent 对话区显示用户工作台范围、建议起步方式和可查看内容,不展示 Gate、诊断、验收、BLOCKED 或 M0-M5 执行轨迹。 +- 工作清单区提供描述目标、核对接线、留存记录三类用户动作。 - 底部输入栏只把文字保存为浏览器本地草稿,不调用硬件写 API,不提交 patch-panel 变更,也不写入审计或证据记录。 ## 右侧硬件与状态面板 -- BOX-SIMU / Gateway-SIMU / Patch Panel 卡片展示只读计数、source fallback 与当前阻塞状态。 +- BOX-SIMU / Gateway-SIMU / Patch Panel 卡片展示只读计数、source fallback 与硬件写入边界。 +- 接线:`hwlab-patch-panel` 面板使用表格展示源设备、源端口、接线盘、目标设备、目标端口、状态、证据来源和 trace/evidence。 - M3 虚拟硬件可信闭环的上位约束是:`res_boxsimu_1:DO1 -> hwlab-patch-panel -> res_boxsimu_2:DI1`。只有观察到这条完整链路并带有可信记录时,才可作为 M3 DEV-LIVE 依据。 - Web 前端不会新增 `hardware.*` 写 RPC、patch-panel 写接口、audit 写接口或 evidence 写接口。 -## 右侧二级入口 +## 右侧工作面板 - 控制:显示本地草稿和只读能力边界。这里的控制项保持禁用或本地记录状态。 - 接线:解释 `hwlab-patch-panel` 当前 source wiring,帮助确认 DO1 到 DI1 的可信闭环路径是否仍由 patch-panel 拥有。 - 可信记录:合并 source fixture 与只读 `evidence.record.query` 结果。source、fixture、LOCAL、DRY-RUN 记录不能升级成 DEV-LIVE。 -- 诊断:只检查 same-origin `/health/live`、same-origin `/v1` 与只读 `/json-rpc` 方法。允许的方法包括 `system.health`、`cloud.adapter.describe`、`audit.event.query`、`evidence.record.query`。 -- Gate:从活动栏 `诊` 进入,作为验收复核二级页面保留,不替代默认工作台。 +- 技术复核:Gate、诊断、验收、BLOCKED 和 M0-M5 执行轨迹只在活动栏 `内部复核` 打开的二级页面展示,不替代默认工作台。 + +## 内部复核页 + +- Gate / 诊断 / 验收:从活动栏 `内部复核` 进入,作为验收复核二级页面保留。 +- 解阻路径:先完成 DB live readiness,让 same-origin `/health/live` 能证明 db.ready=true、db.connected=true;再通过 `hwlab-patch-panel` 建立 `res_boxsimu_1:DO1 -> hwlab-patch-panel -> res_boxsimu_2:DI1`,并附带可信 trace/evidence。 +- 诊断探测只检查 same-origin `/health/live`、same-origin `/v1` 与只读 `/json-rpc` 方法。允许的方法包括 `system.health`、`cloud.adapter.describe`、`audit.event.query`、`evidence.record.query`。 ## 来源标签含义 diff --git a/web/hwlab-cloud-web/index.html b/web/hwlab-cloud-web/index.html index 1524ea76..e75ef806 100644 --- a/web/hwlab-cloud-web/index.html +++ b/web/hwlab-cloud-web/index.html @@ -10,12 +10,11 @@
-

M3 可信闭环用户界面

-

Agent 对话与执行轨迹

-

browser/CLI/gateway -> cloud-web/cloud-api

+

用户工作台

+

Agent 对话与硬件资源面板

+

当前浏览器会话只保存任务草稿,硬件动作需通过受控后端流程。

-
- 同源只读探测 - 探测中 - 正在检查 /health/live、/v1 与只读 /json-rpc。 +
+ 工作区状态 + 只读模式 + 可整理任务、查看资源、核对接线和可信记录;不会触发硬件变更。
@@ -70,20 +68,20 @@

Agent 工作区

Agent 对话

- 阻塞 BLOCKED 已展示 + 本地草稿
-
+
-

执行轨迹

-

M0-M5 执行轨迹

+

工作清单

+

下一步任务草稿

- 演练 DRY-RUN 计划 + 浏览器本地
-
+
@@ -105,15 +103,25 @@
-

二级入口

-

Gate / 诊断

+

内部二级入口

+

Gate / 诊断 / 验收

来源 SOURCE 报告

- Gate 数据仅作为诊断与验收复核入口保留;默认入口仍停留在用户工作台。 + Gate、诊断、验收、BLOCKED 与 M0-M5 执行轨迹仅在此内部页面展示;默认入口保持用户工作台。

+
+
+
+

解阻路径

+

DB live 与 patch-panel 闭环

+
+ 阻塞 BLOCKED +
+
+
@@ -122,6 +130,13 @@
+
+
+

M0-M5 执行轨迹

+ 演练 DRY-RUN 计划 +
+
+

DEV 健康契约

@@ -158,6 +173,20 @@
    +
    +
    +
    +

    同源只读

    +

    诊断探测

    +
    + 开发实况 DEV-LIVE 只读探测 +
    +
    +
    +

    允许的只读 RPC

    +
    +
    +
    @@ -192,7 +221,6 @@ -
    @@ -208,7 +236,23 @@

    接线:hwlab-patch-panel

    来源 SOURCE 接线 -
    +
    + + + + + + + + + + + + + + +
    源设备源端口接线盘目标设备目标端口状态证据来源trace/evidence
    +
    - +
    diff --git a/web/hwlab-cloud-web/scripts/check.mjs b/web/hwlab-cloud-web/scripts/check.mjs index bc6238e1..805fbbe6 100644 --- a/web/hwlab-cloud-web/scripts/check.mjs +++ b/web/hwlab-cloud-web/scripts/check.mjs @@ -50,9 +50,9 @@ assert.match(html, /HWLAB 云工作台/); for (const chineseWorkbenchText of [ "硬件资源", "Agent 对话", - "执行轨迹", + "工作清单", "可信记录", - "诊断", + "内部复核", "使用说明", "允许的只读 RPC" ]) { @@ -75,18 +75,18 @@ for (const legacyEnglishTitle of [ assert.doesNotMatch(`${html}\n${app}`, legacyEnglishTitle); } assert.match(html, /硬件状态:BOX-SIMU \/ Gateway-SIMU \/ hwlab-patch-panel/); -assert.match(html, /Gate \/ 诊断/); +assert.match(html, /Gate \/ 诊断 \/ 验收/); for (const workbenchElement of [ "activity-rail", "explorer", "resource-tree", "conversation-list", - "trace-list", + "task-list", "right-sidebar", "command-form", "hardware-list", "control-list", - "wiring-list", + "wiring-body", "records-list", "diagnostics-list" ]) { @@ -112,7 +112,9 @@ assert.match(styles, /\.help-content\s*{[^}]*overflow:\s*auto;/s); assert.match(helpMarkdown, /^# 云工作台内部使用说明/m); for (const helpTerm of [ "左侧资源与功能导航", - "Agent 对话与 trace", + "Agent 对话与工作清单", + "完整中文标签", + "内部复核", "BOX-SIMU", "Gateway-SIMU", "hwlab-patch-panel", @@ -135,6 +137,25 @@ 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]) { + assert.doesNotMatch(visibleShellHtml, backendTerm, `default workbench must not expose backend term ${backendTerm}`); +} +for (const fullNavLabel of ["工作台", "内部复核", "使用说明", "资源树"]) { + assert.match(html, new RegExp(`>${escapeRegExp(fullNavLabel)}<`), `missing full nav label ${fullNavLabel}`); +} +for (const oneCharNavLabel of [">台<", ">证<", ">诊<", ">帮<"]) { + assert.doesNotMatch(html, new RegExp(escapeRegExp(oneCharNavLabel)), `legacy one-character nav label must be removed: ${oneCharNavLabel}`); +} +assert.doesNotMatch(html, /data-side-tab-jump="records"/u, "trusted records must not remain a left-side standalone shortcut"); +for (const wiringHeader of ["源设备", "源端口", "接线盘", "目标设备", "目标端口", "状态", "证据来源", "trace/evidence"]) { + assert.match(html, new RegExp(`${escapeRegExp(wiringHeader)}<\\/th>`), `missing wiring column ${wiringHeader}`); +} +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/); assert.match(buildScript, /"help\.md"/); assert.match(buildScript, /"third_party\/marked\/marked\.esm\.js"/); assert.match(buildScript, /"third_party\/marked\/LICENSE"/); @@ -142,17 +163,16 @@ assert.match(markedLicense, /MarkedJS/); assert.match(markedLicense, /Permission is hereby granted, free of charge/); assert.match(html, /live-status/); assert.match(html, /method-list/); -assert.match(html, /source-strip/); -assert.match(html, /来源 SOURCE 加载中/); -assert.match(html, /演练 DRY-RUN 加载中/); -assert.match(html, /开发实况 DEV-LIVE 探测中/); -assert.match(html, /阻塞 BLOCKED 等待中/); +assert.match(html, /quick-actions/); +assert.match(html, /编写任务/); +assert.match(html, /查看接线/); +assert.match(html, /查看状态/); assert.doesNotMatch(html, /M3 Diagnostics Console/); assert.match(styles, /html,\s*\nbody\s*{[^}]*height:\s*100%;[^}]*overflow:\s*hidden;/s); assert.match(styles, /body\s*>\s*\[data-app-shell\]\s*{[^}]*min-height:\s*0;/s); assert.match(styles, /\.workbench-shell\s*{[^}]*height:\s*100(?:d)?vh;[^}]*overflow:\s*hidden;/s); assert.match(styles, /\.view\s*{[^}]*min-height:\s*0;[^}]*overflow:\s*auto;/s); -assert.match(styles, /\.(?:resource-tree|compact-list|conversation-list|trace-list|hardware-list)[^{]*{[^}]*min-height:\s*0;[^}]*overflow:\s*auto;/s); +assert.match(styles, /\.(?:resource-tree|compact-list|conversation-list|trace-list|task-list|hardware-list)[^{]*{[^}]*min-height:\s*0;[^}]*overflow:\s*auto;/s); assert.match(app, /fetchJson\("\/v1"\)/); assert.match(app, /fetchJson\("\/health\/live"\)/); assert.match(app, /callRpc\("system\.health"\)/); @@ -206,3 +226,15 @@ console.log("hwlab-cloud-web check ok: workbench shell, hardware evidence panel, function escapeRegExp(value) { return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); } + +function sectionSource(source, viewId) { + const start = source.search(new RegExp(`]*\\bdata-view=["']${escapeRegExp(viewId)}["'][^>]*>`, "u")); + if (start === -1) return ""; + const next = source.slice(start + 1).search(/]*\bdata-view=["'][^"']+["'][^>]*>/u); + return next === -1 ? source.slice(start) : source.slice(start, start + 1 + next); +} + +function beforeSection(source, viewId) { + const index = source.search(new RegExp(`]*\\bdata-view=["']${escapeRegExp(viewId)}["'][^>]*>`, "u")); + return index === -1 ? source : source.slice(0, index); +} diff --git a/web/hwlab-cloud-web/styles.css b/web/hwlab-cloud-web/styles.css index b43ca2cb..d06efa21 100644 --- a/web/hwlab-cloud-web/styles.css +++ b/web/hwlab-cloud-web/styles.css @@ -68,7 +68,7 @@ ul { height: 100dvh; min-height: 0; display: grid; - grid-template-columns: 48px 292px minmax(460px, 1fr) 366px; + grid-template-columns: 92px 292px minmax(460px, 1fr) 366px; grid-template-rows: 1fr; overflow: hidden; background: rgba(15, 17, 16, 0.88); @@ -84,9 +84,9 @@ ul { .activity-rail { display: grid; - grid-template-rows: repeat(3, 42px) 1fr 42px; + grid-template-rows: repeat(3, minmax(42px, auto)) 1fr minmax(42px, auto); gap: 6px; - padding: 8px 6px; + padding: 8px; background: var(--rail); border-right: 1px solid var(--line); overflow: auto; @@ -97,15 +97,17 @@ ul { } .rail-button { - width: 36px; + width: 100%; min-height: 36px; + padding: 6px 5px; border: 1px solid transparent; background: transparent; color: var(--muted); - font-family: var(--mono); - font-size: 11px; + font-size: 12px; font-weight: 760; + line-height: 1.2; cursor: pointer; + overflow-wrap: anywhere; } .rail-button:hover, @@ -132,7 +134,7 @@ ul { } .explorer-collapsed { - grid-template-columns: 48px 0 minmax(460px, 1fr) 366px; + grid-template-columns: 92px 0 minmax(460px, 1fr) 366px; } .explorer-collapsed .explorer { @@ -204,16 +206,29 @@ h3 { overflow-wrap: anywhere; } -.source-strip { +.quick-actions { display: grid; - grid-template-columns: 1fr 1fr; + grid-template-columns: 1fr; gap: 6px; } -.source-strip .state-tag { +.quick-actions button { width: 100%; - justify-content: center; - min-width: 0; + min-height: 32px; + padding: 6px 8px; + border: 1px solid var(--line); + background: var(--surface-2); + color: var(--text); + font-weight: 760; + text-align: left; + cursor: pointer; +} + +.quick-actions button:hover, +.quick-actions button:focus-visible { + border-color: var(--line-strong); + background: var(--surface-3); + outline: 0; } .tree-panel { @@ -252,6 +267,7 @@ h3 { .compact-list, .conversation-list, .trace-list, +.task-list, .hardware-list { min-height: 0; overflow: auto; @@ -412,7 +428,8 @@ h3 { } .conversation-panel, -.trace-panel { +.trace-panel, +.task-panel { grid-template-rows: auto minmax(0, 1fr); } @@ -424,7 +441,8 @@ h3 { .conversation-list, .compact-list, -.trace-list { +.trace-list, +.task-list { display: grid; align-content: start; gap: 8px; @@ -659,13 +677,13 @@ h3 { .side-workspace { display: grid; - grid-template-rows: auto minmax(0, 1fr); + grid-template-rows: auto minmax(0, 1fr) auto; overflow: hidden; } .side-tabs { display: grid; - grid-template-columns: repeat(4, minmax(0, 1fr)); + grid-template-columns: repeat(3, minmax(0, 1fr)); border-bottom: 1px solid var(--line); } @@ -725,6 +743,11 @@ h3 { border-top: 1px solid var(--line); } +.sidebar-note { + padding: 10px 12px; + background: var(--surface); +} + .rpc-chip-list { display: flex; flex-wrap: wrap; @@ -792,6 +815,10 @@ table { background: var(--surface-2); } +.wiring-table { + min-width: 980px; +} + th, td { text-align: left; @@ -933,12 +960,12 @@ tbody tr:last-child td { @media (max-width: 1240px) { .workbench-shell { - grid-template-columns: 48px 260px minmax(420px, 1fr); + grid-template-columns: 86px 260px minmax(420px, 1fr); grid-template-rows: minmax(0, 1fr) clamp(220px, 36dvh, 360px); } .explorer-collapsed { - grid-template-columns: 48px 0 minmax(0, 1fr); + grid-template-columns: 86px 0 minmax(0, 1fr); } .right-sidebar { @@ -957,7 +984,7 @@ tbody tr:last-child td { @media (max-width: 860px) { .workbench-shell, .explorer-collapsed { - grid-template-columns: 44px minmax(0, 1fr); + grid-template-columns: 76px minmax(0, 1fr); grid-template-rows: clamp(150px, 26dvh, 240px) minmax(0, 1fr) clamp(180px, 32dvh, 280px); } @@ -992,7 +1019,7 @@ tbody tr:last-child td { .hardware-list, .milestone-grid, .gate-grid, - .source-strip { + .quick-actions { grid-template-columns: 1fr; }