diff --git a/cmd/hwlab-device-pod/main.test.ts b/cmd/hwlab-device-pod/main.test.ts index 228c70b4..262e06c2 100644 --- a/cmd/hwlab-device-pod/main.test.ts +++ b/cmd/hwlab-device-pod/main.test.ts @@ -356,6 +356,11 @@ test("device-pod executor maps absorbed G14 device-pod operations to host CLI ar }; try { + await submitAndWait(service.port, "workspace.bootsharp", { + path: ".", + depth: 2, + limit: 120 + }, "job_map_bootsharp", baseBody); await submitAndWait(service.port, "workspace.put", { path: "User/new.c", contentB64: Buffer.from("int x;\n").toString("base64"), @@ -375,12 +380,13 @@ test("device-pod executor maps absorbed G14 device-pod operations to host CLI ar expectResultField: ["pin", "value"] }, "job_map_jsonrpc", baseBody); - assert.match(commands[0], / workspace put User\/new\.c --content-b64 /u); - assert.match(commands[0], / --create-dirs$/u); - assert.match(commands[1], / workspace keil add-source User\/new\.c --base projects\/app --group User$/u); - assert.match(commands[2], / io-probe uart\/1 jsonrpc gpio\.read /u); - assert.match(commands[2], / --params "\{\\"pin\\":\\"PB5\\"\}" /u); - assert.match(commands[2], / --require-jsonrpc-result --expect-result-field pin --expect-result-field value$/u); + assert.match(commands[0], / workspace bootsharp \. --depth 2 --limit 120$/u); + assert.match(commands[1], / workspace put User\/new\.c --content-b64 /u); + assert.match(commands[1], / --create-dirs$/u); + assert.match(commands[2], / workspace keil add-source User\/new\.c --base projects\/app --group User$/u); + assert.match(commands[3], / io-probe uart\/1 jsonrpc gpio\.read /u); + assert.match(commands[3], / --params "\{\\"pin\\":\\"PB5\\"\}" /u); + assert.match(commands[3], / --require-jsonrpc-result --expect-result-field pin --expect-result-field value$/u); } finally { await service.stop(); await new Promise((resolve, reject) => cloudApi.close((error) => (error ? reject(error) : resolve()))); diff --git a/cmd/hwlab-device-pod/main.ts b/cmd/hwlab-device-pod/main.ts index 9d4488c6..5571c27a 100644 --- a/cmd/hwlab-device-pod/main.ts +++ b/cmd/hwlab-device-pod/main.ts @@ -369,7 +369,7 @@ function gatewayAdapterBlocker(summary) { } function gatewayDispatchFailedBlocker(summary) { - return { code: "gateway_dispatch_failed", layer: "device-pod", retryable: true, summary, userMessage: "Device Pod gateway/device-host-cli dispatch failed." }; + return { code: "gateway_dispatch_failed", layer: "device-pod", retryable: true, summary: String(summary || "gateway/device-host-cli dispatch failed"), userMessage: "Device Pod gateway/device-host-cli dispatch failed." }; } function gatewayDispatchPreflightBlocker(route, command) { @@ -392,9 +392,11 @@ function jobFromGatewayDispatch(job, payload, httpStatus) { } const result = normalizeObject(payload.result); const dispatch = normalizeObject(result.dispatch); + const dispatchText = gatewayDispatchText(result, dispatch); const blocker = payload.blocker ?? result.blocker ?? (payload.error ? gatewayDispatchFailedBlocker(payload.error.message ?? "gateway dispatch failed") : null); const completed = !blocker && httpStatus < 400 && (result.status === "completed" || result.status === "succeeded" || dispatch.dispatchStatus === "succeeded" || dispatch.dispatchStatus === "completed"); const status = completed ? "completed" : blocker?.code === "gateway_dispatch_unavailable" ? "blocked" : "failed"; + const failureSummary = blocker?.summary || dispatch.message || dispatch.stderr || dispatchText || result.status || "gateway/device-host-cli dispatch failed"; const now = new Date().toISOString(); return { ...job, @@ -402,15 +404,15 @@ function jobFromGatewayDispatch(job, payload, httpStatus) { updatedAt: now, completedAt: now, output: boundedOutput({ - text: gatewayDispatchText(result, dispatch), + text: dispatchText, httpStatus, dispatch, gateway: result.gateway ?? null, auditId: result.auditId ?? null, evidenceId: result.evidenceId ?? null, - summary: completed ? "gateway/device-host-cli dispatch completed" : blocker?.summary ?? "gateway/device-host-cli dispatch failed" + summary: completed ? "gateway/device-host-cli dispatch completed" : failureSummary }), - blocker: completed ? null : blocker ?? gatewayDispatchFailedBlocker(dispatch.message ?? dispatch.stderr ?? result.status ?? "gateway dispatch failed") + blocker: completed ? null : (blocker?.summary ? blocker : gatewayDispatchFailedBlocker(failureSummary)) }; } @@ -418,7 +420,8 @@ function gatewayDispatchText(result, dispatch) { if (typeof dispatch.stdout === "string" && dispatch.stdout) return dispatch.stdout; if (typeof dispatch.stderr === "string" && dispatch.stderr) return dispatch.stderr; if (typeof result.text === "string") return result.text; - return JSON.stringify(result); + if (result && Object.keys(result).length > 0) return JSON.stringify(result); + return ""; } function deviceHostCommand(profile, job) { @@ -468,6 +471,7 @@ function shellQuote(value) { } function deviceHostArgs(intent, args = {}) { + if (intent === "workspace.bootsharp") return ["workspace", "bootsharp", textOr(args.path, "."), ...hostOptionArgs(args, ["depth", "limit", "agentsLimit"])]; if (intent === "workspace.ls") return ["workspace", "ls", textOr(args.path, ".")]; if (intent === "workspace.cat") { const path = textOr(args.path, ""); @@ -518,8 +522,8 @@ function deviceHostArgs(intent, args = {}) { "workspace", "build", textOr(args.action, "start"), + ...jobIdArgs(args), ...hostOptionArgs(args, ["target", "timeoutMs", "clean", "dryRun"]), - ...jobIdArgs(args) ]; } if (intent === "debug.status") return ["debug-probe", "status"]; @@ -529,6 +533,7 @@ function deviceHostArgs(intent, args = {}) { "debug-probe", "download", textOr(args.action, "start"), + ...jobIdArgs(args), ...hostOptionArgs(args, [ "target", "timeoutMs", @@ -537,8 +542,7 @@ function deviceHostArgs(intent, args = {}) { "durationMs", "port", "baudRate" - ]), - ...jobIdArgs(args) + ]) ]; } if (intent === "debug.reset") return ["debug-probe", "reset"]; diff --git a/docs/reference/spec-device-pod.md b/docs/reference/spec-device-pod.md index 3e9b82f7..d09fd357 100644 --- a/docs/reference/spec-device-pod.md +++ b/docs/reference/spec-device-pod.md @@ -190,6 +190,7 @@ DELETE /v1/admin/device-pod-grants/{devicePodId}/{userId} 第一阶段 intent 集合: - `workspace.ls` +- `workspace.bootsharp` - `workspace.cat` - `workspace.rg` - `workspace.apply-patch` @@ -263,6 +264,7 @@ manages: many devicePodId - `profile list/show` 调用 cloud-api `/v1/device-pods` 和 `/status`,只显示服务端脱敏 profile 摘要和 `profileHash`。 - `setup first-admin` 和 `admin device-pod upsert/grant` 只作为 cloud-api REST wrapper,用于首次空库 seed 或 admin profile/grant 管理;它们接受显式 `--profile-json` 或 `--device-pod-json`,不得读取本地 `.device-pod/*.json` 作为权威 profile。 - `devicePodId:workspace|debug-probe|io-probe...` selector 转换为 `POST /v1/device-pods/{devicePodId}/jobs` 或 job status/output/cancel REST,不直接调用 `/v1/rpc/hardware.invoke.shell`。 +- `bootsharp --pod-id ` 和 `:workspace:/ bootsharp` 都转换为 `workspace.bootsharp` job,用于返回 workspace tree、AGENTS.md 摘要和当前路径提示;该入口是上下文恢复和 DS 派单的首个探测动作,不读取本地 profile。 - workspace 写操作覆盖 `apply-patch`、`put`、`rm`、`rmdir`、`build` 和 `keil add-source/remove-source` 等 Keil 工程维护动作。 - 源码局部编辑优先使用 `apply-patch`;`put` 只用于明确的整文件写入或新文件创建。 - UART 业务覆盖 `read`、`write`、`read-after-launch-flash` 和 `jsonrpc`;JSON-RPC 请求必须由 device-host-cli 校验 response id,除非显式传入允许 id mismatch 的业务参数。 @@ -274,6 +276,8 @@ manages: many devicePodId D601 Windows F103 gateway 的稳定命名使用 `gws_D601_F103`。不要因为当前 `devicePodId` 或样例设备名是 `device-pod-71-freq` / `71-FREQ`,把 gateway session 改成旧的 `gws_d601_win_71_freq`;profile route 和 Windows gateway 运行脚本必须使用同一组 F103 命名。 +当前正式接入的 F103 v2 逻辑设备名为 `D601-F103-V2`。它的 Windows workspace 固定为 `F:\Work\D601-HWLAB`,Keil 工程为 `projects/01_baseline/Projects/MDK-ARM/atk_f103.uvprojx`,Keil target 为 `USART`,Keil 可执行文件为 `C:\Keil_v5\UV4\UV4.exe`,UART 为 `COM9`/`115200`。v0.2 服务端 profile 必须使用 `devicePodId=D601-F103-V2`,不得继续把该 workspace 暴露成 `device-pod-71-freq`。 + Windows 侧固定入口: ```text @@ -298,15 +302,15 @@ set "HWLAB_GATEWAY_CMD_TIMEOUT_MS=120000" set "HWLAB_GATEWAY_PORT=7001" ``` -v0.2 `device-pod-71-freq` 的服务端 profile route 必须和 gateway 注册保持一致: +v0.2 `D601-F103-V2` 的服务端 profile route 必须和 gateway 注册保持一致: ```json { "gatewaySessionId": "gws_D601_F103", "resourceId": "res_windows_host", "capabilityId": "cap_windows_cmd_exec", - "hostWorkspaceRoot": "F:\\Work\\ConStart", - "hostCli": "node C:\\Users\\liang\\AppData\\Local\\Temp\\hwlab-v02-device-host-cli\\device-host-cli.mjs" + "hostWorkspaceRoot": "F:\\Work\\D601-HWLAB", + "hostCli": "node tools\\device-host-cli.mjs" } ``` @@ -345,6 +349,40 @@ bun tools/device-pod-cli.ts job output --pod-id device-pod-71-freq "$JOB" \ --cookie "$COOKIE" ``` +`D601-F103-V2` 的验收命令使用同一条 v0.2 CLI 链路,只替换 `devicePodId` 并从 `bootsharp` 开始: + +```bash +bun tools/hwlab-cli/bin/hwlab-cli.ts client device-pods status D601-F103-V2 \ + --base-url http://74.48.78.17:19666 \ + --full + +COOKIE=$(node -pe 'require("./.state/hwlab-cli/session.json").cookie') +bun tools/device-pod-cli.ts bootsharp --pod-id D601-F103-V2 \ + --api-base-url http://74.48.78.17:19667 \ + --cookie "$COOKIE" + +LEASE=$(bun tools/device-pod-cli.ts lease acquire --pod-id D601-F103-V2 \ + --api-base-url http://74.48.78.17:19667 \ + --cookie "$COOKIE" \ + --reason "D601-F103-V2 build/download validation" | node -e 'let s="";process.stdin.on("data",d=>s+=d).on("end",()=>console.log(JSON.parse(s).body.leaseToken))') + +bun tools/device-pod-cli.ts D601-F103-V2:workspace:/ build start \ + --api-base-url http://74.48.78.17:19667 \ + --cookie "$COOKIE" \ + --lease-token "$LEASE" \ + --reason "D601-F103-V2 Keil build validation" \ + --timeout-ms 120000 + +bun tools/device-pod-cli.ts D601-F103-V2:debug-probe download start \ + --api-base-url http://74.48.78.17:19667 \ + --cookie "$COOKIE" \ + --lease-token "$LEASE" \ + --reason "D601-F103-V2 Keil download validation" \ + --capture-uart uart/1 \ + --capture-duration-ms 8000 \ + --timeout-ms 120000 +``` + 验收通过条件:`client device-pods status` 返回 `status=ok` 且 `blocker=null`;gateway sessions 中 `gws_D601_F103` 为 online;`device-pod-cli` job 进入 `completed`,且 D601 Windows gateway `/status.lastDispatch.operationId` 与该 job 的 `operationId` 一致并有 `lastResultAt`。旧 `gws_d601_win_71_freq` session 只能作为历史 stale 线索,不能作为 v0.2 F103 gateway 的目标命名。 ## 测试规格 diff --git a/internal/cloud/access-control.ts b/internal/cloud/access-control.ts index b72f29f1..3c65b32d 100644 --- a/internal/cloud/access-control.ts +++ b/internal/cloud/access-control.ts @@ -8,6 +8,7 @@ const SESSION_MAX_AGE_SECONDS = 60 * 60 * 24 * 7; const CODE_AGENT_DEVICE_POD_SESSION_REFRESH_SKEW_SECONDS = 60 * 5; const DEVICE_JOB_CONTRACT_VERSION = "device-pod-job-v1"; const DEVICE_JOB_INTENTS = new Set([ + "workspace.bootsharp", "workspace.ls", "workspace.cat", "workspace.rg", diff --git a/internal/cloud/codex-stdio-session.ts b/internal/cloud/codex-stdio-session.ts index adb9ce0f..4acd379b 100644 --- a/internal/cloud/codex-stdio-session.ts +++ b/internal/cloud/codex-stdio-session.ts @@ -204,7 +204,7 @@ export const CODEX_STDIO_BOUNDARY_INSTRUCTIONS = [ "Use the provided workspace and repo-owned Codex stdio session only.", "Do not read or print secrets, tokens, kubeconfig files, DB URLs, private keys, or raw environment values.", "For hardware, gateway, box-simu, patch-panel, DAP, PWM, Keil, serial, and Windows skill requests, execute the requested work through the repo-owned Codex stdio session with the available tran wrapper, skill CLI, or project tool that actually reaches the target.", - "For any request that mentions device-pod, device-pod-cli, device-host-cli, a device-pod profile, device-pod-71-freq, device-pod-71-00075-11, or a registered device-pod Keil build/download/UART/debug-probe operation, first use the HWLAB internal skill named device-pod-cli. The canonical skill location is /app/skills/device-pod-cli/SKILL.md; read that manifest and run node /app/skills/device-pod-cli/scripts/device-pod-cli.mjs for the operation.", + "For any request that mentions device-pod, device-pod-cli, device-host-cli, a device-pod profile, device-pod-71-freq, device-pod-71-00075-11, D601-F103-V2, or a registered device-pod Keil build/download/UART/debug-probe operation, first use the HWLAB internal skill named device-pod-cli. The canonical skill location is /app/skills/device-pod-cli/SKILL.md; read that manifest and run node /app/skills/device-pod-cli/scripts/device-pod-cli.mjs for the operation. After selecting the target pod or resuming context, run bootsharp --pod-id before edits, build, or download.", "When a matching device-pod profile exists, do not bypass device-pod-cli with direct hwlab-gateway-tran.mjs, Windows Keil skills, serial-monitor skills, keil-cli.py, or ad hoc path discovery. Only drop to tran or Windows-side skills when the device-pod-cli skill explicitly says to bootstrap, install, or repair the lower layer.", "For registered PC gateway Windows command or skill requests, use the preloaded tran wrapper: node /app/tools/hwlab-gateway-tran.mjs gws_DESKTOP-1MHOD9I:/f/work [options] -- . The locator before ':' is the gateway session and the path after ':' is the Windows workspace, with /f/work and f:/work both mapping to F:\\work.", "For Windows cmd, call tran as: node /app/tools/hwlab-gateway-tran.mjs gws_DESKTOP-1MHOD9I:/f/work cmd -- . For PowerShell, call tran as: node /app/tools/hwlab-gateway-tran.mjs gws_DESKTOP-1MHOD9I:/f/work ps --