Merge pull request #2494 from pikasTech/fix/l1-agentrun-v02-smoke
修复 L1 Workbench AgentRun 直连与端口退让
This commit is contained in:
@@ -75,7 +75,15 @@ description: UniDesk Web 开发与受控浏览器验证技能。用户提到 Web
|
||||
- 通过 native auth/access profile 只授权当前调试微服务的 `navId`;
|
||||
- 左侧根导航必须存在,但只能显示当前微服务的一个页面入口;
|
||||
- 只代理当前微服务 API 路径,禁止为了渲染产品壳层启动其他 HWLAB 后端;
|
||||
- 当前微服务前端、API 和 Worker 使用独立端口与生命周期,不得抢占或停止其他 L1 会话;
|
||||
- 当前微服务前端、API 和 Worker 使用 YAML-first 固定端口与独立生命周期;
|
||||
- YAML 声明的 L1 端口被占用时:
|
||||
- 同一服务的旧 L1 进程只通过项目 CLI 停止或重启;
|
||||
- 其他服务不得停止、复用或接管;
|
||||
- 先用有界监听状态确认一个空闲端口;
|
||||
- 再修改本 L1 服务的 owning YAML 端口并通过 parser/CLI 读取新值;
|
||||
- 禁止用命令行覆盖、临时环境变量或隐藏 fallback 绕过 YAML;
|
||||
- 端口退让后继续启动,并从 YAML 新端口执行 `web-probe native-readiness`;
|
||||
- L1 进程的启动、停止、重启、状态和日志只通过项目 CLI 管理;
|
||||
- Vite 必须绑定可访问的 native 端口并保持 HMR,浏览器端 HMR 地址不得写成 `0.0.0.0`。
|
||||
- L1 公网调试入口交给用户前必须完成浏览器 readiness 验收:
|
||||
- 使用 `bun scripts/cli.ts web-probe native-readiness --node <node> --lane <lane> --profile <profile>` 一次完成验收;
|
||||
|
||||
@@ -115,6 +115,22 @@ lanes:
|
||||
workbench:
|
||||
publicHostRef: config/unidesk-host-k8s.yaml#runtime.publicHost
|
||||
stateDir: .state/workbench-native/services
|
||||
mode: agentrun-native
|
||||
temporal:
|
||||
address: 10.43.145.243:7233
|
||||
namespace: unidesk
|
||||
taskQueue: hwlab-v03-workbench-native-l1
|
||||
activity:
|
||||
startToCloseTimeoutMs: 900000
|
||||
retryInitialIntervalMs: 1000
|
||||
retryMaximumIntervalMs: 10000
|
||||
retryMaximumAttempts: 1
|
||||
agentRun:
|
||||
managerUrl: http://10.43.171.18:8080
|
||||
runtimeApiUrl: http://hwlab-cloud-api.hwlab-v03.svc.cluster.local:6667
|
||||
runtimeWebUrl: http://hwlab-cloud-web.hwlab-v03.svc.cluster.local:8080
|
||||
terminalTimeoutMs: 600000
|
||||
pollIntervalMs: 1000
|
||||
api:
|
||||
bindHost: 0.0.0.0
|
||||
probeHost: 127.0.0.1
|
||||
@@ -126,7 +142,7 @@ lanes:
|
||||
web:
|
||||
bindHost: 0.0.0.0
|
||||
probeHost: 127.0.0.1
|
||||
port: 5173
|
||||
port: 5174
|
||||
runtimeConfig:
|
||||
displayTime:
|
||||
timeZone: Asia/Shanghai
|
||||
|
||||
@@ -180,6 +180,14 @@ AgentRun terminal `failed`、`blocked` 或 `canceled` 也是最终结果,不
|
||||
|
||||
Workbench 浏览器回归需求以 UniDesk OA [PJ2026-010401 Web工作台](../../project-management/PJ2026-01/specs/PJ2026-010401-web-workbench.md) 为权威;具体 Web 开发、受控 fake-server、fixture 采集脱敏、移动端断言、截图 artifact 和线上 web-probe 闭环统一见 `$unidesk-webdev`。HWLAB repo 只保留入口边界:专项代码位于 `web/hwlab-cloud-web`,命令必须在 issue/CLI 选中的目标 node/lane workspace 或独立 worktree 上运行,不得在 master server 本地跑浏览器或仓库级前端 check。
|
||||
|
||||
- L1 Native 端口冲突处理:
|
||||
- 端口被同一 L1 服务的旧进程占用时,只能通过项目 CLI 停止或重启该服务;
|
||||
- 端口被其他服务占用时,禁止停止、接管或复用其他服务;
|
||||
- 必须先确认空闲端口,再修改 `config/hwlab-node-lanes.yaml#lanes.<lane>.targets.<node>.nativeDevelopment.workbench` 中本服务的端口;
|
||||
- API、Worker、Web 的启动、状态和公网入口继续由同一 YAML 与 `hwlab nodes native-development workbench` 解析;
|
||||
- 禁止用命令行参数、临时环境变量或代码 fallback 形成第二端口真相;
|
||||
- 端口退让后必须从 YAML 新端口执行 `web-probe native-readiness`,确认公网页面、DOM、交互和浏览器错误均通过后再向用户披露入口。
|
||||
|
||||
MDTODO/Project Management Web 的重写或布局类 issue 以 UniDesk OA 对应 SPEC 为权威;关闭时必须用选中 node/lane 的 public origin 和 `$unidesk-webdev` 的 `observe command` 证据证明默认任务正文、Rxx 树、Source/File 选择、报告侧栏和报告全屏状态。`observe` 同时采集 control/observer 页面时,显式 command result、control URL 和截图是用户动作证据;被动 observer 周期刷新或 stop 后根路由空态只能作为对照,不能覆盖用户入口截图和 command JSON。
|
||||
|
||||
Workbench session lifecycle、空 session 后台回收、archived/deleted deep link 和 GET 纯读投影的判定口径统一见 `$unidesk-webdev`。指挥侧关闭这类 issue 时只记录选中 node/lane、受控配置来源、后台 mutation/GC 证据、list/detail/messages/deep link 摘要和截图 SHA;TTL、interval、batch 等可调数值以选中 lane 的受控配置为准,不在本参考维护第二份数值真相。
|
||||
|
||||
@@ -4,10 +4,18 @@ import { hwlabNativeDevelopmentEnvironment, hwlabNativeDevelopmentHelp } from ".
|
||||
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;
|
||||
const spec = hwlabRuntimeLaneSpecForNode("v03", "NC01");
|
||||
const native = spec.nativeDevelopment?.workbench;
|
||||
expect(native).toBeDefined();
|
||||
const env = hwlabNativeDevelopmentEnvironment(native!);
|
||||
const env = hwlabNativeDevelopmentEnvironment(native!, spec, "0123456789abcdef0123456789abcdef01234567");
|
||||
expect(env.WORKBENCH_NATIVE_SERVICE_STATE_DIR).toBe(native!.stateDir);
|
||||
expect(env.WORKBENCH_MODE).toBe("agentrun-native");
|
||||
expect(env.WORKBENCH_CLOUD_API_URL).toBeUndefined();
|
||||
expect(env.WORKBENCH_CLOUD_API_AUTHORIZATION).toBeUndefined();
|
||||
expect(env.AGENTRUN_MGR_URL).toBe(native!.agentRun.managerUrl);
|
||||
expect(env.AGENTRUN_API_KEY).toMatch(/^hwl_live_/u);
|
||||
expect(env.HWLAB_CODE_AGENT_DEFAULT_PROVIDER_PROFILE).toBe("gpt.pika");
|
||||
expect(env.HWLAB_CODE_AGENT_AGENTRUN_SOURCE_COMMIT).toBe("0123456789abcdef0123456789abcdef01234567");
|
||||
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);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { runCommand } from "./command";
|
||||
import { hwlabRuntimeLaneConfigPath, hwlabRuntimeLaneSpecForNode, isHwlabRuntimeLane, type HwlabRuntimeNativeDevelopmentSpec } from "./hwlab-node-lanes";
|
||||
import { readEnvSourceFile, requiredEnvValue } from "./secrets";
|
||||
import { hwlabRuntimeLaneConfigPath, hwlabRuntimeLaneSpecForNode, isHwlabRuntimeLane, type HwlabRuntimeLaneSpec, type HwlabRuntimeNativeDevelopmentSpec } from "./hwlab-node-lanes";
|
||||
|
||||
type Service = "api" | "worker" | "web";
|
||||
type Action = "start" | "stop" | "restart" | "status" | "logs";
|
||||
@@ -28,7 +29,11 @@ export function runHwlabNativeDevelopmentCommand(args: string[]): Record<string,
|
||||
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 sourceCommitResult = runCommand(["git", "rev-parse", "HEAD"], spec.workspace, { timeoutMs: 5_000 });
|
||||
if (sourceCommitResult.exitCode !== 0 || !/^[0-9a-f]{40}$/iu.test(sourceCommitResult.stdout.trim())) {
|
||||
throw new Error(`cannot resolve current HWLAB source commit from ${spec.workspace}`);
|
||||
}
|
||||
const env = hwlabNativeDevelopmentEnvironment(native, spec, sourceCommitResult.stdout.trim());
|
||||
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);
|
||||
@@ -52,8 +57,41 @@ export function runHwlabNativeDevelopmentCommand(args: string[]): Record<string,
|
||||
};
|
||||
}
|
||||
|
||||
export function hwlabNativeDevelopmentEnvironment(native: HwlabRuntimeNativeDevelopmentSpec["workbench"]): NodeJS.ProcessEnv {
|
||||
export function hwlabNativeDevelopmentEnvironment(native: HwlabRuntimeNativeDevelopmentSpec["workbench"], spec: HwlabRuntimeLaneSpec, sourceCommit: string): NodeJS.ProcessEnv {
|
||||
const codeAgent = spec.codeAgentRuntime;
|
||||
if (codeAgent === undefined) throw new Error(`${hwlabRuntimeLaneConfigPath()}#lanes.${spec.lane}.targets.${spec.nodeId}.codeAgentRuntime is required`);
|
||||
if (!/^[0-9a-f]{40}$/iu.test(sourceCommit)) throw new Error("sourceCommit must be the current HWLAB workspace 40-hex git SHA");
|
||||
const authorizationSource = readEnvSourceFile({
|
||||
root: "/root/.unidesk/.state/secrets",
|
||||
sourceRef: codeAgent.apiKeySourceRef,
|
||||
});
|
||||
const agentRunApiKey = requiredEnvValue(authorizationSource.values, codeAgent.apiKeySourceKey, codeAgent.apiKeySourceRef);
|
||||
return {
|
||||
WORKBENCH_MODE: native.mode,
|
||||
WORKBENCH_TEMPORAL_ADDRESS: native.temporal.address,
|
||||
WORKBENCH_TEMPORAL_NAMESPACE: native.temporal.namespace,
|
||||
WORKBENCH_TEMPORAL_TASK_QUEUE: native.temporal.taskQueue,
|
||||
WORKBENCH_ACTIVITY_START_TO_CLOSE_TIMEOUT_MS: String(native.temporal.activity.startToCloseTimeoutMs),
|
||||
WORKBENCH_ACTIVITY_RETRY_INITIAL_INTERVAL_MS: String(native.temporal.activity.retryInitialIntervalMs),
|
||||
WORKBENCH_ACTIVITY_RETRY_MAXIMUM_INTERVAL_MS: String(native.temporal.activity.retryMaximumIntervalMs),
|
||||
WORKBENCH_ACTIVITY_RETRY_MAXIMUM_ATTEMPTS: String(native.temporal.activity.retryMaximumAttempts),
|
||||
WORKBENCH_AGENTRUN_TERMINAL_TIMEOUT_MS: String(native.agentRun.terminalTimeoutMs),
|
||||
WORKBENCH_AGENTRUN_POLL_INTERVAL_MS: String(native.agentRun.pollIntervalMs),
|
||||
AGENTRUN_MGR_URL: native.agentRun.managerUrl,
|
||||
AGENTRUN_API_KEY: agentRunApiKey,
|
||||
HWLAB_CODE_AGENT_ADAPTER: codeAgent.adapter,
|
||||
HWLAB_CODE_AGENT_AGENTRUN_ALLOW_PRIVATE_URL: "1",
|
||||
HWLAB_CODE_AGENT_AGENTRUN_PROVIDER_ID: spec.nodeId,
|
||||
HWLAB_CODE_AGENT_DEFAULT_PROVIDER_PROFILE: codeAgent.defaultProviderProfile,
|
||||
HWLAB_CODE_AGENT_AGENTRUN_RUNNER_NAMESPACE: codeAgent.runnerNamespace,
|
||||
HWLAB_CODE_AGENT_AGENTRUN_REPO_URL: spec.gitReadUrl,
|
||||
HWLAB_CODE_AGENT_AGENTRUN_SOURCE_COMMIT: sourceCommit,
|
||||
HWLAB_RUNTIME_API_URL: native.agentRun.runtimeApiUrl,
|
||||
HWLAB_RUNTIME_WEB_URL: native.agentRun.runtimeWebUrl,
|
||||
HWLAB_RUNTIME_NAMESPACE: spec.runtimeNamespace,
|
||||
HWLAB_RUNTIME_LANE: spec.lane,
|
||||
HWLAB_RUNTIME_ENDPOINT_LOCKED: "1",
|
||||
HWLAB_CODE_AGENT_ASSEMBLED_RUNTIME: "1",
|
||||
WORKBENCH_NATIVE_SERVICE_STATE_DIR: native.stateDir,
|
||||
WORKBENCH_API_BIND_HOST: native.api.bindHost,
|
||||
WORKBENCH_API_PROBE_HOST: native.api.probeHost,
|
||||
|
||||
@@ -670,6 +670,25 @@ export interface HwlabRuntimeNativeDevelopmentSpec {
|
||||
readonly publicHostRef: string;
|
||||
readonly publicHost: string;
|
||||
readonly stateDir: string;
|
||||
readonly mode: "agentrun-native";
|
||||
readonly temporal: {
|
||||
readonly address: string;
|
||||
readonly namespace: string;
|
||||
readonly taskQueue: string;
|
||||
readonly activity: {
|
||||
readonly startToCloseTimeoutMs: number;
|
||||
readonly retryInitialIntervalMs: number;
|
||||
readonly retryMaximumIntervalMs: number;
|
||||
readonly retryMaximumAttempts: number;
|
||||
};
|
||||
};
|
||||
readonly agentRun: {
|
||||
readonly managerUrl: string;
|
||||
readonly runtimeApiUrl: string;
|
||||
readonly runtimeWebUrl: string;
|
||||
readonly terminalTimeoutMs: number;
|
||||
readonly pollIntervalMs: number;
|
||||
};
|
||||
readonly api: { readonly bindHost: string; readonly probeHost: string; readonly port: number };
|
||||
readonly worker: { readonly bindHost: string; readonly probeHost: string; readonly healthPort: number };
|
||||
readonly web: {
|
||||
@@ -1436,6 +1455,10 @@ function nativeDevelopmentConfig(value: unknown, path: string): HwlabRuntimeNati
|
||||
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 mode = enumStringField(workbench, "mode", `${path}.workbench`, ["agentrun-native"]);
|
||||
const temporal = asRecord(workbench.temporal, `${path}.workbench.temporal`);
|
||||
const temporalActivity = asRecord(temporal.activity, `${path}.workbench.temporal.activity`);
|
||||
const agentRun = asRecord(workbench.agentRun, `${path}.workbench.agentRun`);
|
||||
const api = asRecord(workbench.api, `${path}.workbench.api`);
|
||||
const worker = asRecord(workbench.worker, `${path}.workbench.worker`);
|
||||
const web = asRecord(workbench.web, `${path}.workbench.web`);
|
||||
@@ -1451,6 +1474,25 @@ function nativeDevelopmentConfig(value: unknown, path: string): HwlabRuntimeNati
|
||||
publicHostRef,
|
||||
publicHost,
|
||||
stateDir: relativeWorkspacePathField(stringField(workbench, "stateDir", `${path}.workbench`), `${path}.workbench.stateDir`),
|
||||
mode,
|
||||
temporal: {
|
||||
address: stringField(temporal, "address", `${path}.workbench.temporal`),
|
||||
namespace: stringField(temporal, "namespace", `${path}.workbench.temporal`),
|
||||
taskQueue: stringField(temporal, "taskQueue", `${path}.workbench.temporal`),
|
||||
activity: {
|
||||
startToCloseTimeoutMs: numberField(temporalActivity, "startToCloseTimeoutMs", `${path}.workbench.temporal.activity`),
|
||||
retryInitialIntervalMs: numberField(temporalActivity, "retryInitialIntervalMs", `${path}.workbench.temporal.activity`),
|
||||
retryMaximumIntervalMs: numberField(temporalActivity, "retryMaximumIntervalMs", `${path}.workbench.temporal.activity`),
|
||||
retryMaximumAttempts: numberField(temporalActivity, "retryMaximumAttempts", `${path}.workbench.temporal.activity`),
|
||||
},
|
||||
},
|
||||
agentRun: {
|
||||
managerUrl: httpUrlField(agentRun, "managerUrl", `${path}.workbench.agentRun`),
|
||||
runtimeApiUrl: httpUrlField(agentRun, "runtimeApiUrl", `${path}.workbench.agentRun`),
|
||||
runtimeWebUrl: httpUrlField(agentRun, "runtimeWebUrl", `${path}.workbench.agentRun`),
|
||||
terminalTimeoutMs: numberField(agentRun, "terminalTimeoutMs", `${path}.workbench.agentRun`),
|
||||
pollIntervalMs: numberField(agentRun, "pollIntervalMs", `${path}.workbench.agentRun`),
|
||||
},
|
||||
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: {
|
||||
@@ -1488,6 +1530,17 @@ function nativeDevelopmentConfig(value: unknown, path: string): HwlabRuntimeNati
|
||||
};
|
||||
}
|
||||
|
||||
function httpUrlField(raw: Record<string, unknown>, key: string, path: string): string {
|
||||
const value = stringField(raw, key, path);
|
||||
try {
|
||||
const parsed = new URL(value);
|
||||
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") throw new Error("unsupported protocol");
|
||||
} catch {
|
||||
throw new Error(`${path}.${key} must be an http(s) URL`);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function sourceWorkspaceHostDependenciesConfig(value: unknown, path: string): HwlabRuntimeSourceWorkspaceHostDependenciesSpec | undefined {
|
||||
if (value === undefined) return undefined;
|
||||
const raw = asRecord(value, path);
|
||||
|
||||
Reference in New Issue
Block a user