feat: 统一 L1 公网调试入口配置
This commit is contained in:
@@ -47,6 +47,11 @@ description: >-
|
||||
- Web 使用 `$unidesk-webdev` 的 `web-probe` custom/local 入口访问 native Web。
|
||||
- 微服务项目只启动当前微服务的前端、API、Worker及必要依赖。
|
||||
- 前端、API 和 Worker可以分别启动、查看日志、重启和停止。
|
||||
- 对用户披露的 native API/Web 调试入口必须是 owning YAML 声明的公网 `IP:port`:
|
||||
- `0.0.0.0` 只表示进程 bind;
|
||||
- `127.0.0.1` 只用于本机 probe 或进程间代理;
|
||||
- bind/probe 地址不得作为 L1 用户入口返回;
|
||||
- 公网 IP、API port 和 Web port 缺失时必须明确失败,不得回退到 localhost 或代码默认值。
|
||||
- 适合前后端联调、异步作业、Workflow、网络接口和页面交互的快速开发。
|
||||
|
||||
## L2 Development
|
||||
|
||||
@@ -111,6 +111,22 @@ lanes:
|
||||
browserCommand: npx playwright install chromium
|
||||
stateDir: /tmp/hwlab-v03-source-workspace-bootstrap
|
||||
timeoutSeconds: 1800
|
||||
nativeDevelopment:
|
||||
workbench:
|
||||
publicHostRef: config/unidesk-host-k8s.yaml#runtime.publicHost
|
||||
stateDir: .state/workbench-native/services
|
||||
api:
|
||||
bindHost: 0.0.0.0
|
||||
probeHost: 127.0.0.1
|
||||
port: 6677
|
||||
worker:
|
||||
bindHost: 127.0.0.1
|
||||
probeHost: 127.0.0.1
|
||||
healthPort: 6678
|
||||
web:
|
||||
bindHost: 0.0.0.0
|
||||
probeHost: 127.0.0.1
|
||||
port: 5173
|
||||
sourceAuthority:
|
||||
extends: templates.hwlabV03.sourceAuthorityGiteaSnapshot
|
||||
giteaMirrorRepoKey: hwlab-nc01-v03
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import { expect, test } from "bun:test";
|
||||
|
||||
import { hwlabNativeDevelopmentEnvironment, hwlabNativeDevelopmentHelp } from "./hwlab-native-development";
|
||||
import { hwlabRuntimeLaneSpecForNode } from "./hwlab-node-lanes";
|
||||
|
||||
test("L1 Workbench exposure uses YAML public IP and separate bind/probe addresses", () => {
|
||||
const native = hwlabRuntimeLaneSpecForNode("v03", "NC01").nativeDevelopment?.workbench;
|
||||
expect(native).toBeDefined();
|
||||
const env = hwlabNativeDevelopmentEnvironment(native!);
|
||||
expect(env.WORKBENCH_NATIVE_SERVICE_STATE_DIR).toBe(native!.stateDir);
|
||||
expect(env.WORKBENCH_API_BIND_HOST).toBe(native!.api.bindHost);
|
||||
expect(env.WORKBENCH_API_PROBE_HOST).toBe(native!.api.probeHost);
|
||||
expect(env.WORKBENCH_API_PUBLIC_HOST).toBe(native!.publicHost);
|
||||
expect(env.WORKBENCH_WEB_PUBLIC_HOST).toBe(native!.publicHost);
|
||||
expect(hwlabNativeDevelopmentHelp()).toMatchObject({ ok: true, command: "hwlab nodes native-development workbench" });
|
||||
});
|
||||
@@ -0,0 +1,94 @@
|
||||
import { runCommand } from "./command";
|
||||
import { hwlabRuntimeLaneConfigPath, hwlabRuntimeLaneSpecForNode, isHwlabRuntimeLane, type HwlabRuntimeNativeDevelopmentSpec } from "./hwlab-node-lanes";
|
||||
|
||||
type Service = "api" | "worker" | "web";
|
||||
type Action = "start" | "stop" | "restart" | "status" | "logs";
|
||||
|
||||
export function hwlabNativeDevelopmentHelp(): Record<string, unknown> {
|
||||
return {
|
||||
ok: true,
|
||||
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 <node> --lane <lane>",
|
||||
],
|
||||
contract: "L1 API/Web 用户入口只输出 owning YAML 解析出的公网 IP 和 port;bind/probe 地址不作为用户入口。",
|
||||
configPath: hwlabRuntimeLaneConfigPath(),
|
||||
};
|
||||
}
|
||||
|
||||
export function runHwlabNativeDevelopmentCommand(args: string[]): Record<string, unknown> {
|
||||
if (args.length === 0 || args.includes("--help") || args.includes("-h")) return hwlabNativeDevelopmentHelp();
|
||||
const [application, serviceRaw, actionRaw] = args;
|
||||
if (application !== "workbench") throw new Error("native-development currently supports workbench");
|
||||
const service = parseService(serviceRaw);
|
||||
const action = parseAction(actionRaw);
|
||||
const node = requiredOption(args, "--node");
|
||||
const lane = requiredOption(args, "--lane");
|
||||
if (!isHwlabRuntimeLane(lane)) throw new Error(`--lane must be declared in ${hwlabRuntimeLaneConfigPath()}`);
|
||||
const spec = hwlabRuntimeLaneSpecForNode(lane, node);
|
||||
const native = spec.nativeDevelopment?.workbench;
|
||||
if (native === undefined) throw new Error(`${hwlabRuntimeLaneConfigPath()}#lanes.${lane}.targets.${node}.nativeDevelopment.workbench is required`);
|
||||
const env = hwlabNativeDevelopmentEnvironment(native);
|
||||
const command = ["bun", "tools/hwlab-cli/bin/hwlab-cli.ts", "workbench", "service", service, action];
|
||||
const result = runCommand(command, spec.workspace, { timeoutMs: 15_000, env: { ...process.env, ...env } });
|
||||
const payload = parsePayload(result.stdout);
|
||||
const publicEndpoint = service === "api"
|
||||
? { ip: native.publicHost, port: native.api.port, url: `http://${native.publicHost}:${native.api.port}` }
|
||||
: service === "web"
|
||||
? { ip: native.publicHost, port: native.web.port, url: `http://${native.publicHost}:${native.web.port}/workbench` }
|
||||
: null;
|
||||
return {
|
||||
ok: result.exitCode === 0 && payload.ok !== false,
|
||||
command: `hwlab nodes native-development workbench ${service} ${action}`,
|
||||
node,
|
||||
lane,
|
||||
service,
|
||||
action,
|
||||
configSource: `${hwlabRuntimeLaneConfigPath()}#lanes.${lane}.targets.${node}.nativeDevelopment.workbench`,
|
||||
publicHostSource: native.publicHostRef,
|
||||
userEndpoint: publicEndpoint,
|
||||
serviceResult: payload,
|
||||
...(result.exitCode === 0 ? {} : { error: { code: "hwlab_native_service_failed", exitCode: result.exitCode, stderr: result.stderr.trim().slice(-2000) } }),
|
||||
};
|
||||
}
|
||||
|
||||
export function hwlabNativeDevelopmentEnvironment(native: HwlabRuntimeNativeDevelopmentSpec["workbench"]): NodeJS.ProcessEnv {
|
||||
return {
|
||||
WORKBENCH_NATIVE_SERVICE_STATE_DIR: native.stateDir,
|
||||
WORKBENCH_API_BIND_HOST: native.api.bindHost,
|
||||
WORKBENCH_API_PROBE_HOST: native.api.probeHost,
|
||||
WORKBENCH_API_PUBLIC_HOST: native.publicHost,
|
||||
WORKBENCH_API_PORT: String(native.api.port),
|
||||
WORKBENCH_WORKER_BIND_HOST: native.worker.bindHost,
|
||||
WORKBENCH_WORKER_PROBE_HOST: native.worker.probeHost,
|
||||
WORKBENCH_WORKER_HEALTH_PORT: String(native.worker.healthPort),
|
||||
WORKBENCH_WEB_BIND_HOST: native.web.bindHost,
|
||||
WORKBENCH_WEB_PROBE_HOST: native.web.probeHost,
|
||||
WORKBENCH_WEB_PUBLIC_HOST: native.publicHost,
|
||||
WORKBENCH_WEB_PORT: String(native.web.port),
|
||||
WORKBENCH_NATIVE_API_URL: `http://${native.api.probeHost}:${native.api.port}`,
|
||||
};
|
||||
}
|
||||
|
||||
function parsePayload(stdout: string): Record<string, unknown> {
|
||||
const parsed = JSON.parse(stdout) as unknown;
|
||||
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) throw new Error("HWLAB Workbench CLI returned invalid JSON");
|
||||
return parsed as Record<string, unknown>;
|
||||
}
|
||||
|
||||
function parseService(value: string | undefined): Service {
|
||||
if (value === "api" || value === "worker" || value === "web") return value;
|
||||
throw new Error("native-development workbench service must be api, worker, or web");
|
||||
}
|
||||
|
||||
function parseAction(value: string | undefined): Action {
|
||||
if (value === "start" || value === "stop" || value === "restart" || value === "status" || value === "logs") return value;
|
||||
throw new Error("native-development workbench action must be start, stop, restart, status, or logs");
|
||||
}
|
||||
|
||||
function requiredOption(args: string[], option: string): string {
|
||||
const index = args.indexOf(option);
|
||||
const value = index < 0 ? undefined : args[index + 1];
|
||||
if (!value || value.startsWith("--")) throw new Error(`${option} requires a value`);
|
||||
return value;
|
||||
}
|
||||
@@ -49,6 +49,7 @@ export function hwlabNodeHelp(scope: "legacy-cicd" | "platform-maintenance" | nu
|
||||
"bun scripts/cli.ts hwlab nodes hwpod-preinstall plan --node <node> --lane <lane> --dry-run",
|
||||
"bun scripts/cli.ts hwlab nodes hwpod-node plan --node G14-WSL --lane v03",
|
||||
"bun scripts/cli.ts hwlab nodes fake-model-provider plan --node D518 --lane v03 --provider fake-echo",
|
||||
"bun scripts/cli.ts hwlab nodes native-development workbench web status --node <node> --lane <lane>",
|
||||
"bun scripts/cli.ts hwlab nodes secret status --node <node> --lane <lane> --name <secret>",
|
||||
"bun scripts/cli.ts hwlab nodes test-accounts status --node <node> --lane <lane>",
|
||||
"bun scripts/cli.ts hwlab nodes observability performance-summary --node <node> --lane <lane>",
|
||||
@@ -62,6 +63,7 @@ export function hwlabNodeHelp(scope: "legacy-cicd" | "platform-maintenance" | nu
|
||||
"hwpod-preinstall": "Render YAML-first HWPOD preinstall configRefs, runtime mount targets, PM MDTODO source, and gateway profile status.",
|
||||
"hwpod-node": "通过 YAML 和 trans 部署、查询 Windows 原生 Python HWPOD 节点。",
|
||||
"fake-model-provider": "Materialize and operate YAML-declared fake Responses model providers for HWLAB/AgentRun sentinel checks.",
|
||||
"native-development": "按 owning YAML 独立管理 L1 API、Worker 与 Web,并为 API/Web 披露公网 IP 和 port。",
|
||||
secret: "Inspect YAML-declared runtime Secrets without printing secret values; writes are documented only by platform-maintenance help.",
|
||||
"test-accounts": "Prepare YAML-declared HWLAB admin/test account API keys with redacted sourceRef/fingerprint output.",
|
||||
observability: "Read runtime metrics and authenticated Web Performance summaries.",
|
||||
|
||||
@@ -4,10 +4,12 @@
|
||||
// SPEC: PJ2026-01060308 cicd-yaml-targets draft-2026-06-25-cicd-yaml-targets.
|
||||
// Responsibility: YAML source-of-truth parsing for HWLAB node/lane Workbench observability.
|
||||
import { readFileSync } from "node:fs";
|
||||
import { isIP } from "node:net";
|
||||
import { rootPath } from "./config";
|
||||
import { HWLAB_NODE_CONTROL_PLANE_CONFIG_PATH } from "./hwlab-node-control-plane-model";
|
||||
import { parseWebProbeConsoleVerificationProfiles, type HwlabRuntimeWebProbeConsoleVerificationProfileSpec } from "./hwlab-node-web-probe-console-profile";
|
||||
import { materializeYamlComposition } from "./yaml-composition";
|
||||
import { resolveConfigRefString } from "./ops/config-refs";
|
||||
|
||||
export type HwlabRuntimeLane = string;
|
||||
|
||||
@@ -660,6 +662,17 @@ export interface HwlabRuntimeSourceWorkspaceHostDependenciesSpec {
|
||||
};
|
||||
}
|
||||
|
||||
export interface HwlabRuntimeNativeDevelopmentSpec {
|
||||
readonly workbench: {
|
||||
readonly publicHostRef: string;
|
||||
readonly publicHost: string;
|
||||
readonly stateDir: string;
|
||||
readonly api: { readonly bindHost: string; readonly probeHost: string; readonly port: number };
|
||||
readonly worker: { readonly bindHost: string; readonly probeHost: string; readonly healthPort: number };
|
||||
readonly web: { readonly bindHost: string; readonly probeHost: string; readonly port: number };
|
||||
};
|
||||
}
|
||||
|
||||
export interface HwlabRuntimePipelineProvenanceSpec {
|
||||
readonly configRef: string;
|
||||
readonly renderer: "hwlab-runtime-lane";
|
||||
@@ -711,6 +724,7 @@ export interface HwlabRuntimeLaneSpec {
|
||||
readonly codeAgentProvider?: HwlabRuntimeCodeAgentProviderSpec;
|
||||
readonly codeAgentRuntime?: HwlabRuntimeCodeAgentRuntimeSpec;
|
||||
readonly sourceWorkspace?: HwlabRuntimeSourceWorkspaceSpec;
|
||||
readonly nativeDevelopment?: HwlabRuntimeNativeDevelopmentSpec;
|
||||
readonly externalPostgres?: HwlabRuntimeExternalPostgresSpec;
|
||||
readonly runtimeStore?: HwlabRuntimeStoreSpec;
|
||||
readonly webProbe?: HwlabRuntimeWebProbeSpec;
|
||||
@@ -777,6 +791,7 @@ interface HwlabLaneConfig {
|
||||
readonly codeAgentProvider?: HwlabRuntimeCodeAgentProviderSpec;
|
||||
readonly codeAgentRuntime?: HwlabRuntimeCodeAgentRuntimeSpec;
|
||||
readonly sourceWorkspace?: HwlabRuntimeSourceWorkspaceSpec;
|
||||
readonly nativeDevelopment?: HwlabRuntimeNativeDevelopmentSpec;
|
||||
readonly externalPostgres?: HwlabRuntimeExternalPostgresSpec;
|
||||
readonly runtimeStore?: HwlabRuntimeStoreSpec;
|
||||
readonly webProbe?: HwlabRuntimeWebProbeSpec;
|
||||
@@ -1036,6 +1051,7 @@ function laneConfig(id: HwlabRuntimeLane, raw: Record<string, unknown>): HwlabLa
|
||||
codeAgentProvider: codeAgentProviderConfig(raw.codeAgentProvider, `lanes.${id}.codeAgentProvider`),
|
||||
codeAgentRuntime: codeAgentRuntimeConfig(raw.codeAgentRuntime, `lanes.${id}.codeAgentRuntime`),
|
||||
sourceWorkspace: sourceWorkspaceConfig(raw.sourceWorkspace, `lanes.${id}.sourceWorkspace`),
|
||||
nativeDevelopment: nativeDevelopmentConfig(raw.nativeDevelopment, `lanes.${id}.nativeDevelopment`),
|
||||
externalPostgres: externalPostgresConfig(raw.externalPostgres, `lanes.${id}.externalPostgres`),
|
||||
runtimeStore: runtimeStoreConfig(raw.runtimeStore, `lanes.${id}.runtimeStore`),
|
||||
webProbe: webProbeConfig(raw.webProbe, `lanes.${id}.webProbe`),
|
||||
@@ -1074,6 +1090,7 @@ function laneTargetConfig(
|
||||
sourceAuthority: mergeOptionalRecord(baseRaw.sourceAuthority, targetRaw.sourceAuthority),
|
||||
sourceSnapshot: mergeOptionalRecord(baseRaw.sourceSnapshot, targetRaw.sourceSnapshot),
|
||||
sourceWorkspace: mergeOptionalRecord(baseRaw.sourceWorkspace, targetRaw.sourceWorkspace),
|
||||
nativeDevelopment: mergeOptionalRecord(baseRaw.nativeDevelopment, targetRaw.nativeDevelopment),
|
||||
externalPostgres: mergeOptionalRecord(baseRaw.externalPostgres, targetRaw.externalPostgres),
|
||||
runtimeStore: mergeOptionalRecord(baseRaw.runtimeStore, targetRaw.runtimeStore),
|
||||
webProbe: mergeOptionalRecord(baseRaw.webProbe, targetRaw.webProbe),
|
||||
@@ -1394,6 +1411,28 @@ function sourceWorkspaceConfig(value: unknown, path: string): HwlabRuntimeSource
|
||||
};
|
||||
}
|
||||
|
||||
function nativeDevelopmentConfig(value: unknown, path: string): HwlabRuntimeNativeDevelopmentSpec | undefined {
|
||||
if (value === undefined) return undefined;
|
||||
const raw = asRecord(value, path);
|
||||
const workbench = asRecord(raw.workbench, `${path}.workbench`);
|
||||
const publicHostRef = stringField(workbench, "publicHostRef", `${path}.workbench`);
|
||||
const publicHost = resolveConfigRefString(publicHostRef, `${path}.workbench.publicHostRef`);
|
||||
if (isIP(publicHost) === 0) throw new Error(`${path}.workbench.publicHostRef must resolve to a public IP address`);
|
||||
const api = asRecord(workbench.api, `${path}.workbench.api`);
|
||||
const worker = asRecord(workbench.worker, `${path}.workbench.worker`);
|
||||
const web = asRecord(workbench.web, `${path}.workbench.web`);
|
||||
return {
|
||||
workbench: {
|
||||
publicHostRef,
|
||||
publicHost,
|
||||
stateDir: relativeWorkspacePathField(stringField(workbench, "stateDir", `${path}.workbench`), `${path}.workbench.stateDir`),
|
||||
api: { bindHost: stringField(api, "bindHost", `${path}.workbench.api`), probeHost: stringField(api, "probeHost", `${path}.workbench.api`), port: numberField(api, "port", `${path}.workbench.api`) },
|
||||
worker: { bindHost: stringField(worker, "bindHost", `${path}.workbench.worker`), probeHost: stringField(worker, "probeHost", `${path}.workbench.worker`), healthPort: numberField(worker, "healthPort", `${path}.workbench.worker`) },
|
||||
web: { bindHost: stringField(web, "bindHost", `${path}.workbench.web`), probeHost: stringField(web, "probeHost", `${path}.workbench.web`), port: numberField(web, "port", `${path}.workbench.web`) },
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function sourceWorkspaceHostDependenciesConfig(value: unknown, path: string): HwlabRuntimeSourceWorkspaceHostDependenciesSpec | undefined {
|
||||
if (value === undefined) return undefined;
|
||||
const raw = asRecord(value, path);
|
||||
@@ -2465,6 +2504,7 @@ function buildRuntimeLaneSpec(config: HwlabLaneConfig): HwlabRuntimeLaneSpec {
|
||||
...(config.codeAgentProvider === undefined ? {} : { codeAgentProvider: config.codeAgentProvider }),
|
||||
...(config.codeAgentRuntime === undefined ? {} : { codeAgentRuntime: config.codeAgentRuntime }),
|
||||
...(config.sourceWorkspace === undefined ? {} : { sourceWorkspace: config.sourceWorkspace }),
|
||||
...(config.nativeDevelopment === undefined ? {} : { nativeDevelopment: config.nativeDevelopment }),
|
||||
...(config.externalPostgres === undefined ? {} : { externalPostgres: config.externalPostgres }),
|
||||
...(config.runtimeStore === undefined ? {} : { runtimeStore: config.runtimeStore }),
|
||||
...(config.webProbe === undefined ? {} : { webProbe: config.webProbe }),
|
||||
|
||||
@@ -47,6 +47,7 @@ import { legacyHwlabNodeWebProbeUnsupported } from "../web-probe";
|
||||
import { startNodeDelegatedJob } from "./web-probe";
|
||||
import { assertKnownOptions } from "./web-probe-observe";
|
||||
import { hwlabNodeDeliveryMutationDecision } from "./delivery-authority";
|
||||
import { runHwlabNativeDevelopmentCommand } from "../hwlab-native-development";
|
||||
|
||||
export { hwlabNodeHelp, hwlabNodeWebProbeHelp, hwlabNodeObservabilityHelp } from "../hwlab-node-help";
|
||||
|
||||
@@ -569,6 +570,9 @@ export async function runHwlabNodeCommand(_config: Config, args: string[]): Prom
|
||||
if (domain === "fake-model-provider") {
|
||||
return runHwlabFakeModelProviderCommand(_config, args.slice(1));
|
||||
}
|
||||
if (domain === "native-development") {
|
||||
return runHwlabNativeDevelopmentCommand(args.slice(1));
|
||||
}
|
||||
if (domain === "web-probe") {
|
||||
return legacyHwlabNodeWebProbeUnsupported(args.slice(1));
|
||||
}
|
||||
@@ -584,7 +588,7 @@ export async function runHwlabNodeCommand(_config: Config, args: string[]): Prom
|
||||
return runNodeDelegatedDomain(_config, domain, args.slice(1));
|
||||
}
|
||||
if (domain !== "secret") {
|
||||
return { ok: false, command: `hwlab nodes ${domain ?? ""}`.trim(), message: "supported commands: hwlab nodes control-plane, hwlab nodes git-mirror, hwlab nodes hwpod-preinstall, hwlab nodes hwpod-node, hwlab nodes fake-model-provider, hwlab nodes observability, hwlab nodes secret, hwlab nodes test-accounts. web-probe moved to top-level: bun scripts/cli.ts web-probe --help" };
|
||||
return { ok: false, command: `hwlab nodes ${domain ?? ""}`.trim(), message: "supported commands: hwlab nodes control-plane, hwlab nodes git-mirror, hwlab nodes native-development, hwlab nodes hwpod-preinstall, hwlab nodes hwpod-node, hwlab nodes fake-model-provider, hwlab nodes observability, hwlab nodes secret, hwlab nodes test-accounts. web-probe moved to top-level: bun scripts/cli.ts web-probe --help" };
|
||||
}
|
||||
const options = parseSecretOptions(args.slice(1));
|
||||
return runNodeSecret(options);
|
||||
|
||||
Reference in New Issue
Block a user