@@ -0,0 +1,712 @@
// 规格: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 < string , boolean > ;
}
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 < string , unknown > {
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 < Record < string , unknown > > {
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 < string , unknown > | null = null ;
let desktopApply : Record < string , unknown > | 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 < typeof readSecretMaterial > , artifact : ArtifactBundle , remote : ReturnType < typeof remoteStatus > ) : Record < string , unknown > {
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 < string , unknown > ,
remote : Record < string , unknown > | null ,
cloud : Record < string , unknown > | null ,
apply : Record < string , unknown > | null ,
) : Record < string , unknown > {
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 < ArtifactBundle > {
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 , 0 o600 ) ;
return { value , fingerprint : sha256 ( value ) , generated : true , sourcePath } ;
}
function applyCloudAuth ( spec : HwpodNodeSpec ) : Record < string , unknown > {
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 < string , unknown > {
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 ,
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 < string , unknown > | 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 < Record < string , unknown > > {
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 , unknown > ) : 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 { $ _.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 , unknown > ) : 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
$ 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) | 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
Start-Process -FilePath $ gui -ArgumentList @( $ launcherArgs + @( $ scriptPath)) -WorkingDirectory $ root
Start-Sleep -Seconds 3
$ processes = @(Get-CimInstance Win32_Process | Where-Object { $ _.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 < string , unknown > , 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 < string , unknown > {
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 + ) \ ] / g u , " . $ 1 " ) . s p l i t ( " . " ) . f i l t e r ( B o o l e a n ) ) {
if ( Array . isArray ( current ) ) current = current [ Number ( raw ) ] ;
else if ( current && typeof current === "object" ) current = ( current as Record < string , unknown > ) [ raw ] ;
else return undefined ;
}
return current ;
}
function record ( value : unknown , path : string ) : Record < string , any > {
if ( ! value || typeof value !== "object" || Array . isArray ( value ) ) throw new Error ( ` ${ path } 必须是对象 ` ) ;
return value as Record < string , any > ;
}
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 < string , string > {
const result : Record < string , string > = { } ;
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 ( / ^ [ ' " ] | [ ' " ] $ / g u , " " ) ;
}
return result ;
}
function parseJsonRecord ( textValue : string ) : Record < string , unknown > | null {
const lines = textValue . trim ( ) . split ( / \ r ? \ n / u ) . f i l t e r ( B o o l e a n ) ;
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 < void > {
return new Promise ( ( resolve ) = > setTimeout ( resolve , ms ) ) ;
}