Merge pull request #133 from pikasTech/fix/cloud-workbench-117-120

fix: address Cloud Workbench feedback 117-120
This commit is contained in:
Lyon
2026-05-22 20:19:29 +08:00
committed by GitHub
6 changed files with 421 additions and 203 deletions
+103 -7
View File
@@ -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(/<section\b[^>]*\bdata-view=["']([^"']+)["'][^>]*>/gu)]
.filter((match) => match[1] !== "workspace")
.filter((match) => !/\bhidden\b/u.test(match[0]));
const diagnosticsPanel = html.match(/<section\b[^>]*\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(`<section\\b[^>]*\\bdata-view=["']${escapeRegExp(view)}["'][^>]*>`, "u"))?.[0] ?? null;
}
function sectionSource(source, viewId) {
const start = source.search(new RegExp(`<section\\b[^>]*\\bdata-view=["']${escapeRegExp(viewId)}["'][^>]*>`, "u"));
if (start === -1) return "";
const next = source.slice(start + 1).search(/<section\b[^>]*\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(`<section\\b[^>]*\\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(/<section\b[^>]*\bdata-side-panel=["']wiring["'][\s\S]*?<\/section>/u)?.[0] ?? "";
return requiredWiringColumns.every((column) => wiringPanel.includes(`<th>${column}</th>`));
}
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 =
+129 -108
View File
@@ -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 (
{
+18 -11
View File
@@ -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`
## 来源标签含义
+80 -45
View File
@@ -10,12 +10,11 @@
<body>
<main class="workbench-shell" data-app-shell data-help-route-policy="non-default-internal-help" data-internal-help-path="./help.md">
<aside class="activity-rail" aria-label="工作台活动栏">
<button class="rail-button active" type="button" data-route="workspace" title="工作台" aria-label="工作台"></button>
<button class="rail-button" type="button" data-side-tab-jump="records" title="可信记录" aria-label="可信记录"></button>
<button class="rail-button" type="button" data-route="gate" title="Gate 诊断" aria-label="Gate 诊断"></button>
<button class="rail-button" type="button" data-route="help" title="使用说明" aria-label="使用说明"></button>
<button class="rail-button active" type="button" data-route="workspace" title="工作台" aria-label="工作台">工作</button>
<button class="rail-button" type="button" data-route="gate" title="内部复核" aria-label="内部复核">内部复核</button>
<button class="rail-button" type="button" data-route="help" title="使用说明" aria-label="使用说明">使用说明</button>
<span class="rail-spacer" aria-hidden="true"></span>
<button class="rail-button" id="explorer-toggle" type="button" title="展开或收起资源树" aria-label="展开或收起资源树"></button>
<button class="rail-button" id="explorer-toggle" type="button" title="展开或收起资源树" aria-label="展开或收起资源树">资源</button>
</aside>
<aside class="explorer" aria-label="资源浏览与硬件资源树">
@@ -24,13 +23,12 @@
<p class="eyebrow">HWLAB Cloud</p>
<h1>云工作台</h1>
</div>
<span class="status-dot tone-blocked" id="explorer-status">阻塞 BLOCKED</span>
<span class="status-dot tone-source" id="explorer-status">只读工作区</span>
</header>
<section class="source-strip" id="source-strip" aria-label="来源状态">
<span class="state-tag tone-source">来源 SOURCE 加载中</span>
<span class="state-tag tone-dry-run">演练 DRY-RUN 加载中</span>
<span class="state-tag tone-dev-live">开发实况 DEV-LIVE 探测中</span>
<span class="state-tag tone-blocked">阻塞 BLOCKED 等待中</span>
<section class="quick-actions" aria-label="常用工作入口">
<button type="button" data-focus-command>编写任务</button>
<button type="button" data-side-tab-jump="wiring">查看接线</button>
<button type="button" data-side-tab-jump="control">查看状态</button>
</section>
<section class="tree-panel">
<div class="panel-title-row">
@@ -41,24 +39,24 @@
</section>
<section class="tree-panel blockers-panel">
<div class="panel-title-row">
<h2>当前阻塞</h2>
<span id="blocker-count">0</span>
<h2>常用能力</h2>
<span id="capability-count">0</span>
</div>
<div id="explorer-blockers" class="compact-list"></div>
<div id="explorer-capabilities" class="compact-list"></div>
</section>
</aside>
<section class="center-workspace">
<header class="topbar">
<div class="topbar-main">
<p class="eyebrow">M3 可信闭环用户界面</p>
<h2>Agent 对话与执行轨迹</h2>
<p id="route-path" class="route-path">browser/CLI/gateway -> cloud-web/cloud-api</p>
<p class="eyebrow">用户工作台</p>
<h2>Agent 对话与硬件资源面板</h2>
<p id="route-path" class="route-path">当前浏览器会话只保存任务草稿,硬件动作需通过受控后端流程。</p>
</div>
<div class="probe-card" aria-live="polite">
<span class="metric-label">同源只读探测</span>
<strong id="live-status" class="tone-probing">探测中</strong>
<small id="live-detail">正在检查 /health/live、/v1 与只读 /json-rpc</small>
<div class="probe-card user-card" aria-live="polite">
<span class="metric-label">工作区状态</span>
<strong id="live-status" class="tone-source">只读模式</strong>
<small id="live-detail">可整理任务、查看资源、核对接线和可信记录;不会触发硬件变更</small>
</div>
</header>
@@ -70,20 +68,20 @@
<p class="eyebrow">Agent 工作区</p>
<h2 id="workspace-title">Agent 对话</h2>
</div>
<span class="state-tag tone-blocked">阻塞 BLOCKED 已展示</span>
<span class="state-tag tone-source">本地草稿</span>
</div>
<div class="conversation-list" id="conversation-list" aria-live="polite"></div>
</div>
<div class="workspace-panel trace-panel">
<div class="workspace-panel task-panel">
<div class="panel-title-row">
<div>
<p class="eyebrow">执行轨迹</p>
<h2>M0-M5 执行轨迹</h2>
<p class="eyebrow">工作清单</p>
<h2>下一步任务草稿</h2>
</div>
<span class="state-tag tone-dry-run">演练 DRY-RUN 计划</span>
<span class="state-tag tone-source">浏览器本地</span>
</div>
<div id="trace-list" class="trace-list"></div>
<div id="task-list" class="task-list"></div>
</div>
</div>
</section>
@@ -105,15 +103,25 @@
<div class="workspace-panel">
<div class="panel-title-row">
<div>
<p class="eyebrow">二级入口</p>
<h2 id="gate-title">Gate / 诊断</h2>
<p class="eyebrow">内部二级入口</p>
<h2 id="gate-title">Gate / 诊断 / 验收</h2>
</div>
<span class="state-tag tone-source">来源 SOURCE 报告</span>
</div>
<p class="panel-copy">
Gate 数据仅作为诊断验收复核入口保留;默认入口仍停留在用户工作台。
Gate诊断验收、BLOCKED 与 M0-M5 执行轨迹仅在此内部页面展示;默认入口保持用户工作台。
</p>
</div>
<div class="workspace-panel danger">
<div class="panel-title-row">
<div>
<p class="eyebrow">解阻路径</p>
<h2>DB live 与 patch-panel 闭环</h2>
</div>
<span class="state-tag tone-blocked">阻塞 BLOCKED</span>
</div>
<div id="unblock-list" class="compact-list"></div>
</div>
<div class="gate-grid" id="gate-metadata"></div>
<div class="workspace-panel">
<div class="panel-title-row">
@@ -122,6 +130,13 @@
</div>
<div class="milestone-grid" id="milestone-grid"></div>
</div>
<div class="workspace-panel">
<div class="panel-title-row">
<h2>M0-M5 执行轨迹</h2>
<span class="state-tag tone-dry-run">演练 DRY-RUN 计划</span>
</div>
<div id="trace-list" class="trace-list"></div>
</div>
<div class="workspace-panel">
<div class="panel-title-row">
<h2>DEV 健康契约</h2>
@@ -158,6 +173,20 @@
<ol id="validation-list" class="command-list"></ol>
</div>
</div>
<div class="workspace-panel">
<div class="panel-title-row">
<div>
<p class="eyebrow">同源只读</p>
<h2>诊断探测</h2>
</div>
<span class="state-tag tone-dev-live">开发实况 DEV-LIVE 只读探测</span>
</div>
<div id="diagnostics-list" class="compact-list"></div>
<div class="readonly-rpc">
<h3>允许的只读 RPC</h3>
<div id="method-list" class="rpc-chip-list"></div>
</div>
</div>
</section>
<form class="command-bar" id="command-form" aria-label="Agent 输入">
@@ -192,7 +221,6 @@
<button class="side-tab active" id="tab-control" type="button" role="tab" aria-selected="true" data-side-tab="control">控制</button>
<button class="side-tab" id="tab-wiring" type="button" role="tab" aria-selected="false" data-side-tab="wiring">接线</button>
<button class="side-tab" id="tab-records" type="button" role="tab" aria-selected="false" data-side-tab="records">可信记录</button>
<button class="side-tab" id="tab-diagnostics" type="button" role="tab" aria-selected="false" data-side-tab="diagnostics">诊断</button>
</div>
<section class="side-panel active" id="panel-control" role="tabpanel" aria-labelledby="tab-control" data-side-panel="control">
@@ -208,7 +236,23 @@
<h2>接线:hwlab-patch-panel</h2>
<span class="state-tag tone-source">来源 SOURCE 接线</span>
</div>
<div id="wiring-list" class="compact-list"></div>
<div class="table-wrap wiring-table-wrap">
<table class="wiring-table">
<thead>
<tr>
<th>源设备</th>
<th>源端口</th>
<th>接线盘</th>
<th>目标设备</th>
<th>目标端口</th>
<th>状态</th>
<th>证据来源</th>
<th>trace/evidence</th>
</tr>
</thead>
<tbody id="wiring-body"></tbody>
</table>
</div>
</section>
<section class="side-panel" id="panel-records" role="tabpanel" aria-labelledby="tab-records" data-side-panel="records" hidden>
@@ -219,19 +263,10 @@
<div id="records-list" class="compact-list"></div>
</section>
<section class="side-panel" id="panel-diagnostics" role="tabpanel" aria-labelledby="tab-diagnostics" data-side-panel="diagnostics" hidden>
<div class="panel-title-row">
<h2>诊断</h2>
<span class="state-tag tone-dev-live">开发实况 DEV-LIVE 只读探测</span>
</div>
<div id="diagnostics-list" class="compact-list"></div>
<div class="readonly-rpc">
<h3>使用说明</h3>
<p class="panel-copy">仅允许同源只读诊断;不新增硬件写 API。</p>
<h3>允许的只读 RPC</h3>
<div id="method-list" class="rpc-chip-list"></div>
</div>
</section>
<div class="readonly-rpc sidebar-note">
<h3>复核入口</h3>
<p class="panel-copy">技术复核已移至二级页面,不作为默认工作区内容。</p>
</div>
</section>
</aside>
</main>
+44 -12
View File
@@ -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(`<th>${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(`<section\\b[^>]*\\bdata-view=["']${escapeRegExp(viewId)}["'][^>]*>`, "u"));
if (start === -1) return "";
const next = source.slice(start + 1).search(/<section\b[^>]*\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(`<section\\b[^>]*\\bdata-view=["']${escapeRegExp(viewId)}["'][^>]*>`, "u"));
return index === -1 ? source : source.slice(0, index);
}
+47 -20
View File
@@ -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;
}