From 8a13d1e52f9e81164f0a2febf04f9ffd050d355c Mon Sep 17 00:00:00 2001 From: pikastech Date: Sun, 19 Jul 2026 18:32:45 +0200 Subject: [PATCH] fix: bind Workbench L0 state from YAML --- config/hwlab-node-lanes.yaml | 1 + scripts/src/hwlab-native-development.test.ts | 1 + scripts/src/hwlab-native-development.ts | 45 +++++++++++++++++++- scripts/src/hwlab-node-lanes.ts | 2 + 4 files changed, 47 insertions(+), 2 deletions(-) diff --git a/config/hwlab-node-lanes.yaml b/config/hwlab-node-lanes.yaml index 2436eb59..009b72de 100644 --- a/config/hwlab-node-lanes.yaml +++ b/config/hwlab-node-lanes.yaml @@ -140,6 +140,7 @@ lanes: upstream: http://127.0.0.1:5174 healthPath: /health/live stateDir: .state/workbench-native/services + stateFile: .state/workbench-native/state.json mode: agentrun-native temporal: address: 10.43.145.243:7233 diff --git a/scripts/src/hwlab-native-development.test.ts b/scripts/src/hwlab-native-development.test.ts index bc1728c6..e45dc659 100644 --- a/scripts/src/hwlab-native-development.test.ts +++ b/scripts/src/hwlab-native-development.test.ts @@ -9,6 +9,7 @@ test("L1 Workbench exposure uses YAML HTTPS origin and separate bind/probe addre expect(native).toBeDefined(); const env = hwlabNativeDevelopmentEnvironment(native!, spec, "0123456789abcdef0123456789abcdef01234567"); expect(env.WORKBENCH_NATIVE_SERVICE_STATE_DIR).toBe(native!.stateDir); + expect(env.WORKBENCH_NATIVE_STATE_FILE).toBe(`${spec.workspace}/${native!.stateFile}`); expect(env.WORKBENCH_MODE).toBe("agentrun-native"); expect(env.WORKBENCH_CLOUD_API_URL).toBeUndefined(); expect(env.WORKBENCH_CLOUD_API_AUTHORIZATION).toBeUndefined(); diff --git a/scripts/src/hwlab-native-development.ts b/scripts/src/hwlab-native-development.ts index 1ee22b42..fb69094b 100644 --- a/scripts/src/hwlab-native-development.ts +++ b/scripts/src/hwlab-native-development.ts @@ -1,3 +1,5 @@ +import { resolve } from "node:path"; + import { runCommand } from "./command"; import { readEnvSourceFile, requiredEnvValue } from "./secrets"; import { hwlabRuntimeLaneConfigPath, hwlabRuntimeLaneSpecForNode, isHwlabRuntimeLane, type HwlabRuntimeLaneSpec, type HwlabRuntimeNativeDevelopmentSpec } from "./hwlab-node-lanes"; @@ -12,6 +14,7 @@ export function hwlabNativeDevelopmentHelp(): Record { command: "hwlab nodes native-development workbench", usage: [ "bun scripts/cli.ts hwlab nodes native-development workbench api|worker|web start|stop|restart|status|logs --node --lane ", + "bun scripts/cli.ts hwlab nodes native-development workbench cli --node --lane ", "bun scripts/cli.ts hwlab nodes native-development caserun api|web start|stop|restart|status|logs --node --lane ", ], contract: "L1 API/Web 用户入口只输出 owning YAML 解析出的固定 HTTPS 域名;IP、port 和 bind/probe 地址不作为用户入口。", @@ -23,8 +26,6 @@ export function runHwlabNativeDevelopmentCommand(args: string[]): Record); + if (application === "workbench" && serviceRaw === "cli") { + const commandArgs = withoutOptions(args.slice(2), ["--node", "--lane"]); + if (commandArgs.length === 0) throw new Error("native-development workbench cli requires a Workbench command"); + const command = ["bun", "tools/hwlab-cli/bin/hwlab-cli.ts", "workbench", ...commandArgs]; + const result = runCommand(command, spec.workspace, { + timeoutMs: cliTimeoutMs(commandArgs), + env: { ...process.env, ...env, WORKBENCH_API_URL: env.WORKBENCH_NATIVE_API_URL }, + }); + return { + ok: result.exitCode === 0, + command: "hwlab nodes native-development workbench cli", + node, + lane, + transport: commandArgs.includes("--over-api") ? "native-api" : "local-dispatcher", + configSource: `${hwlabRuntimeLaneConfigPath()}#lanes.${lane}.targets.${node}.nativeDevelopment.workbench`, + workbenchResult: parsePayload(result.stdout), + ...(result.exitCode === 0 ? {} : { error: { code: "hwlab_native_cli_failed", exitCode: result.exitCode, stderr: result.stderr.trim().slice(-2000) } }), + }; + } + const service = parseService(serviceRaw, application); + const action = parseAction(actionRaw); const command = ["bun", "tools/hwlab-cli/bin/hwlab-cli.ts", application, "service", service, action]; const result = runCommand(command, spec.workspace, { timeoutMs: 15_000, env: { ...process.env, ...env } }); const payload = parsePayload(result.stdout); @@ -119,6 +141,7 @@ export function hwlabNativeDevelopmentEnvironment(native: HwlabRuntimeNativeDeve HWLAB_KAFKA_CLIENT_ID: native.kafka.clientId, HWLAB_KAFKA_HWLAB_EVENT_GROUP_ID: native.kafka.hwlabEventConsumerGroupId, WORKBENCH_NATIVE_SERVICE_STATE_DIR: native.stateDir, + WORKBENCH_NATIVE_STATE_FILE: resolve(spec.workspace, native.stateFile), WORKBENCH_API_BIND_HOST: native.api.bindHost, WORKBENCH_API_PROBE_HOST: native.api.probeHost, WORKBENCH_PUBLIC_BASE_URL: native.publicExposure.publicBaseUrl, @@ -170,6 +193,24 @@ function requiredOption(args: string[], option: string): string { return value; } +function withoutOptions(args: string[], options: string[]): string[] { + const result: string[] = []; + for (let index = 0; index < args.length; index += 1) { + if (options.includes(args[index])) { + index += 1; + continue; + } + result.push(args[index]); + } + return result; +} + +function cliTimeoutMs(args: string[]): number { + const index = args.indexOf("--timeout-ms"); + const requested = index >= 0 ? Number.parseInt(args[index + 1] ?? "", 10) : 0; + return Number.isSafeInteger(requested) && requested > 0 ? requested + 5_000 : 30_000; +} + function hwlabCaseRunNativeDevelopmentEnvironment(native: NonNullable): NodeJS.ProcessEnv { return { CASERUN_NATIVE_SERVICE_STATE_DIR: native.stateDir, diff --git a/scripts/src/hwlab-node-lanes.ts b/scripts/src/hwlab-node-lanes.ts index 360a353e..a2ba3f42 100644 --- a/scripts/src/hwlab-node-lanes.ts +++ b/scripts/src/hwlab-node-lanes.ts @@ -676,6 +676,7 @@ export interface HwlabRuntimeNativeDevelopmentSpec { readonly workbench: { readonly publicExposure: HwlabRuntimeNativePublicExposureSpec; readonly stateDir: string; + readonly stateFile: string; readonly mode: "agentrun-native"; readonly temporal: { readonly address: string; @@ -1499,6 +1500,7 @@ function nativeDevelopmentConfig(value: unknown, path: string): HwlabRuntimeNati workbench: { publicExposure, stateDir: relativeWorkspacePathField(stringField(workbench, "stateDir", `${path}.workbench`), `${path}.workbench.stateDir`), + stateFile: relativeWorkspacePathField(stringField(workbench, "stateFile", `${path}.workbench`), `${path}.workbench.stateFile`), mode, temporal: { address: stringField(temporal, "address", `${path}.workbench.temporal`),