// 规格:PJ2026-010104 draft-2026-07-10-g14-wsl-python-hwpod-node。 // 职责:通过 YAML-first 管理 trans 可达的 Windows Python HWPOD 节点部署和状态。 import { createHash, randomBytes } from "node:crypto"; import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"; import { dirname, isAbsolute, join } from "node:path"; import { repoRoot, rootPath } from "./config"; import { runCommand, type CommandResult } from "./command"; type HwpodNodeAction = "plan" | "status" | "apply"; type HwpodNodeComponent = "all" | "cloud-auth" | "desktop"; interface HwpodNodeOptions { action: HwpodNodeAction; component: HwpodNodeComponent; node: string; lane: string; confirm: boolean; dryRun: boolean; } interface SecretSpec { sourceRef: string; sourceKey: string; generateIfMissing: boolean; secretName: string; targetKey: string; } interface SourceSpec { sourceId: string; projectId: string; hwpodId: string; nodeId: string; workspaceRootRef: string; mdtodoRootRef: string; capabilities: Record; } interface HwpodNodeSpec { configPath: string; node: string; lane: string; providerId: string; windowsRoute: string; nodeId: string; hwpodId: string; interactiveUser: string; publicOrigin: string; nodeOpsUrl: string; cloudKubeRoute: string; cloudNamespace: string; cloudSecret: SecretSpec; opsAuth: { sourceRef: string; sourceKey: string }; python: { probeLauncherPath: string; guiLauncherPath: string; launcherArgs: string[]; expectedVersionPrefix: string; }; runtime: { root: string; scriptPath: string; configPath: string; credentialPath: string; logPath: string; }; artifact: { metadataUrl: string; channel: string; platform: string }; workspace: { allowedRoots: string[]; defaultRoot: string }; desktopConfig: { serverUrl: string; autoConnect: boolean; requireCredential: boolean; updateEnabled: boolean; updateAutoApplyWhenIdle: boolean; }; startup: { mode: string; runKeyName: string; initialLaunch: string; processPattern: string }; sourceRef: string; source: SourceSpec; } interface ArtifactBundle { latestVersion: string; downloadUrl: string; sha256: string; content: Buffer; } const CONFIG_PATH = "config/hwlab-hwpod-nodes.yaml"; const SECRET_ROOT = rootPath(".state", "secrets"); export function hwlabHwpodNodeHelp(): Record { return { ok: true, command: "hwlab nodes hwpod-node", description: "通过 YAML 和 trans 部署、查询 Windows 原生 Python HWPOD 节点。", configPath: CONFIG_PATH, actions: { plan: "校验 YAML、SecretRef、发布文件元数据和 Windows 路由,不修改运行面。", status: "查询 Windows 文件、解释器、交互会话、启动项、进程和云端节点操作状态。", apply: "显式确认后同步云端认证 Secret 或部署 Windows Python 图形节点。", }, components: { "cloud-auth": "生成缺失的 YAML 声明凭据,并同步 cloud-api 使用的 Kubernetes Secret。", desktop: "校验发布文件 SHA 后部署桌面节点、配置、凭据和 HKCU Run 启动项。", all: "按 cloud-auth、desktop 顺序执行。", }, examples: [ "bun scripts/cli.ts hwlab nodes hwpod-node plan --node G14-WSL --lane v03", "bun scripts/cli.ts hwlab nodes hwpod-node status --node G14-WSL --lane v03", "bun scripts/cli.ts hwlab nodes hwpod-node apply --node G14-WSL --lane v03 --component cloud-auth --confirm", "bun scripts/cli.ts hwlab nodes hwpod-node apply --node G14-WSL --lane v03 --component desktop --confirm", ], valuesRedacted: true, }; } export async function runHwlabHwpodNodeCommand(args: string[]): Promise> { if (args.length === 0 || args.includes("--help") || args.includes("-h") || args[0] === "help") return hwlabHwpodNodeHelp(); const options = parseOptions(args); const spec = readSpec(options.node, options.lane); const artifact = await readArtifact(spec, false); const secretBefore = readSecretMaterial(spec.cloudSecret); const remoteBefore = remoteStatus(spec); const graph = configGraph(spec, secretBefore, artifact, remoteBefore); if (options.action === "plan") return resultEnvelope(options, spec, graph, null, null, null); const cloudBefore = await cloudProbe(spec); if (options.action === "status") return resultEnvelope(options, spec, graph, remoteBefore.payload, cloudBefore, null); if (options.dryRun) return resultEnvelope(options, spec, graph, remoteBefore.payload, cloudBefore, { status: "dry-run", component: options.component }); if (!options.confirm) throw new Error("hwpod-node apply 必须显式使用 --confirm 或 --dry-run"); let cloudApply: Record | null = null; let desktopApply: Record | null = null; if (options.component === "all" || options.component === "cloud-auth") cloudApply = applyCloudAuth(spec); if (options.component === "all" || options.component === "desktop") { const secret = readSecretMaterial(spec.cloudSecret); if (!secret.present || secret.value === null) throw new Error(`${spec.cloudSecret.sourceRef}.${spec.cloudSecret.sourceKey} 缺失;先应用 cloud-auth`); const fullArtifact = await readArtifact(spec, true); desktopApply = applyDesktop(spec, fullArtifact, secret.value); } await delay(2500); const remoteAfter = remoteStatus(spec); const cloudAfter = await cloudProbe(spec); const applyResult = { status: "applied", component: options.component, cloudAuth: cloudApply, desktop: desktopApply, valuesRedacted: true }; return resultEnvelope(options, spec, configGraph(spec, readSecretMaterial(spec.cloudSecret), artifact, remoteAfter), remoteAfter.payload, cloudAfter, applyResult); } function parseOptions(args: string[]): HwpodNodeOptions { const action = args[0]; if (action !== "plan" && action !== "status" && action !== "apply") { throw new Error("hwpod-node 用法:plan|status|apply --node NODE --lane LANE [--component all|cloud-auth|desktop] [--confirm|--dry-run]"); } const knownValue = new Set(["--node", "--lane", "--component"]); const knownFlag = new Set(["--confirm", "--dry-run", "--full"]); for (let index = 1; index < args.length; index += 1) { const item = args[index]; if (knownValue.has(item)) { index += 1; if (args[index] === undefined) throw new Error(`${item} 缺少值`); continue; } if (!knownFlag.has(item)) throw new Error(`hwpod-node 不支持参数 ${item}`); } const node = option(args, "--node"); const lane = option(args, "--lane"); const componentRaw = option(args, "--component", "all"); if (componentRaw !== "all" && componentRaw !== "cloud-auth" && componentRaw !== "desktop") throw new Error(`--component 不支持 ${componentRaw}`); const confirm = args.includes("--confirm"); const dryRun = args.includes("--dry-run"); if (confirm && dryRun) throw new Error("--confirm 与 --dry-run 不能同时使用"); if (action !== "apply" && (confirm || dryRun)) throw new Error(`${action} 是只读动作,不接受 --confirm 或 --dry-run`); return { action, component: componentRaw, node, lane, confirm, dryRun }; } function option(args: string[], name: string, fallback?: string): string { const index = args.indexOf(name); if (index < 0) { if (fallback !== undefined) return fallback; throw new Error(`缺少 ${name}`); } const value = args[index + 1]; if (!value || value.startsWith("--")) throw new Error(`${name} 缺少值`); return value; } function readSpec(node: string, lane: string): HwpodNodeSpec { const config = yamlFile(CONFIG_PATH); const cloud = record(config.cloud, `${CONFIG_PATH}#cloud`); const targets = record(config.targets, `${CONFIG_PATH}#targets`); const target = record(targets[node], `${CONFIG_PATH}#targets.${node}`); if (target.enabled !== true) throw new Error(`${CONFIG_PATH}#targets.${node}.enabled 必须为 true`); const targetLane = text(target.lane, `targets.${node}.lane`); if (targetLane !== lane) throw new Error(`目标 ${node} 声明 lane=${targetLane},不是 ${lane}`); const python = record(target.python, `targets.${node}.python`); const runtime = record(target.runtime, `targets.${node}.runtime`); const artifact = record(target.artifact, `targets.${node}.artifact`); const workspace = record(target.workspace, `targets.${node}.workspace`); const desktopConfig = record(target.desktopConfig, `targets.${node}.desktopConfig`); const startup = record(target.startup, `targets.${node}.startup`); const nodeAuth = record(cloud.nodeAuth, "cloud.nodeAuth"); const opsAuth = record(cloud.opsAuth, "cloud.opsAuth"); const sourceRef = text(target.projectManagementSourceRef, `targets.${node}.projectManagementSourceRef`); const source = readSource(sourceRef); const allowedRoots = texts(workspace.allowedRoots, `targets.${node}.workspace.allowedRoots`); if (allowedRoots.length === 0) throw new Error(`targets.${node}.workspace.allowedRoots 不能为空`); const spec: HwpodNodeSpec = { configPath: CONFIG_PATH, node, lane, providerId: text(target.providerId, `targets.${node}.providerId`), windowsRoute: text(target.windowsRoute, `targets.${node}.windowsRoute`), nodeId: text(target.nodeId, `targets.${node}.nodeId`), hwpodId: text(target.hwpodId, `targets.${node}.hwpodId`), interactiveUser: text(target.interactiveUser, `targets.${node}.interactiveUser`), publicOrigin: text(cloud.publicOrigin, "cloud.publicOrigin"), nodeOpsUrl: text(cloud.nodeOpsUrl, "cloud.nodeOpsUrl"), cloudKubeRoute: text(cloud.kubeRoute, "cloud.kubeRoute"), cloudNamespace: text(cloud.namespace, "cloud.namespace"), cloudSecret: { sourceRef: text(nodeAuth.sourceRef, "cloud.nodeAuth.sourceRef"), sourceKey: text(nodeAuth.sourceKey, "cloud.nodeAuth.sourceKey"), generateIfMissing: bool(nodeAuth.generateIfMissing, "cloud.nodeAuth.generateIfMissing"), secretName: text(nodeAuth.secretName, "cloud.nodeAuth.secretName"), targetKey: text(nodeAuth.targetKey, "cloud.nodeAuth.targetKey"), }, opsAuth: { sourceRef: text(opsAuth.sourceRef, "cloud.opsAuth.sourceRef"), sourceKey: text(opsAuth.sourceKey, "cloud.opsAuth.sourceKey") }, python: { probeLauncherPath: text(python.probeLauncherPath, `targets.${node}.python.probeLauncherPath`), guiLauncherPath: text(python.guiLauncherPath, `targets.${node}.python.guiLauncherPath`), launcherArgs: texts(python.launcherArgs, `targets.${node}.python.launcherArgs`), expectedVersionPrefix: text(python.expectedVersionPrefix, `targets.${node}.python.expectedVersionPrefix`), }, runtime: { root: text(runtime.root, `targets.${node}.runtime.root`), scriptPath: text(runtime.scriptPath, `targets.${node}.runtime.scriptPath`), configPath: text(runtime.configPath, `targets.${node}.runtime.configPath`), credentialPath: text(runtime.credentialPath, `targets.${node}.runtime.credentialPath`), logPath: text(runtime.logPath, `targets.${node}.runtime.logPath`), }, artifact: { metadataUrl: text(artifact.metadataUrl, `targets.${node}.artifact.metadataUrl`), channel: text(artifact.channel, `targets.${node}.artifact.channel`), platform: text(artifact.platform, `targets.${node}.artifact.platform`), }, workspace: { allowedRoots, defaultRoot: text(workspace.defaultRoot, `targets.${node}.workspace.defaultRoot`) }, desktopConfig: { serverUrl: text(desktopConfig.serverUrl, `targets.${node}.desktopConfig.serverUrl`), autoConnect: bool(desktopConfig.autoConnect, `targets.${node}.desktopConfig.autoConnect`), requireCredential: bool(desktopConfig.requireCredential, `targets.${node}.desktopConfig.requireCredential`), updateEnabled: bool(desktopConfig.updateEnabled, `targets.${node}.desktopConfig.updateEnabled`), updateAutoApplyWhenIdle: bool(desktopConfig.updateAutoApplyWhenIdle, `targets.${node}.desktopConfig.updateAutoApplyWhenIdle`), }, startup: { mode: text(startup.mode, `targets.${node}.startup.mode`), runKeyName: text(startup.runKeyName, `targets.${node}.startup.runKeyName`), initialLaunch: text(startup.initialLaunch, `targets.${node}.startup.initialLaunch`), processPattern: text(startup.processPattern, `targets.${node}.startup.processPattern`), }, sourceRef, source, }; validateCrossRefs(spec); return spec; } function readSource(ref: string): SourceSpec { const parsed = configRef(ref); const doc = yamlFile(parsed.file); const value = pathValue(doc, parsed.path); const source = record(value, ref); const capabilitiesRaw = record(source.capabilities, `${ref}.capabilities`); return { sourceId: text(source.sourceId, `${ref}.sourceId`), projectId: text(source.projectId, `${ref}.projectId`), hwpodId: text(source.hwpodId, `${ref}.hwpodId`), nodeId: text(source.nodeId, `${ref}.nodeId`), workspaceRootRef: text(source.workspaceRootRef, `${ref}.workspaceRootRef`), mdtodoRootRef: text(source.mdtodoRootRef, `${ref}.mdtodoRootRef`), capabilities: Object.fromEntries(Object.entries(capabilitiesRaw).map(([key, value]) => [key, bool(value, `${ref}.capabilities.${key}`)])), }; } function validateCrossRefs(spec: HwpodNodeSpec): void { const conflicts: string[] = []; if (spec.source.nodeId !== spec.nodeId) conflicts.push(`source.nodeId=${spec.source.nodeId} != target.nodeId=${spec.nodeId}`); if (spec.source.hwpodId !== spec.hwpodId) conflicts.push(`source.hwpodId=${spec.source.hwpodId} != target.hwpodId=${spec.hwpodId}`); if (!spec.workspace.allowedRoots.includes(spec.workspace.defaultRoot)) conflicts.push("workspace.defaultRoot 不在 allowedRoots 中"); if (spec.source.workspaceRootRef !== spec.workspace.defaultRoot) conflicts.push("source.workspaceRootRef 与 workspace.defaultRoot 不一致"); if (spec.startup.mode !== "hkcu-run") conflicts.push(`startup.mode 仅支持 hkcu-run,实际为 ${spec.startup.mode}`); if (!spec.windowsRoute.endsWith(":win")) conflicts.push("windowsRoute 必须指向 :win plane"); if (conflicts.length > 0) throw new Error(`HWPOD 节点配置冲突:${conflicts.join(";")}`); } function configGraph(spec: HwpodNodeSpec, secret: ReturnType, artifact: ArtifactBundle, remote: ReturnType): Record { return { ok: remote.reachable && artifact.sha256.length > 0 && (secret.present || spec.cloudSecret.generateIfMissing), configPath: spec.configPath, sourceRef: spec.sourceRef, target: { node: spec.node, lane: spec.lane, providerId: spec.providerId, windowsRoute: spec.windowsRoute, nodeId: spec.nodeId, hwpodId: spec.hwpodId, interactiveUser: spec.interactiveUser, workspaceRoots: spec.workspace.allowedRoots, mdtodoRootRef: spec.source.mdtodoRootRef, }, cloudAuth: { sourceRef: spec.cloudSecret.sourceRef, sourceKey: spec.cloudSecret.sourceKey, sourcePresent: secret.present, fingerprint: secret.fingerprint, secretName: spec.cloudSecret.secretName, targetKey: spec.cloudSecret.targetKey, generateIfMissing: spec.cloudSecret.generateIfMissing, valuesRedacted: true, }, artifact: { latestVersion: artifact.latestVersion, downloadUrl: artifact.downloadUrl, sha256: artifact.sha256, bytes: artifact.content.length }, route: { reachable: remote.reachable, exitCode: remote.result.exitCode, timedOut: remote.result.timedOut }, valuesRedacted: true, }; } function resultEnvelope( options: HwpodNodeOptions, spec: HwpodNodeSpec, graph: Record, remote: Record | null, cloud: Record | null, apply: Record | null, ): Record { const remoteReady = remote?.installed === true && remote?.processReady === true && remote?.interactiveProcessReady === true; const cloudReady = cloud?.ok === true; const ok = options.action === "plan" ? graph.ok === true : options.action === "status" ? remoteReady && cloudReady : apply?.status === "applied" && (options.component === "cloud-auth" || (remoteReady && cloudReady)); return { ok, command: `hwlab nodes hwpod-node ${options.action} --node ${spec.node} --lane ${spec.lane}${options.action === "apply" ? ` --component ${options.component}${options.confirm ? " --confirm" : " --dry-run"}` : ""}`, status: ok ? options.action === "apply" ? "applied" : "ready" : "blocked", graph, remote, cloud, apply, next: { plan: `bun scripts/cli.ts hwlab nodes hwpod-node plan --node ${spec.node} --lane ${spec.lane}`, status: `bun scripts/cli.ts hwlab nodes hwpod-node status --node ${spec.node} --lane ${spec.lane}`, cloudAuth: `bun scripts/cli.ts hwlab nodes hwpod-node apply --node ${spec.node} --lane ${spec.lane} --component cloud-auth --confirm`, desktop: `bun scripts/cli.ts hwlab nodes hwpod-node apply --node ${spec.node} --lane ${spec.lane} --component desktop --confirm`, }, valuesRedacted: true, }; } async function readArtifact(spec: HwpodNodeSpec, includeContent: boolean): Promise { const metadataUrl = new URL(spec.artifact.metadataUrl); metadataUrl.searchParams.set("platform", spec.artifact.platform); metadataUrl.searchParams.set("channel", spec.artifact.channel); metadataUrl.searchParams.set("current", "0.0.0"); const metadataResponse = await fetch(metadataUrl, { signal: AbortSignal.timeout(15000) }); if (!metadataResponse.ok) throw new Error(`HWPOD 节点更新元数据返回 HTTP ${metadataResponse.status}`); const metadata = record(await metadataResponse.json(), "HWPOD 节点更新元数据"); const downloadUrl = text(metadata.downloadUrl, "更新元数据.downloadUrl"); const sha256 = text(metadata.sha256, "更新元数据.sha256").toLowerCase(); const latestVersion = text(metadata.latestVersion, "更新元数据.latestVersion"); const download = new URL(downloadUrl); if (download.hostname !== new URL(spec.publicOrigin).hostname || !download.pathname.endsWith(".py")) throw new Error("更新元数据下载地址不属于 YAML 声明的公共入口或不是 .py 文件"); if (!includeContent) return { latestVersion, downloadUrl, sha256, content: Buffer.alloc(0) }; const response = await fetch(download, { signal: AbortSignal.timeout(30000) }); if (!response.ok) throw new Error(`HWPOD 节点文件下载返回 HTTP ${response.status}`); const content = Buffer.from(await response.arrayBuffer()); const actual = createHash("sha256").update(content).digest("hex"); if (actual !== sha256) throw new Error(`HWPOD 节点文件 SHA 不匹配:expected=${sha256} actual=${actual}`); return { latestVersion, downloadUrl, sha256, content }; } function readSecretMaterial(spec: { sourceRef: string; sourceKey: string }): { present: boolean; value: string | null; fingerprint: string | null; sourcePath: string } { const sourcePath = secretPath(spec.sourceRef); if (!existsSync(sourcePath)) return { present: false, value: null, fingerprint: null, sourcePath }; const values = parseEnv(readFileSync(sourcePath, "utf8")); const value = values[spec.sourceKey] ?? null; return { present: value !== null && value.length > 0, value, fingerprint: value ? sha256(value) : null, sourcePath }; } function ensureSecretMaterial(spec: SecretSpec): { value: string; fingerprint: string; generated: boolean; sourcePath: string } { const current = readSecretMaterial(spec); if (current.present && current.value) return { value: current.value, fingerprint: current.fingerprint ?? sha256(current.value), generated: false, sourcePath: current.sourcePath }; if (!spec.generateIfMissing) throw new Error(`${spec.sourceRef}.${spec.sourceKey} 缺失且 generateIfMissing=false`); const value = randomBytes(32).toString("base64url"); const sourcePath = secretPath(spec.sourceRef); mkdirSync(dirname(sourcePath), { recursive: true }); const existing = existsSync(sourcePath) ? parseEnv(readFileSync(sourcePath, "utf8")) : {}; existing[spec.sourceKey] = value; writeFileSync(sourcePath, `${Object.entries(existing).map(([key, item]) => `${key}=${item}`).join("\n")}\n`, { encoding: "utf8", mode: 0o600 }); chmodSync(sourcePath, 0o600); return { value, fingerprint: sha256(value), generated: true, sourcePath }; } function applyCloudAuth(spec: HwpodNodeSpec): Record { const material = ensureSecretMaterial(spec.cloudSecret); const manifest = JSON.stringify({ apiVersion: "v1", kind: "Secret", metadata: { name: spec.cloudSecret.secretName, namespace: spec.cloudNamespace, labels: { "app.kubernetes.io/managed-by": "unidesk", "hwlab.pikastech.local/config": "hwpod-node-auth" } }, type: "Opaque", data: { [spec.cloudSecret.targetKey]: Buffer.from(material.value, "utf8").toString("base64") }, }); const result = runCommand(["trans", spec.cloudKubeRoute, "kubectl", "-n", spec.cloudNamespace, "apply", "-f", "-"], repoRoot, { input: manifest, timeoutMs: 55000 }); if (!commandSucceeded(result)) throw new Error(`同步 HWPOD 节点认证 Secret 失败:${compactFailure(result)}`); return { ok: true, status: "applied", generated: material.generated, sourceRef: spec.cloudSecret.sourceRef, sourceKey: spec.cloudSecret.sourceKey, fingerprint: material.fingerprint, secretName: spec.cloudSecret.secretName, targetKey: spec.cloudSecret.targetKey, valuesRedacted: true, }; } function applyDesktop(spec: HwpodNodeSpec, artifact: ArtifactBundle, credential: string): Record { const config = { schemaVersion: 1, serverUrl: spec.desktopConfig.serverUrl, nodeId: spec.nodeId, autoConnect: spec.desktopConfig.autoConnect, requireCredential: spec.desktopConfig.requireCredential, credentialFile: spec.runtime.credentialPath, allowedWorkspaceRoots: spec.workspace.allowedRoots, defaultWorkspaceRoot: spec.workspace.defaultRoot, update: { enabled: spec.desktopConfig.updateEnabled, autoApplyWhenIdle: spec.desktopConfig.updateAutoApplyWhenIdle, channel: spec.artifact.channel, metadataUrl: spec.artifact.metadataUrl, downloadHostAllowlist: [new URL(spec.publicOrigin).hostname], }, }; const payload = { interactiveUser: spec.interactiveUser, runtimeRoot: spec.runtime.root, scriptPath: spec.runtime.scriptPath, configPath: spec.runtime.configPath, credentialPath: spec.runtime.credentialPath, logPath: spec.runtime.logPath, probeLauncherPath: spec.python.probeLauncherPath, guiLauncherPath: spec.python.guiLauncherPath, launcherArgs: spec.python.launcherArgs, expectedVersionPrefix: spec.python.expectedVersionPrefix, runKeyName: spec.startup.runKeyName, artifactBase64: artifact.content.toString("base64"), artifactSha256: artifact.sha256, configBase64: Buffer.from(`${JSON.stringify(config, null, 2)}\n`, "utf8").toString("base64"), configSha256: sha256Hex(`${JSON.stringify(config, null, 2)}\n`), credentialBase64: Buffer.from(`${credential}\n`, "utf8").toString("base64"), }; const result = runPowerShell(spec.windowsRoute, desktopApplyScript(payload), 55000); if (!commandSucceeded(result)) throw new Error(`部署 Windows HWPOD 节点失败:${compactFailure(result)}`); const parsed = parseJsonRecord(result.stdout); if (parsed?.ok !== true) throw new Error(`Windows HWPOD 节点部署未就绪:${JSON.stringify(parsed ?? { stdout: result.stdout.slice(-600) })}`); return { ...parsed, artifactVersion: artifact.latestVersion, artifactSha256: artifact.sha256, valuesRedacted: true }; } function remoteStatus(spec: HwpodNodeSpec): { reachable: boolean; payload: Record | null; result: CommandResult } { const payload = { interactiveUser: spec.interactiveUser, runtimeRoot: spec.runtime.root, scriptPath: spec.runtime.scriptPath, configPath: spec.runtime.configPath, credentialPath: spec.runtime.credentialPath, logPath: spec.runtime.logPath, probeLauncherPath: spec.python.probeLauncherPath, launcherArgs: spec.python.launcherArgs, expectedVersionPrefix: spec.python.expectedVersionPrefix, runKeyName: spec.startup.runKeyName, }; const result = runPowerShell(spec.windowsRoute, desktopStatusScript(payload), 45000); const reachable = commandSucceeded(result); return { reachable, payload: reachable ? parseJsonRecord(result.stdout) : null, result }; } async function cloudProbe(spec: HwpodNodeSpec): Promise> { const auth = readSecretMaterial(spec.opsAuth); if (!auth.present || !auth.value) return { ok: false, status: "blocked", blocker: "ops-auth-source-missing", sourceRef: spec.opsAuth.sourceRef, valuesRedacted: true }; const body = { contractVersion: "hwpod-node-ops-v1", planId: `hwpod_status_${Date.now()}`, hwpodId: spec.hwpodId, nodeId: spec.nodeId, intent: "node.version", ops: [{ opId: "op_status", op: "node.version", args: {} }], }; try { const response = await fetch(spec.nodeOpsUrl, { method: "POST", headers: { "content-type": "application/json", authorization: `Bearer ${auth.value}`, "x-source-service-id": "unidesk-hwpod-node-cli" }, body: JSON.stringify(body), signal: AbortSignal.timeout(15000), }); const value = record(await response.json(), "node-ops status response"); const result = Array.isArray(value.results) ? record(value.results[0], "node-ops result") : {}; return { ok: response.ok && value.ok === true && result.ok === true, httpStatus: response.status, status: value.status ?? null, nodeId: spec.nodeId, nodeVersion: record(result.output ?? {}, "node-ops output").version ?? null, blocker: result.blocker ?? value.blocker ?? value.error ?? null, requestId: record(value.requestMeta ?? {}, "requestMeta").requestId ?? null, valuesRedacted: true, }; } catch (error) { return { ok: false, status: "blocked", blocker: error instanceof Error ? error.message : String(error), valuesRedacted: true }; } } function desktopStatusScript(payload: Record): string { return powerShellPayload(payload, ` $ErrorActionPreference = 'Stop' $expectedUser = [string]$payload.interactiveUser $scriptPath = [string]$payload.scriptPath $configPath = [string]$payload.configPath $credentialPath = [string]$payload.credentialPath $logPath = [string]$payload.logPath $probe = [string]$payload.probeLauncherPath $launcherArgs = @($payload.launcherArgs | ForEach-Object { [string]$_ }) $versionOut = @(& $probe @launcherArgs -c "import sys;print(sys.version);print(sys.executable)" 2>&1) $versionExit = $LASTEXITCODE $processes = @(Get-CimInstance Win32_Process | Where-Object { $_.Name -like 'python*.exe' -and $_.CommandLine -and $_.CommandLine.Contains($scriptPath) }) $explorerSessions = @(Get-Process explorer -ErrorAction SilentlyContinue | Select-Object -ExpandProperty SessionId -Unique) $interactive = @($processes | Where-Object { $explorerSessions -contains $_.SessionId }) $runValue = (Get-ItemProperty -Path 'HKCU:\\Software\\Microsoft\\Windows\\CurrentVersion\\Run' -Name ([string]$payload.runKeyName) -ErrorAction SilentlyContinue).([string]$payload.runKeyName) function HashOrNull([string]$path) { if (Test-Path $path) { return 'sha256:' + (Get-FileHash -Algorithm SHA256 $path).Hash.ToLowerInvariant() }; return $null } $result = [ordered]@{ ok = $true status = 'observed' user = $env:USERNAME expectedUser = $expectedUser userMatches = [bool]($env:USERNAME -ieq $expectedUser) pythonReady = [bool]((Test-Path $probe) -and ($versionExit -eq 0) -and (($versionOut -join "\`n") -like "*$($payload.expectedVersionPrefix)*")) pythonSummary = (($versionOut | Select-Object -First 2) -join ' | ') installed = [bool]((Test-Path $scriptPath) -and (Test-Path $configPath) -and (Test-Path $credentialPath)) scriptSha256 = HashOrNull $scriptPath configSha256 = HashOrNull $configPath credentialPresent = [bool](Test-Path $credentialPath) credentialFingerprint = HashOrNull $credentialPath runKeyReady = [bool](-not [string]::IsNullOrWhiteSpace([string]$runValue)) processCount = [int]$processes.Count processReady = [bool]($processes.Count -eq 1) interactiveProcessCount = [int]$interactive.Count interactiveProcessReady = [bool]($interactive.Count -eq 1) processIds = @($processes | ForEach-Object { [int]$_.ProcessId }) explorerSessions = @($explorerSessions) logPresent = [bool](Test-Path $logPath) valuesRedacted = $true } $result | ConvertTo-Json -Compress -Depth 6 `); } function desktopApplyScript(payload: Record): string { return powerShellPayload(payload, ` $ErrorActionPreference = 'Stop' if ($env:USERNAME -ine [string]$payload.interactiveUser) { throw "interactive user mismatch expected=$($payload.interactiveUser) actual=$env:USERNAME" } $root = [string]$payload.runtimeRoot $scriptPath = [string]$payload.scriptPath $configPath = [string]$payload.configPath $credentialPath = [string]$payload.credentialPath $logPath = [string]$payload.logPath $probe = [string]$payload.probeLauncherPath $gui = [string]$payload.guiLauncherPath $launcherArgs = @($payload.launcherArgs | ForEach-Object { [string]$_ }) if (-not (Test-Path $probe)) { throw "Python probe launcher missing: $probe" } if (-not (Test-Path $gui)) { throw "Python GUI launcher missing: $gui" } $versionOut = @(& $probe @launcherArgs -c "import sys;import tkinter;print(sys.version);print(sys.executable);print(tkinter.TkVersion)" 2>&1) if ($LASTEXITCODE -ne 0 -or (($versionOut -join "\`n") -notlike "*$($payload.expectedVersionPrefix)*")) { throw "Python version mismatch: $($versionOut -join ' | ')" } New-Item -ItemType Directory -Force -Path $root, (Split-Path -Parent $configPath), (Split-Path -Parent $credentialPath), (Split-Path -Parent $logPath) | Out-Null $old = @(Get-CimInstance Win32_Process | Where-Object { $_.CommandLine -and $_.CommandLine.Contains($scriptPath) }) foreach ($proc in $old) { Stop-Process -Id $proc.ProcessId -Force -ErrorAction SilentlyContinue } Start-Sleep -Milliseconds 500 [IO.File]::WriteAllBytes($scriptPath, [Convert]::FromBase64String([string]$payload.artifactBase64)) [IO.File]::WriteAllBytes($configPath, [Convert]::FromBase64String([string]$payload.configBase64)) [IO.File]::WriteAllBytes($credentialPath, [Convert]::FromBase64String([string]$payload.credentialBase64)) $artifactActual = (Get-FileHash -Algorithm SHA256 $scriptPath).Hash.ToLowerInvariant() $configActual = (Get-FileHash -Algorithm SHA256 $configPath).Hash.ToLowerInvariant() if ($artifactActual -ne [string]$payload.artifactSha256) { throw "artifact SHA mismatch" } if ($configActual -ne [string]$payload.configSha256) { throw "config SHA mismatch" } $null = & icacls $credentialPath /inheritance:r /grant:r "$env:USERNAME\`:(R,W)" 2>&1 $argText = (($launcherArgs | ForEach-Object { '"' + ($_ -replace '"','\"') + '"' }) -join ' ') $runCommand = '"' + $gui + '" ' + $argText + ' "' + $scriptPath + '"' $null = New-Item -Path 'HKCU:\\Software\\Microsoft\\Windows\\CurrentVersion\\Run' -Force Set-ItemProperty -Path 'HKCU:\\Software\\Microsoft\\Windows\\CurrentVersion\\Run' -Name ([string]$payload.runKeyName) -Value $runCommand $stdoutPath = $logPath + '.stdout' $stderrPath = $logPath + '.stderr' Start-Process -FilePath $gui -ArgumentList @($launcherArgs + @($scriptPath)) -WorkingDirectory $root -RedirectStandardOutput $stdoutPath -RedirectStandardError $stderrPath Start-Sleep -Seconds 3 $processes = @(Get-CimInstance Win32_Process | Where-Object { $_.Name -like 'python*.exe' -and $_.CommandLine -and $_.CommandLine.Contains($scriptPath) }) $explorerSessions = @(Get-Process explorer -ErrorAction SilentlyContinue | Select-Object -ExpandProperty SessionId -Unique) $interactive = @($processes | Where-Object { $explorerSessions -contains $_.SessionId }) $result = [ordered]@{ ok = [bool](($processes.Count -eq 1) -and ($interactive.Count -eq 1)) status = $(if (($processes.Count -eq 1) -and ($interactive.Count -eq 1)) { 'applied' } else { 'installed-not-interactive' }) pythonSummary = (($versionOut | Select-Object -First 3) -join ' | ') scriptSha256 = 'sha256:' + $artifactActual configSha256 = 'sha256:' + $configActual credentialPresent = $true processCount = [int]$processes.Count interactiveProcessCount = [int]$interactive.Count processIds = @($processes | ForEach-Object { [int]$_.ProcessId }) runKeyReady = $true stoppedProcessIds = @($old | ForEach-Object { [int]$_.ProcessId }) valuesRedacted = $true } $result | ConvertTo-Json -Compress -Depth 6 `); } function powerShellPayload(payload: Record, body: string): string { const encoded = Buffer.from(JSON.stringify(payload), "utf8").toString("base64"); return `$payload = ([Text.Encoding]::UTF8.GetString([Convert]::FromBase64String('${encoded}')) | ConvertFrom-Json)\n${body.trim()}\n`; } function runPowerShell(route: string, script: string, timeoutMs: number): CommandResult { return runCommand(["trans", route, "ps"], repoRoot, { input: script, timeoutMs }); } function yamlFile(path: string): Record { const absolute = rootPath(path); if (!existsSync(absolute)) throw new Error(`${path} 不存在`); return record(Bun.YAML.parse(readFileSync(absolute, "utf8")) as unknown, path); } function configRef(ref: string): { file: string; path: string } { const parts = ref.split("#"); if (parts.length !== 2 || !parts[0] || !parts[1]) throw new Error(`配置引用必须使用 path.yaml#object.path:${ref}`); if (isAbsolute(parts[0]) || parts[0].includes("..")) throw new Error(`配置引用文件必须是仓库内相对路径:${ref}`); return { file: parts[0], path: parts[1] }; } function pathValue(input: unknown, path: string): unknown { let current = input; for (const raw of path.replace(/\[(\d+)\]/gu, ".$1").split(".").filter(Boolean)) { if (Array.isArray(current)) current = current[Number(raw)]; else if (current && typeof current === "object") current = (current as Record)[raw]; else return undefined; } return current; } function record(value: unknown, path: string): Record { if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(`${path} 必须是对象`); return value as Record; } function text(value: unknown, path: string): string { if (typeof value !== "string" || value.trim().length === 0) throw new Error(`${path} 必须是非空字符串`); return value.trim(); } function texts(value: unknown, path: string): string[] { if (!Array.isArray(value) || value.some((item) => typeof item !== "string" || item.trim().length === 0)) throw new Error(`${path} 必须是非空字符串数组`); return value.map((item) => String(item)); } function bool(value: unknown, path: string): boolean { if (typeof value !== "boolean") throw new Error(`${path} 必须是布尔值`); return value; } function secretPath(sourceRef: string): string { if (isAbsolute(sourceRef) || sourceRef.includes("..")) throw new Error(`Secret sourceRef 必须是相对路径且不能包含 ..:${sourceRef}`); return join(SECRET_ROOT, sourceRef); } function parseEnv(content: string): Record { const result: Record = {}; for (const raw of content.split(/\r?\n/u)) { const line = raw.trim(); if (!line || line.startsWith("#")) continue; const index = line.indexOf("="); if (index <= 0) continue; result[line.slice(0, index).trim()] = line.slice(index + 1).trim().replace(/^['"]|['"]$/gu, ""); } return result; } function parseJsonRecord(textValue: string): Record | null { const lines = textValue.trim().split(/\r?\n/u).filter(Boolean); for (let index = lines.length - 1; index >= 0; index -= 1) { try { return record(JSON.parse(lines[index]) as unknown, "远端 JSON"); } catch {} } return null; } function compactFailure(result: CommandResult): string { const reason = result.timedOut ? "command-timed-out" : "command-failed"; return `${reason}; exit=${result.exitCode ?? "-"}; stderr=${result.stderr.trim().slice(-500)}`; } function sha256(value: string | Buffer): string { return `sha256:${sha256Hex(value)}`; } function sha256Hex(value: string | Buffer): string { return createHash("sha256").update(value).digest("hex"); } function commandSucceeded(result: CommandResult): boolean { return result.exitCode === 0 && !result.timedOut; } function delay(ms: number): Promise { return new Promise((resolve) => setTimeout(resolve, ms)); }