fix: address Cloud Workbench feedback 117-120
This commit is contained in:
@@ -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
@@ -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
@@ -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`。
|
||||
|
||||
## 来源标签含义
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user