feat: add Cloud Workbench Markdown help page
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import { gateSummary } from "./gate-summary.mjs";
|
||||
import { runtime } from "./runtime.mjs";
|
||||
import { marked } from "./third_party/marked/marked.esm.js";
|
||||
|
||||
const API_TIMEOUT_MS = 4500;
|
||||
const rpcReadMethods = Object.freeze([
|
||||
@@ -71,7 +72,9 @@ const el = {
|
||||
wiringList: byId("wiring-list"),
|
||||
recordsList: byId("records-list"),
|
||||
diagnosticsList: byId("diagnostics-list"),
|
||||
methodList: byId("method-list")
|
||||
methodList: byId("method-list"),
|
||||
helpContent: byId("help-content"),
|
||||
helpStatus: byId("help-status")
|
||||
};
|
||||
|
||||
const state = {
|
||||
@@ -84,6 +87,7 @@ initSideTabs();
|
||||
initCommandBar();
|
||||
renderStaticWorkbench();
|
||||
renderProbePending();
|
||||
loadHelpSurface();
|
||||
loadLiveSurface().then(renderLiveSurface);
|
||||
|
||||
function byId(id) {
|
||||
@@ -139,6 +143,33 @@ function initExplorerToggle() {
|
||||
});
|
||||
}
|
||||
|
||||
async function loadHelpSurface() {
|
||||
el.helpStatus.textContent = "加载中";
|
||||
el.helpStatus.className = "state-tag tone-dry-run";
|
||||
el.helpContent.dataset.helpState = "loading";
|
||||
|
||||
try {
|
||||
const response = await fetch("./help.md", { cache: "no-store" });
|
||||
if (!response.ok) {
|
||||
throw new Error(`help.md HTTP ${response.status}`);
|
||||
}
|
||||
const markdown = await response.text();
|
||||
el.helpContent.innerHTML = marked.parse(markdown, { gfm: true, breaks: false });
|
||||
el.helpContent.dataset.helpState = "ready";
|
||||
el.helpStatus.textContent = "已就绪";
|
||||
el.helpStatus.className = "state-tag tone-source";
|
||||
} catch (error) {
|
||||
el.helpContent.dataset.helpState = "error";
|
||||
el.helpContent.replaceChildren();
|
||||
const notice = document.createElement("div");
|
||||
notice.className = "help-error";
|
||||
notice.textContent = `帮助内容加载失败:${error.message}。默认工作台仍可继续使用。`;
|
||||
el.helpContent.append(notice);
|
||||
el.helpStatus.textContent = "加载失败";
|
||||
el.helpStatus.className = "state-tag tone-blocked";
|
||||
}
|
||||
}
|
||||
|
||||
function initSideTabs() {
|
||||
for (const tab of document.querySelectorAll("[data-side-tab]")) {
|
||||
tab.addEventListener("click", () => selectSideTab(tab.dataset.sideTab));
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
# 云工作台内部使用说明
|
||||
|
||||
本页面说明当前 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。
|
||||
|
||||
## 中间 Agent 对话与 trace
|
||||
|
||||
- Agent 对话区显示工作台范围、当前验收摘要、阻塞项和可执行的本地草稿动作。
|
||||
- 执行轨迹区展示 M0-M5 计划步骤、依赖关系和输出数量,用于复核流程状态。
|
||||
- 底部输入栏只把文字保存为浏览器本地草稿,不调用硬件写 API,不提交 patch-panel 变更,也不写入审计或证据记录。
|
||||
|
||||
## 右侧硬件与状态面板
|
||||
|
||||
- BOX-SIMU / Gateway-SIMU / Patch Panel 卡片展示只读计数、source fallback 与当前阻塞状态。
|
||||
- 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:从活动栏 `诊` 进入,作为验收复核二级页面保留,不替代默认工作台。
|
||||
|
||||
## 来源标签含义
|
||||
|
||||
- `SOURCE`:来自仓库内 source report、静态配置或已签入摘要,用于解释当前界面。
|
||||
- `LOCAL`:只存在于当前浏览器会话的本地输入或草稿。
|
||||
- `DRY-RUN`:演练或 fixture 证据,不能声称真实硬件闭环已经通过。
|
||||
- `DEV-LIVE`:必须来自同源只读探测或可信记录,并且满足 DB readiness、阻塞清零与 M3 闭环证据要求。
|
||||
- `BLOCKED`:表示当前前置条件未满足、探测失败、证据不足或不能把 SOURCE / LOCAL / DRY-RUN / fixture 升级为 DEV-LIVE。
|
||||
|
||||
## 端口与只读边界
|
||||
|
||||
- 前端入口保持 `:16666`,API / edge 入口保持 `:16667`。
|
||||
- 浏览器内 API 访问保持 same-origin `/v1`、只读 `/json-rpc` 与 `/health/live`。
|
||||
- 旧公网 `:6666` / `:6667` 不是当前前端验收口径;内部 k3s 服务端口说明不改变浏览器入口。
|
||||
@@ -8,11 +8,12 @@
|
||||
<script type="module" src="./app.mjs"></script>
|
||||
</head>
|
||||
<body>
|
||||
<main class="workbench-shell" data-app-shell>
|
||||
<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>
|
||||
<span class="rail-spacer" aria-hidden="true"></span>
|
||||
<button class="rail-button" id="explorer-toggle" type="button" title="展开或收起资源树" aria-label="展开或收起资源树">树</button>
|
||||
</aside>
|
||||
@@ -87,6 +88,19 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="view help-view" id="help" data-view="help" aria-labelledby="help-title" hidden>
|
||||
<div class="workspace-panel help-panel">
|
||||
<div class="panel-title-row">
|
||||
<div>
|
||||
<p class="eyebrow">内部说明</p>
|
||||
<h2 id="help-title">使用说明</h2>
|
||||
</div>
|
||||
<span class="state-tag tone-dry-run" id="help-status">加载中</span>
|
||||
</div>
|
||||
<div id="help-content" class="help-content" aria-live="polite"></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="view gate-view" id="gate" data-view="gate" aria-labelledby="gate-title" hidden>
|
||||
<div class="workspace-panel">
|
||||
<div class="panel-title-row">
|
||||
|
||||
@@ -5,8 +5,22 @@ const rootDir = path.resolve(path.dirname(new URL(import.meta.url).pathname), ".
|
||||
const distDir = path.resolve(rootDir, "dist");
|
||||
fs.mkdirSync(distDir, { recursive: true });
|
||||
|
||||
for (const file of ["index.html", "styles.css", "app.mjs", "gate-summary.mjs", "runtime.mjs", "workbench-hardware-panel.mjs"]) {
|
||||
fs.copyFileSync(path.resolve(rootDir, file), path.resolve(distDir, file));
|
||||
const runtimeFiles = [
|
||||
"index.html",
|
||||
"styles.css",
|
||||
"app.mjs",
|
||||
"gate-summary.mjs",
|
||||
"runtime.mjs",
|
||||
"workbench-hardware-panel.mjs",
|
||||
"help.md",
|
||||
"third_party/marked/marked.esm.js",
|
||||
"third_party/marked/LICENSE"
|
||||
];
|
||||
|
||||
for (const file of runtimeFiles) {
|
||||
const targetPath = path.resolve(distDir, file);
|
||||
fs.mkdirSync(path.dirname(targetPath), { recursive: true });
|
||||
fs.copyFileSync(path.resolve(rootDir, file), targetPath);
|
||||
}
|
||||
|
||||
console.log(`hwlab-cloud-web build ok: ${distDir}`);
|
||||
|
||||
@@ -15,7 +15,10 @@ const requiredFiles = [
|
||||
"app.mjs",
|
||||
"gate-summary.mjs",
|
||||
"runtime.mjs",
|
||||
"workbench-hardware-panel.mjs"
|
||||
"workbench-hardware-panel.mjs",
|
||||
"help.md",
|
||||
"third_party/marked/marked.esm.js",
|
||||
"third_party/marked/LICENSE"
|
||||
];
|
||||
|
||||
for (const file of requiredFiles) {
|
||||
@@ -28,6 +31,9 @@ for (const file of requiredFiles) {
|
||||
const html = fs.readFileSync(path.resolve(rootDir, "index.html"), "utf8");
|
||||
const styles = fs.readFileSync(path.resolve(rootDir, "styles.css"), "utf8");
|
||||
const app = fs.readFileSync(path.resolve(rootDir, "app.mjs"), "utf8");
|
||||
const helpMarkdown = fs.readFileSync(path.resolve(rootDir, "help.md"), "utf8");
|
||||
const buildScript = fs.readFileSync(path.resolve(rootDir, "scripts/build.mjs"), "utf8");
|
||||
const markedLicense = fs.readFileSync(path.resolve(rootDir, "third_party/marked/LICENSE"), "utf8");
|
||||
const artifactPublisher = fs.readFileSync(path.resolve(repoRoot, "scripts/dev-artifact-publish.mjs"), "utf8");
|
||||
const frontendSource = `${html}\n${app}\n${artifactPublisher}`;
|
||||
|
||||
@@ -80,6 +86,51 @@ for (const workbenchElement of [
|
||||
for (const viewId of ["workspace", "gate"]) {
|
||||
assert.match(html, new RegExp(`data-view="${viewId}"`));
|
||||
}
|
||||
assert.match(html, /data-route="help"/);
|
||||
assert.match(html, /data-view="help"/);
|
||||
assert.match(html, /<section class="view help-view"[\s\S]*?hidden/);
|
||||
assert.match(html, /id="help-content"/);
|
||||
assert.match(html, /data-help-route-policy="non-default-internal-help"/);
|
||||
assert.match(app, /from "\.\/third_party\/marked\/marked\.esm\.js"/);
|
||||
assert.match(app, /marked\.parse\(/);
|
||||
assert.doesNotMatch(app, /function\s+parseMarkdown|markedRegex|markdownRegex/);
|
||||
assert.match(app, /fetch\("\.\/help\.md"/);
|
||||
assert.match(app, /帮助内容加载失败/);
|
||||
assert.match(app, /return "workspace";/);
|
||||
assert.match(styles, /\.help-view\s*{[^}]*overflow:\s*hidden;/s);
|
||||
assert.match(styles, /\.help-panel\s*{[^}]*overflow:\s*hidden;/s);
|
||||
assert.match(styles, /\.help-content\s*{[^}]*overflow:\s*auto;/s);
|
||||
assert.match(helpMarkdown, /^# 云工作台内部使用说明/m);
|
||||
for (const helpTerm of [
|
||||
"左侧资源与功能导航",
|
||||
"Agent 对话与 trace",
|
||||
"BOX-SIMU",
|
||||
"Gateway-SIMU",
|
||||
"hwlab-patch-panel",
|
||||
"控制",
|
||||
"接线",
|
||||
"可信记录",
|
||||
"诊断",
|
||||
"Gate",
|
||||
"SOURCE",
|
||||
"LOCAL",
|
||||
"DRY-RUN",
|
||||
"DEV-LIVE",
|
||||
"BLOCKED",
|
||||
"res_boxsimu_1:DO1 -> hwlab-patch-panel -> res_boxsimu_2:DI1",
|
||||
"same-origin `/v1`",
|
||||
"只读 `/json-rpc`",
|
||||
"`/health/live`",
|
||||
"`:16666`",
|
||||
"`:16667`"
|
||||
]) {
|
||||
assert.match(helpMarkdown, new RegExp(escapeRegExp(helpTerm)), `missing help term: ${helpTerm}`);
|
||||
}
|
||||
assert.match(buildScript, /"help\.md"/);
|
||||
assert.match(buildScript, /"third_party\/marked\/marked\.esm\.js"/);
|
||||
assert.match(buildScript, /"third_party\/marked\/LICENSE"/);
|
||||
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/);
|
||||
@@ -142,3 +193,7 @@ runCloudWebM3ReadonlyContract();
|
||||
runWorkbenchHardwarePanelContract();
|
||||
|
||||
console.log("hwlab-cloud-web check ok: workbench shell, hardware evidence panel, and read-only diagnostics contract are present");
|
||||
|
||||
function escapeRegExp(value) {
|
||||
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
}
|
||||
|
||||
@@ -390,6 +390,12 @@ h3 {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.help-view {
|
||||
min-height: 0;
|
||||
padding: 10px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.conversation-column {
|
||||
min-height: 100%;
|
||||
display: grid;
|
||||
@@ -410,6 +416,12 @@ h3 {
|
||||
grid-template-rows: auto minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.help-panel {
|
||||
height: 100%;
|
||||
grid-template-rows: auto minmax(0, 1fr);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.conversation-list,
|
||||
.compact-list,
|
||||
.trace-list {
|
||||
@@ -459,6 +471,67 @@ h3 {
|
||||
border-color: rgba(231, 110, 94, 0.48);
|
||||
}
|
||||
|
||||
.help-content {
|
||||
min-height: 0;
|
||||
overflow: auto;
|
||||
padding: 16px;
|
||||
background: var(--surface-2);
|
||||
border: 1px solid var(--line);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.help-content > * + * {
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.help-content h1,
|
||||
.help-content h2,
|
||||
.help-content h3 {
|
||||
line-height: 1.25;
|
||||
}
|
||||
|
||||
.help-content h1 {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.help-content h2 {
|
||||
margin-top: 20px;
|
||||
color: var(--accent);
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.help-content h3 {
|
||||
color: var(--accent-2);
|
||||
}
|
||||
|
||||
.help-content p,
|
||||
.help-content li {
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.help-content ul,
|
||||
.help-content ol {
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
.help-content code {
|
||||
padding: 1px 4px;
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--line);
|
||||
color: var(--text);
|
||||
font-family: var(--mono);
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.help-error {
|
||||
padding: 12px;
|
||||
background: rgba(231, 110, 94, 0.08);
|
||||
border: 1px solid rgba(231, 110, 94, 0.48);
|
||||
color: var(--bad);
|
||||
}
|
||||
|
||||
.trace-item {
|
||||
display: grid;
|
||||
grid-template-columns: 38px minmax(0, 1fr) auto;
|
||||
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
# License information
|
||||
|
||||
## Contribution License Agreement
|
||||
|
||||
If you contribute code to this project, you are implicitly allowing your code
|
||||
to be distributed under the MIT license. You are also implicitly verifying that
|
||||
all code is your original work. `</legalese>`
|
||||
|
||||
## Marked
|
||||
|
||||
Copyright (c) 2018+, MarkedJS (https://github.com/markedjs/)
|
||||
Copyright (c) 2011-2018, Christopher Jeffrey (https://github.com/chjj/)
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
|
||||
## Markdown
|
||||
|
||||
Copyright © 2004, John Gruber
|
||||
http://daringfireball.net/
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
|
||||
* Neither the name “Markdown” nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
|
||||
|
||||
This software is provided by the copyright holders and contributors “as is” and any express or implied warranties, including, but not limited to, the implied warranties of merchantability and fitness for a particular purpose are disclaimed. In no event shall the copyright owner or contributors be liable for any direct, indirect, incidental, special, exemplary, or consequential damages (including, but not limited to, procurement of substitute goods or services; loss of use, data, or profits; or business interruption) however caused and on any theory of liability, whether in contract, strict liability, or tort (including negligence or otherwise) arising in any way out of the use of this software, even if advised of the possibility of such damage.
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user