diff --git a/internal/cloud/hwpod-topology-read-model.test.ts b/internal/cloud/hwpod-topology-read-model.test.ts index 16348dd1..a2a59b5a 100644 --- a/internal/cloud/hwpod-topology-read-model.test.ts +++ b/internal/cloud/hwpod-topology-read-model.test.ts @@ -58,6 +58,9 @@ test("typed topology covers multi-device, offline, unconfigured, invalid, mismat assert.equal(nodeA.diagnostic.message.includes("[REDACTED]"), true); assert.equal(nodeA.diagnostic.valuesRedacted, true); assert.equal(nodeA.readiness.find((stage: any) => stage.id === "desktop-visible").status, "ready"); + assert.equal(nodeA.readiness.find((stage: any) => stage.id === "desktop-visible").nextAction, null); + assert.equal(nodeA.readiness.find((stage: any) => stage.id === "hwpod-ready").nextAction.entrypoint, "hwpod-topology"); + assert.equal(nodePayload.items.find((item: any) => item.nodeId === "node-b").readiness.find((stage: any) => stage.id === "workspace-ready").blocker.code, "hwpod_node_spec_missing"); assert.equal(nodePayload.items.find((item: any) => item.nodeId === "node-b").status, "unconfigured"); assert.equal(nodePayload.items.find((item: any) => item.nodeId === "node-c").status, "offline"); assert.equal(nodePayload.invalidSpecs[0].blocker.code, "invalid_hwpod_spec"); diff --git a/internal/cloud/hwpod-topology-read-model.ts b/internal/cloud/hwpod-topology-read-model.ts index ffa50071..272abab4 100644 --- a/internal/cloud/hwpod-topology-read-model.ts +++ b/internal/cloud/hwpod-topology-read-model.ts @@ -9,6 +9,15 @@ export const HWPOD_TOPOLOGY_CONTRACT_VERSION = "hwpod-topology-v1"; const DEFAULT_PAGE_LIMIT = 50; const MAX_PAGE_LIMIT = 100; +export const HWPOD_NODE_READINESS_DEFINITIONS = [ + { id: "downloaded", label: "已下载", authority: "user-device", nextAction: { label: "从节点接入页下载并校验已发布 Python 单文件", entrypoint: "cloud-web-hwpod-onboarding" } }, + { id: "installed", label: "已安装", authority: "python-node-runtime", nextAction: { label: "按 owning YAML 选择的交互用户与原生 Python 启动节点", entrypoint: "yaml-managed-hwpod-node" } }, + { id: "desktop-visible", label: "桌面可见", authority: "python-node-runtime", nextAction: { label: "在目标交互登录会话确认窗口或托盘可见", entrypoint: "hwlab-node-ui" } }, + { id: "registered", label: "已注册", authority: "websocket-registry", nextAction: { label: "保持节点运行并通过受控节点状态入口检查注册", entrypoint: "hwpod-node-status" } }, + { id: "workspace-ready", label: "工作区就绪", authority: "hwpod-spec-probe", nextAction: { label: "通过显式 readiness 探测检查 owning workspace", entrypoint: "hwpod-readiness-probe" } }, + { id: "hwpod-ready", label: "HWPOD 就绪", authority: "hwpod-topology", nextAction: { label: "检查挂载 HWPOD 的 blocker、能力与占用状态", entrypoint: "hwpod-topology" } } +] as const; + export function buildHwpodTopologyReadModel(specs: any[], registrySnapshot: any = {}, options: any = {}) { const observedAt = text(options.observedAt) || new Date().toISOString(); const onlineNodes = normalizeOnlineNodes(registrySnapshot); @@ -267,17 +276,18 @@ function nodeReadiness(input: any) { const localObserved = Boolean(input.onlineNode); const stageBlocker = input.blockers[0] ?? null; return [ - readinessStage("downloaded", "已下载", input.onlineNode?.installed === true ? "ready" : "unknown", "python-node-runtime", input.onlineNode?.installed === true ? null : blocker("hwpod_download_not_observed", "云端不以浏览器下载动作作为安装事实。", false)), - readinessStage("installed", "已安装", input.onlineNode?.installed === true ? "ready" : "unknown", "python-node-runtime", input.onlineNode?.installed === true ? null : blocker("hwpod_install_not_observed", "等待 Python 节点报告安装运行时。", true)), - readinessStage("desktop-visible", "桌面可见", input.onlineNode?.desktopVisible === true ? "ready" : localObserved ? "blocked" : "unknown", "python-node-runtime", input.onlineNode?.desktopVisible === true ? null : blocker("hwpod_desktop_not_visible", "尚未证明图形界面与托盘属于交互登录会话。", true)), - readinessStage("registered", "已注册", input.online ? "ready" : "blocked", "websocket-registry", input.online ? null : blocker("hwpod_node_offline", "未观测到已认证的主动出站 WebSocket。", true)), - readinessStage("workspace-ready", "工作区就绪", input.workspaceReady ? "ready" : "blocked", "hwpod-spec-probe", input.workspaceReady ? null : stageBlocker), - readinessStage("hwpod-ready", "HWPOD 就绪", input.hwpodReady && !input.busy ? "ready" : "blocked", "hwpod-topology", input.hwpodReady && !input.busy ? null : stageBlocker) + readinessStage("downloaded", input.onlineNode?.installed === true ? "ready" : "unknown", input.onlineNode?.installed === true ? null : blocker("hwpod_download_not_observed", "云端不以浏览器下载动作作为安装事实。", false)), + readinessStage("installed", input.onlineNode?.installed === true ? "ready" : "unknown", input.onlineNode?.installed === true ? null : blocker("hwpod_install_not_observed", "等待 Python 节点报告安装运行时。", true)), + readinessStage("desktop-visible", input.onlineNode?.desktopVisible === true ? "ready" : localObserved ? "blocked" : "unknown", input.onlineNode?.desktopVisible === true ? null : blocker("hwpod_desktop_not_visible", "尚未证明图形界面与托盘属于交互登录会话。", true)), + readinessStage("registered", input.online ? "ready" : "blocked", input.online ? null : blocker("hwpod_node_offline", "未观测到已认证的主动出站 WebSocket。", true)), + readinessStage("workspace-ready", input.workspaceReady ? "ready" : "blocked", input.workspaceReady ? null : stageBlocker ?? blocker("hwpod_workspace_not_ready", "工作区尚未通过显式 readiness 探测。", true)), + readinessStage("hwpod-ready", input.hwpodReady && !input.busy ? "ready" : "blocked", input.hwpodReady && !input.busy ? null : stageBlocker ?? blocker("hwpod_not_ready", "尚未观测到可用且空闲的挂载 HWPOD。", true)) ]; } -function readinessStage(id: string, label: string, status: string, authority: string, stageBlocker: any) { - return { id, label, status, authority, blocker: stageBlocker }; +function readinessStage(id: string, status: string, stageBlocker: any) { + const definition = HWPOD_NODE_READINESS_DEFINITIONS.find((stage) => stage.id === id)!; + return { ...definition, status, blocker: stageBlocker, nextAction: status === "ready" ? null : definition.nextAction }; } function invalidSpecSummary(spec: any) { diff --git a/internal/cloud/server-hwpod-http.ts b/internal/cloud/server-hwpod-http.ts index 2719c3f3..222673ef 100644 --- a/internal/cloud/server-hwpod-http.ts +++ b/internal/cloud/server-hwpod-http.ts @@ -15,7 +15,8 @@ import { hwpodSpecWorkspaceProbePlan } from "./hwpod-spec-discovery.ts"; import { - buildHwpodTopologyReadModel + buildHwpodTopologyReadModel, + HWPOD_NODE_READINESS_DEFINITIONS } from "./hwpod-topology-read-model.ts"; import { getHeader, @@ -231,14 +232,7 @@ function hwlabNodeReleaseNotes(env) { } function hwlabNodeReadinessStages() { - return [ - { id: "downloaded", label: "已下载", authority: "user-device" }, - { id: "installed", label: "已安装", authority: "python-node-runtime" }, - { id: "desktop-visible", label: "桌面可见", authority: "python-node-runtime" }, - { id: "registered", label: "已注册", authority: "websocket-registry" }, - { id: "workspace-ready", label: "工作区就绪", authority: "hwpod-spec-probe" }, - { id: "hwpod-ready", label: "HWPOD 就绪", authority: "hwpod-topology" } - ]; + return HWPOD_NODE_READINESS_DEFINITIONS.map((stage) => ({ ...stage, nextAction: { ...stage.nextAction } })); } function hwlabNodeDownloadUrl(env) { diff --git a/web/hwlab-cloud-web/src/components/hwpod/HwpodOnboardingWorkspace.vue b/web/hwlab-cloud-web/src/components/hwpod/HwpodOnboardingWorkspace.vue index 2c17a6cd..26ded5f6 100644 --- a/web/hwlab-cloud-web/src/components/hwpod/HwpodOnboardingWorkspace.vue +++ b/web/hwlab-cloud-web/src/components/hwpod/HwpodOnboardingWorkspace.vue @@ -61,7 +61,7 @@ const formatSize = (value: number): string => v-if="metadata?.artifact.downloadUrl" class="download-button" :href="metadata.artifact.downloadUrl" - download="hwlab-node.py" + :download="metadata.artifact.fileName" >下载 Python 单文件
发布 artifact 当前不可用。
@@ -92,29 +92,21 @@ const formatSize = (value: number): string =>将单文件保存到用户可写目录,不要从浏览器临时目录长期运行。
+将单文件保存到 owning YAML 允许的用户可写目录,不要从浏览器临时目录长期运行。
mkdir C:\HWLAB
- move .\{{ metadata?.artifact.fileName || "hwlab-node.py" }} C:\HWLAB\hwlab-node.py
+ 将本机结果与页面上方发布 SHA 完整比对,不一致时停止运行并重新下载。
-Get-FileHash C:\HWLAB\hwlab-node.py -Algorithm SHA256
- 使用本机 Python 3 启动图形节点,保持窗口或托盘程序运行。
-py -3 C:\HWLAB\hwlab-node.py
- python C:\HWLAB\hwlab-node.py
- 使用 owning YAML 选择的 Windows 交互用户与原生 Python 启动图形节点,保持窗口或托盘程序运行。
+{{ stage.blocker.summary }}
+下一步{{ stage.nextAction.label }}{{ stage.nextAction.entrypoint }}