Files
pikasTech-HWLAB/docs/reference/spec-v02-hwlab-cloud-web.md
T
2026-06-05 11:14:27 +08:00

210 lines
33 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# v0.2 hwlab-cloud-web 服务规格
`hwlab-cloud-web``v0.2` 浏览器工作台,运行在 `hwlab-v02` namespace,内部端口 `8080`,公网经 FRP 暴露为 `http://74.48.78.17:19666/`
## 在系统中的职责划分
- 向用户提供 Cloud Workbench、Code Agent 对话、live status、device-pod 右侧面板、trace 展示和帮助内容。
- 只消费 `hwlab-cloud-api`,不直接访问 Postgres、gateway、device-pod Service、FRP、Kubernetes 或 provider Secret。
- 为浏览器提供同源代理,避免前端直接跨域调用内部 ClusterIP。
- Web 登录按 [spec-v02-auth.md](spec-v02-auth.md) 走 Keycloak OIDC;未登录用户进入 Keycloak 登录/注册,callback 后由 cloud-api 发行 24 小时 `hwlab_session`。本地账号密码表单和自动 admin 登录只允许作为 legacy/bootstrap fallback,不是目标体验。
- Cloud Web 提供 API key 管理入口,让用户查看默认 API key、创建新 key、revoke 或 regenerate;浏览器日常请求仍使用 Web session,不要求用户手动输入 API key。
- Cloud Web 提供 admin-only Access 页面,让管理员按用户管理 role/status、device pod relation、Code Agent session 可见性和工具 capability;页面只调用 cloud-api `/v1/admin/access*` 同源 API,不直接访问 OpenFGA、Postgres、Kubernetes 或 Keycloak admin API。
- Cloud Web 与 `hwlab-cli client` 必须共享同一组非视觉业务 API。浏览器遇到的 Code Agent continuation、trace/result、device-pod list/status 和 device-pod job 问题,必须能通过 `hwlab-cli client` 走同一 `19666` Cloud Web path 复现;不能让 CLI 长期绕到 `19667` Cloud API 后把 Web 路径缺口误判为业务已通过。
- Cloud Web 只承担浏览器 UI 和 `hwlab-cli client` 的同源代理。AgentRun runner 内的 `hwpod` 不走 Cloud Webrunner 使用映射到发起用户的 `HWLAB_API_KEY` 直连 Cloud APICloud Web 不保留 device-pod lease 路由。
- 浏览器启动后必须从 `GET /v1/workbench/workspace` hydrate 账号 workspace;同一个账号在多个浏览器标签页、多个浏览器或 CLI profile 中应看到同一个 `workspaceId`、selected conversation/session/thread、provider profile 和 active trace。浏览器 localStorage 只能作为短期缓存,并必须绑定 actor,不能作为 workspace authority。
- Code Agent session 管理必须全部手动化。Workbench 可以从账号 workspace 恢复“已显式选中”的 session,但不能在普通发送、页面刷新、trace replay、失败恢复或 provider resume 失败时自动创建、滚动或替换 session。没有已选 session 时,composer 必须展示“新建 session/选择 session”的显式动作;session failed/stale/canceled 后必须保留失败证据,继续前由用户显式新建或选择另一个 session。
- 显式 session 的 `providerProfile` 优先于账号 workspace provider profile。Workbench 可以展示 workspace 默认 provider,但对已选 session 发送 turn 时必须使用该 session 的 provider profile;用户想切换 provider 时,应显式创建或选择对应 provider 的 session,不能把旧 workspace provider 静默套到当前 session 上。
- Cloud Web trace 展示与 `hwlab-cli client agent trace --render web` 必须共享同一套 trace row 纯转换路径。Web 发生 row 顺序错乱、final response 缺失、assistant 消息被吞、tool call 只显示泛化占位或噪声事件淹没时,先用 CLI 输出同一渲染 row 摘要和 noise event count 复现;CLI 可复现说明是 trace row 转换问题,CLI 不可复现再进入 DOM/CSS/滚动状态调查。默认展示应压制 AgentRun backend echo、token/rate-limit/status/terminal echo 等低价值事件,但原始 trace JSON 仍必须保留用于 `--full`/下载排障。
- Cloud Web Code Agent composer 必须无锁:运行中 turn 不得把输入框或发送按钮 disabled。浏览器提交时必须基于已显式选中的 session 工作;没有 session 时返回 `session_required` 并引导用户新建 session,不能自动开 session。存在 active running trace 时,用户显式 steer 动作走 `POST /v1/agent/chat/steer`;空闲且 session usable 时,用户显式发送 turn 走 `POST /v1/agent/chat``hwlab-cli client agent composer status` 必须能用同一 policy 输出 `sessionRequired``sessionUsable``submitMode=turn|steer``route``targetTraceId`
- Code Agent result `completed` 只有在同时包含真实 provider/model/trace/conversation 元数据、`providerTrace` 和可展示的 final assistant response 时,才能被 Web 标记为真实完成;`provider=agentrun-v01` 只是执行基础设施标识,不得替代上游 provider/model,也不得把 SOURCE、fixture、echo、mock 或 stub 当成 DEV-LIVE 完成。
- 同一显式 conversation/session 的后续用户消息必须在 AgentRun runner reuse window 有效时复用已存在的 AgentRun run/runner 继续新 command/turn;只有 runner 不可用、已过期或用户显式创建新 session 时才重新 bundle 和启动 runner。每条消息都重新 bundle/runner 属于 v0.2 AgentRun 接入缺口,不能只靠 trace 显示原因当成已完成。
- runner pod 被删、runner Job 重建或旧 lease 失效后的临时恢复,可以显示新的 AgentRun run/job/command identity,但 Web 必须继续以同一个 HWLAB `sessionId` / `threadId` / provider profile 呈现业务会话,并明确区分“同 session 恢复执行壳”和“新业务 session”。只要复用同一 AgentRun `SessionRef`/PVC/thread 且没有拼接历史 promptreplacement run/job 可以作为 session 持久化恢复证据;它不替代 T2.2 的同 run/runner reuse 目标。
- AgentRun 会话连续性只有一个标准路径:Cloud Web/CLI 提交的 `threadId` 必须经 Cloud API adapter 写入 AgentRun command `payload.threadId``SessionRef.threadId`。前端、CLI、API 和 AgentRun 的协议字段、trace、result 和 conversation facts 都以该字段为唯一 thread identity。
- Cloud Web 提交 Code Agent turn 时只发送当前用户消息、共享 workspace 的 `conversationId/sessionId/threadId`、workspace revision 和必要运行元数据;不得发送 `conversationContext/messages`,也不得把浏览器历史拼入 prompt。历史消息只用于本地 UI 展示和 trace/inspect 可见性,不能替代 AgentRun/Codex stdio 原生 `thread/resume`
- **Code Agent Timeout ModelHWLAB #795 final**Workbench 不允许把 Code Agent 长任务总耗时当成失败条件,也不得在浏览器侧引入任何 total-timeout / hard cap / `codeAgentTimeoutMs * N` 兜底 / 轮询次数上限 / 按 wall-clock 缩小的 per-poll 窗口。Code Agent turn 的唯一 abort 信号是 **inactivity-timeout**:即 `fetchJson``timeoutMs`(用户配的 `codeAgentTimeoutMs`)窗口内没有任何新 activitytrace snapshot / user typing / submit kickoff / server 5xx 重试)。一旦 `activityRef.lastActivityAt` 在窗口内被刷新,per-poll 窗口必须维持原值不缩小,外层 `waitForAgentResult` / `pollRunnerTrace``for(;;)` 循环也没有任何累计计时跳出条件。Runaway 保护的责任在调用方:用户可以走 cancel / steer / 关 tab / 关浏览器 / session cancel,而不是浏览器隐式地按 `4 × totalTimeoutMs` 把活跃 turn 杀掉。AgentRun command/provider/backend 失败后,当前消息可显示 failed/blockersession 是否仍 usable 必须由 session 状态显式表达。`thread-resume-failed`、provider continuation 失效、运行面中断或用户取消导致 session failed/stale/canceled 时,不得自动清理并滚动到新 session;继续前必须由用户显式创建或选择 session。
- 同一 AgentRun run 复用多条 command 时,Web trace 展示只显示当前 command 归属事件和必要 run 级状态;旧 command 的 assistant/tool/terminal 尾部不能堆到新 command 末尾。取消轮次的可读进展必须作为脱敏 conversation facts 进入 UI/trace/inspect 证据,而不是靠旧 trace 尾部串线让后续轮次“碰巧看到”;这些 facts 不得作为下一轮模型上下文或 prompt 拼接来源。
## 内部架构
## Code Agent Timeout Model 与 Path Unification(与 #795 / #802 同步固化)
权威规则:Cloud Web 的 `state/workbench.ts` `submitMessage``state/trace-reattach.ts` `useTraceReattach` 必须遵守以下 contract,禁止分裂成 normal / refresh 两套不同代码路径,禁止任何 wall-clock / 轮询次数 / 4× / +60s 兜底来"安全网"式杀掉活跃 turn
- **单一入口 `subscribeToTrace`**`state/runner-trace.ts::subscribeToTrace` 是 trace 轮询的唯一入口。`waitForAgentResult` + `pollRunnerTrace` 旧 dual 函数对已经删除(HWLAB #802 收口)。`submitMessage` 在 POST 拿到 202 后调它;`useTraceReattach``state.workspace.activeTraceId` 变化时调它(路径归一化:refresh 路径也走同一份 active polling,不再是被动显示)。
- **路径归一化**submit path 和 hydrate path 走同一份 `subscribeToTrace` 逻辑。`onActivity` 在每一次**成功**的 result poll 和 trace poll 上都调(不是只在新 snapshot 上调),所以在正常操作下 per-poll inactivity 窗口**永远不 fire**。
- **失败持久化**`subscribeToTrace``onInfrastructureError`4xx / non-5xx 非 200)会被 `submitMessage``useTraceReattach` 同时调 `persistConversation`,让 cloud-api 留痕前端超时/失败。这样 F5 之后用户能直接看到前端 fail 上下文,不只是 backend completed。
- **唯一 abort 信号**per-poll `fetchJson` 的 inactivity-timeout,由 `activityRef` 驱动。`activityRef.lastActivityAt``totalTimeoutMs` 窗口内被刷新就不 abort。
- **没有 total-timeout / 轮询次数上限**:外层 `for(;;)` 循环没有任何累计计时跳出条件。per-poll `getAgentChatResult(url, totalTimeoutMs, activityRef)` 直接传 `totalTimeoutMs`**不**随墙钟缩小成 `totalTimeoutMs - (Date.now() - startedAt)`
- **没有 `Math.max(totalTimeoutMs * N, ...)` 兜底**:之前的 `max(4x, +60s)` 已经按 PR 反馈彻底删除(PR #798)。
- **activity 来源**`store.recordActivity()``App.tsx` 串到 `<CommandBar onTyping>`+ `submitMessage` `updateActivity()` on submit + `subscribeToTrace``onActivity` 在每次成功 poll 上调(保证 inactivity 不被误触)。
- **Runaway 保护责任**:用户在 Web 上有 cancel / steer / 关 tab 三个明确逃生口;CLI 上有 `--timeout-ms` 让调用方自行决定。**不**在浏览器侧加隐式硬上限。
- **Hydrate 路径必须 re-attach running trace**`useTraceReattach` 在 mount / `activeTraceId` 变化时通过 `subscribeToTrace` 主动订阅到 terminal;不在 `useEffect` cleanup 之前 abortplaceholder 消息用 `nextProtocolId("msg")` 创建。
- **POST 失败分类**cold-start runner / network down → `onInfrastructureError``persistConversation` fail 分支;不要把"POST 超时"和"backend 5xx"写死成同一字符串。
不变量(用于 #802 / 未来回归测试):
1. `bun run --cwd web/hwlab-cloud-web check` 全过:`bun run scripts/tsc-check.ts` 严格 React TSX 0 explicit any`bun test` 5 pass / 0 fail。
2. `scripts/fetchJson-inactivity.test.ts``elapsed >= 10_000` 断言(10s 持续活动窗口内永远活)。
3. `state/runner-trace.ts` 只能导出 `subscribeToTrace``waitForAgentResult` / `pollRunnerTrace` / `TRACE_HARD_CAP_ATTEMPTS` 必须不存在。
4. `state/trace-reattach.ts` 必须存在并被 `workbench.ts` 引用;`workbench.ts` 不再直接 `await subscribeToTrace` 以外的方式做 trace 轮询。
5. live `http://74.48.78.17:19666/app.js` 不含 `Math.max(t*4``while (...)``totalTimeoutMs``>=` 比较、`> 120` 之类的硬上限 pattern。
6. CLI end-to-end`hwlab-cli client agent send --wait` 跑通 + refresh 后用 `client agent trace <traceId> --render web` 拿同一份 row。
历史与收敛(蒸馏自 #775 / #777 / #791 / #795 / #797 / #798 / #802 的过程):
- 迁移前 `app-device-pod.ts:fetchJson` 走 inactivity-timeout`scheduleTimeout` 每秒重算 `remainingMs = timeoutMs - (now - lastActivityAt)`)。React 迁移后 #777`client.ts` 加了 `ActivityRef` 抽象,但 `workbench.ts` / `runner-trace.ts` 没把 ref 串起来,于是 Web UI 退化成 total-timeout#795 出现"bootshar 9.7s 跑完但 Web 37s 处 timeout"的回归。
- #795 PR #797 修了一版但留了 `max(4x, +60s)` 兜底 capPR #798 删 hard-cap 改成 `for(;;)`,但前端 fail 状态机没改(fail 消息只入本地 statecloud-api 不知情)+ hydrate path 没有 re-attach 主动订阅。
- #802 收口:用 `subscribeToTrace` 一个入口统一 submit + hydrate`onActivity` 在每次成功 poll 上调让 inactivity 永远不 firefail 消息也 `persistConversation`
权威规则:Cloud Web 的 `state/workbench.ts` `submitMessage``state/runner-trace.ts` `waitForAgentResult` / `pollRunnerTrace` 必须遵守以下 contract,禁止用任何 wall-clock / 轮询次数 / 4× / +60s 兜底来"安全网"式杀掉活跃 turn
- **唯一 abort 信号**per-poll `fetchJson` 的 inactivity-timeout,由 `activityRef` 驱动。`activityRef.lastActivityAt``totalTimeoutMs` 窗口内被刷新就不 abort。
- **没有 total-timeout**`waitForAgentResult``pollRunnerTrace` 的外层循环是 `for (;;)`,没有 `Date.now() - startedAt >= <cap>` 的跳出。per-poll `getAgentChatResult(url, totalTimeoutMs, activityRef)` 直接传 `totalTimeoutMs`**不**随墙钟缩小成 `totalTimeoutMs - (Date.now() - startedAt)`
- **没有轮询次数上限**`TRACE_HARD_CAP_ATTEMPTS` 已删除。"轮询 120 次还没结束"不是 abort 条件;如果上游永远不返回 terminal 且永远不说话,浏览器就持续 poll 到用户 cancel / 关 tab。
- **没有 `Math.max(totalTimeoutMs * N, ...)` 兜底**:之前 round 9 留的 `max(4x, +60s)` 兜底按 PR 反馈彻底删除,理由是它会重新引入 #795 这类"trace 一直在推但被外层 cap 误杀"的回归。
- **activity 来源**`store.recordActivity()``App.tsx` 串到 `<CommandBar onTyping>`+ `submitMessage` submit kickoff + `pollRunnerTrace` 在新 snapshot 时 mutate ref。`runner-trace.ts` 不直接调 `updateActivity`,避免与 `state/workbench.ts``submitActivityRef` 重复。
- **Runaway 保护责任**:用户在 Web 上有 cancel / steer / 关 tab 三个明确逃生口;CLI 上有 `--timeout-ms` 让调用方自行决定。**不**在浏览器侧加隐式硬上限。
不变量(用于 #795 / 未来回归测试):
1. `bun run --cwd web/hwlab-cloud-web check` 全过:`bun run scripts/tsc-check.ts` 严格 React TSX 0 explicit any`bun test` 5 pass / 0 fail`scripts/fetchJson-inactivity.test.ts` 3 case + dist-contract 2 case)。
2. `scripts/fetchJson-inactivity.test.ts` 必须包含 "持续 activity > inactivity 窗口" 用例:例如 200ms cadence 推 activity 共 10sinactivity 窗口 3srequest 必须活到 10s+ 才因 activity stop 而 abort。`elapsed >= 10000` 是不变量;不变量失效即视为本节契约被破坏。
3. live `http://74.48.78.17:19666/app.js` 不能含 `Math.max(t*4``while (...)``totalTimeoutMs``>=` 比较、`> 120` 之类的硬上限 pattern;如出现即视为本节契约被破坏。
历史与收敛(蒸馏自 #775 / #777 / #791 / #795 的过程):
- 迁移前 `app-device-pod.ts:fetchJson` 走的是 inactivity-timeout`scheduleTimeout` 每秒重算 `remainingMs = timeoutMs - (now - lastActivityAt)`),`waitForAgentMessageResult` 也按 `idleMs >= CODE_AGENT_TIMEOUT_MS` 走。React 迁移后 #777`client.ts` 加了 `ActivityRef` 抽象,但 `workbench.ts` / `runner-trace.ts` 没把 ref 串起来,于是 Web UI 退化成 total-timeout#795 出现"bootshar 9.7s 跑完但 Web 37s 处 timeout"的回归。
- #795 PR #797 修了一版,但留了 `max(totalTimeoutMs * 4, totalTimeoutMs + 60s)` 兜底 cap。用户反馈"硬上限等于又引入 #795 同一类回归",第二轮把 `for(;;)` + 删 `attempt > TRACE_HARD_CAP_ATTEMPTS` + 删 `startedAt` 累计计时 + per-poll 传 `totalTimeoutMs`(不缩小),实现"完全无 total / 轮询上限"。
- Round 10 起 commit / spec / 测试同步固化为本节。
- `web/hwlab-cloud-web/app.ts` 是浏览器端主入口,和 `app-device-pod.ts``app-conversation.ts``app-trace.ts``app-helpers.ts` 共同组成实际 bundle 输入集合,组织 Workbench 状态、Code Agent 会话缓存、trace 轮询和 device-pod 面板。
- `internal/dev-entrypoint/http.mjs` 提供静态服务、health 和 HTTP proxy 基础能力。
- `internal/dev-entrypoint/cloud-web-routes.mjs` 定义可代理到 cloud-api 的同源 API route 和认证边界。
- `web/hwlab-cloud-web/auth.ts` 管理工作台登录态、Keycloak redirect/callback 状态和 API key 管理 UI 调用;真正的登录鉴权和用户权限 authority 仍应收敛到 cloud-api。
- `web/hwlab-cloud-web/views/access` 或等价模块实现 Admin Access 页面:ActivityRail 顶层入口、用户列表、权限矩阵、pending diff、OpenFGA readiness/mismatch 展示和保存结果。页面必须复用同源 fetch client 和 auth blocker,不新增直连 OpenFGA client。
## API 接口说明
| 接口 | 说明 |
| --- | --- |
| `GET /` | Cloud Workbench 首屏。 |
| `GET /health``GET /health/live` | 返回 cloud-web 自身 health 和 build metadata。 |
| `GET /help` | 返回可用 route 摘要。 |
| `GET /auth/oidc/login``GET /auth/oidc/callback``GET /auth/session``POST /auth/logout` | 同源代理到 cloud-api 的 Keycloak/Web session 入口;登录鉴权最终规格见 [spec-v02-auth.md](spec-v02-auth.md)。 |
| `GET/POST /v1/api-keys...` | 同源代理到 cloud-api 的 API key 管理入口;短期测试允许当前用户重复查看默认 key 明文。 |
| `GET/PATCH/PUT/DELETE /v1/admin/access...` | 同源代理到 cloud-api 的 Admin Access API;用于 Access 页面读取 summary/user matrix、授予/撤销 device pod relation、tool capability 和 role/status。 |
| `GET /v1``GET /v1/...` | 同源代理到 `hwlab-cloud-api`;公开的 Code Agent result/trace 轮询按 route policy 处理。 |
| `GET/PATCH /v1/workbench/workspace...` | 同源代理到 cloud-api 的账号 workspace authority,用于 Web/CLI 共享工作区和 revision 冲突保护。 |
| `POST/GET/PATCH /v1/agent/sessions...` | 同源代理到 cloud-api 的显式 Code Agent session 生命周期入口;Web 不在普通 send 中隐式创建 session。 |
| `POST /v1/agent/chat``POST /v1/agent/chat/steer``POST /v1/agent/chat/cancel` | 同源代理到 cloud-api 的 Code Agent 入口;steer 必须走同一个 `19666` Web path,由 cloud-api/AgentRun 判断目标 turn 是否可接收。 |
| `POST /v1/device-pods/...` | 受控同源代理到 cloud-api 的 Device Pod job/操作入口;只要 Cloud API 已提供对应能力,Cloud Web 不能只代理 list/status 而让 job POST 在 `19666` 返回 404。 |
| `POST /v1/web-performance` | 浏览器 RUM 上报入口;只允许低基数性能事件和数值,Cloud API 聚合后进入 Prometheus,详见 [spec-v02-observability-monitoring.md](spec-v02-observability-monitoring.md)。 |
| `POST /v1/m3/io``POST /json-rpc` | 同源代理到受控 API;不能绕过 cloud-api 直连硬件服务。 |
## 测试规格
Cloud Web 的默认校验入口是 `bun run --cwd web/hwlab-cloud-web check`。该入口必须在 v0.2 CI 的 `hwlab-cloud-web` 镜像发布前执行,且保持秒级或低十秒级,不引入浏览器、Playwright、公网或真实 provider 依赖。
Browser/layout/live smoke 属于显式专项诊断,不进入默认 Cloud Web check。所有 repo-owned Playwright smoke、layout smoke 和专项截图脚本必须通过 `scripts/src/browser-launcher.mjs` 启动 Chromium;不得直接 `chromium.launch()` 绑定到偶然存在的 Playwright browser cache。launcher 的浏览器来源判定顺序是显式 env(`HWLAB_PLAYWRIGHT_CHROMIUM` / `PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH` / `CHROMIUM_PATH`)、G14 系统 Chromium(优先 `/snap/bin/chromium`)、最后才是 Playwright cache。诊断入口是 `npm run web:browser:doctor`,输出必须包含 `playwrightPackageSource``platform``managedBrowser.cacheStatus``installCheck.status``browser.browserSource``browser.executablePath``browser.fallbackUsed``browser.candidatePaths``failureCode` 和 remediation;浏览器不可用、auth bootstrap 未完成和真实 layout 失败不得再统一表现成 `#command-input hidden` 超时。
临时 live DOM closeout probe 也必须使用 repo-owned 入口,不得在 `/tmp` 或 issue 验收脚本里手写 Playwright import/launch。标准入口是 `npm run web:dom-probe -- start --url http://74.48.78.17:19666/ --fresh-session`,它会从 workspace 解析依赖、复用统一 launcher、把 PID/stdout/stderr/result/screenshot 写入 `.state/web-live-dom-probe/`,并立即返回 `status` 短查询命令;`npm run web:dom-probe -- status <jobId>` 用于轮询结果。前端-only DOM/renderer/scroll 验收默认不提交 Code Agent;只有显式 `--message` 才经 UI 提交一条真实消息,并按默认策略尝试取消运行中的 turn。`npm run web:browser:guard` 是最小 grep guard,用于确认 `scripts/``tools/``web/` 下没有新增绕过 launcher 的直接 Chromium launch 调用,唯一允许文件是 `scripts/src/browser-launcher.mjs`
WebUI 性能监控 issue 的 live closeout 不能只检查页面可见或 sidecar 存活;必须用 `19666` Web 入口触发真实浏览器 RUM 上报,再按 [spec-v02-observability-monitoring.md](spec-v02-observability-monitoring.md) 查询 `hwlab_webui_*` Prometheus 指标。LCP、Navigation Timing、业务 API timing、Long Task、CLS/INP/FID 近似只表达用户感知性能趋势,不替代 trace/result/inspect 的高基数排障证据。
Live smoke 登录前必须等待前端 auth bootstrap 结束(`body[data-auth-state]` 不再是 `checking`,且 login submit 已可见/可用)再填表;登录后必须断言 URL query 不含 `username``password`,防止原生 form submit 泄漏凭据并伪装成 layout 超时。
Workbench build summary 的顶部 chip 可以展示 `/v1/live-builds.latest`,但 `latest` 是跨所有 HWLAB runtime service 的最新构建,不是 Cloud Web 专属字段。Cloud Web 布局或 build-time issue 的验收必须打开详情或读取 `services[]``serviceId=hwlab-cloud-web` 的行,分别核对 build time、env image、actual commit、revision 和 source metadata;不得因为 `latest.serviceId` 指向 `hwlab-cloud-api` 就判定 Web 构建时间缺失。
Cloud Web check 必须先对实际 bundle 输入集合运行 TypeScript 语义检查。语法检查和 Bun build 只能证明源码可解析或可打包,不能稳定发现未绑定标识符;`isRequestTraceEvent is not defined` 这类错误必须由 semantic check 在发布前拦截。实现上可以生成与 dist build 相同顺序的临时 app entry,再执行 `tsc --noEmit` 或等价 TS checker;只跑 `node --check``bun build` 或源码字符串断言不满足本规格。
Cloud Web 单元测试必须自动发现并执行 repo-owned `web/hwlab-cloud-web/**/*.test.ts`,不允许只维护硬编码文件清单。`app-trace` 的 trace row/render helper 必须有纯逻辑单测,直接构造 request、setup、commandExecution、assistant markdown 和 completion events,证明 trace 展示路径不会因为漏定义 helper、事件分类漂移或 markdown body 渲染变更而在浏览器运行期崩溃。
Cloud Web check 通过后仍需执行 bundle build 和 dist freshness 校验,确保实际发布的 `dist/app.js` 来自同一组 TypeScript 输入。默认 check 不恢复旧 quick prompt、旧 layout smoke、旧 M3 evidence、旧 DEV/D601 browser gate 或 Playwright;这些只能作为显式专项诊断,不得进入默认 CI/CD 关键路径。
## T1
阅读 docs/reference/spec-v02-hwlab-cloud-web.md,然后用 cli 手动测试以下内容:访问 `http://74.48.78.17:19666/``/health/live`,确认页面和 health 均来自 v02 cloud-web,而不是 DEV/PROD 端口。
## T2
阅读 docs/reference/spec-v02-hwlab-cloud-web.md,然后用 cli 手动测试以下内容:先通过同源 `19666` 显式创建或选择 Code Agent session,再提交 Code Agent 短连接请求并轮询 result,确认请求经 cloud-web proxy 到 `hwlab-cloud-api`,且 trace 可回放;未创建或选择 session 的普通 send 必须返回 `session_required`,不能自动创建 session。
阅读 docs/reference/spec-v02-hwlab-cloud-web.md,然后用 cli 手动测试以下内容:对运行中 Code Agent trace 调用 `hwlab-cli client agent steer <traceId> --message ...`,确认请求走 Cloud Web 同源 `POST /v1/agent/chat/steer`Web 层不能返回 `serviceId=hwlab-cloud-web` 的 404,目标不存在、非运行中或 runner 拒绝时必须透传 cloud-api/AgentRun 的结构化业务状态。
阅读 docs/reference/spec-v02-hwlab-cloud-web.md,然后用 cli 手动测试以下内容:未选择 session 时运行 `hwlab-cli client agent composer status` 必须显示 `sessionRequired=true`;显式创建 session 并启动一个真实运行中的 Code Agent turn 后,再运行 `hwlab-cli client agent composer status`,确认输出当前 `sessionId``composer.submitMode=steer``composer.route=/v1/agent/chat/steer` 和当前 `targetTraceId`;随后运行 `hwlab-cli client agent composer submit --message ...`,确认 CLI 按 Web composer policy 走 steer,但不自动创建或滚动 session。
## T2.1
阅读 docs/reference/spec-v02-hwlab-cloud-web.md,然后用 cli 手动测试以下内容:对浏览器暴露的 Code Agent trace 运行 `hwlab-cli client agent trace <traceId> --render web --limit 80`,确认 CLI 与 Web 使用同一 trace row 转换;若 final response 缺失、assistant row 顺序错乱或噪声事件过多,先用 CLI 固定复现再修实现。
## T2.2
阅读 docs/reference/spec-v02-hwlab-cloud-web.md,然后用 cli 手动测试以下内容:先显式创建 session,再在同一 conversation/session 连续发送两条 Code Agent 消息,确认第二条复用第一条的 AgentRun `runId` 和 runner `jobName`、生成新的 `commandId`,且不重新 materialize bundle/启动新 runnerresult completed 必须包含真实 provider/model/`providerTrace`/trace/conversation 和 final assistant response。复用失败原因只能作为诊断,不作为本测试通过条件;如果 runner pod 已被删除或旧 lease 已失效,replacement run/job 只能作为同 session/PVC/thread 的恢复证据记录,不能算作本测试的 run/runner reuse 通过。如果 session failed/stale,必须显式创建新 session 再继续。
## T2.3
阅读 docs/reference/spec-v02-hwlab-cloud-web.md,然后用 cli 手动测试以下内容:对一轮已取消但取消前存在 assistant/tool 进展的 trace 发送同一 conversation/session/thread 后续问题,确认请求和 result 只携带标准 `threadId`,不出现历史 thread 别名字段;`--render web` 输出不得把上一 command 的尾部 assistant/tool/terminal row 堆到新 command 末尾。
## T3
阅读 docs/reference/spec-v02-hwlab-cloud-web.md,然后用 cli 手动测试以下内容:打开 Workbench device-pod 面板,确认 status/freshness/blocker 显示来自 `/v1/device-pods`,未登录或未授权时必须显示认证/授权 blocker,不得把 fixture 或 blocked fallback 写成真实硬件 DEV-LIVE。
## T3.1
阅读 docs/reference/spec-v02-hwlab-cloud-web.md,然后用 cli 手动测试以下内容:通过 `19666` Cloud Web 同源 path 对当前允许的 device-pod job/操作 POST 做只读或 dry-run 级验证,确认与 `19667` Cloud API 的 route policy 对齐;如果 Cloud API 返回业务级 4xxCloud Web 也应透传业务错误,不应在 Web 层直接 404。
## T4
阅读 docs/reference/spec-v02-hwlab-cloud-web.md,然后用 cli 手动测试以下内容:运行 `bun run --cwd web/hwlab-cloud-web check`,确认输出或日志显示已执行 Cloud Web TypeScript 语义检查、自动发现的单元测试、bundle build 和 dist freshness 校验;不得用只跑 `bun build` 或浏览器手工刷新替代该检查。
## T5
阅读 docs/reference/spec-v02-hwlab-cloud-web.md,然后用 cli 手动测试以下内容:确认 trace 渲染相关单测覆盖 request、setup、commandExecution、assistant markdown 和 completion row;该测试必须能在无浏览器、无 Playwright、无公网、无真实 provider 的环境中执行。
## T6
阅读 docs/reference/spec-v02-hwlab-cloud-web.md 和 docs/reference/spec-v02-openfga-authorization.md,然后用 cli 手动测试以下内容:登录 admin 后打开 Cloud Web Access 页面,确认 ActivityRail 显示 Access 入口,页面加载 `/v1/admin/access/summary` 和用户权限矩阵;普通用户访问同一路由必须显示 authorization blocker。通过页面授予/撤销一次 device pod relation 后,`hwlab-cli client access users inspect <user>` 必须看到同一 effective matrix。
## Session state 持久化与 eviction reset
HWLAB v0.2 Code Agent chat turn 默认走 `sessionStorage=persistent`:每次 turn 由 `code-agent-agentrun-adapter.ts` 在创建 run 之前显式调 `POST /api/v1/sessions`,让 AgentRun v0.1 同步创建 RWO PVC`agentrun-v01-session-<sessionId>`1GiStorageClass 走 env `AGENTRUN_SESSION_STORAGE_CLASS` 默认 `local-path`)。runner Job manifest 渲染时多挂一个 `agentrun-sessions` volume + `AGENTRUN_SESSION_PVC_NAME` / `_NAMESPACE` / `_MOUNT_PATH` / `AGENTRUN_CODEX_ROLLOUT_SUBDIR` env,使 codex app-server 自己把 rollout JSONL 写进 PVC,跨 runner pod 重建天然 `thread/resume:completed`
runner pod 或 runner Job 丢失但 PVC 仍存在时,下一轮必须继续使用同一个 HWLAB `sessionId`、标准 `threadId`、session provider profile 和 AgentRun `SessionRef`/PVC 执行 resume。当前临时恢复允许 Cloud API 启动 replacement run/job 作为执行壳;验收必须记录 replacement run/job 与原业务 session 的对应关系,并证明没有通过历史 prompt、messages 或 fake resume 续接。长期目标仍是 runner reuse window 内同一 AgentRun run/runner 多 command。
### Eviction reset UX
当 AgentRun v0.1 上报 `failureKind=session-store-evicted`(PVC 被删或 TTL 到期)时,HWLAB cloud-api 走 reset UX
-`conversationId`,发新 `sessionId``newSessionIdAfterEviction(baseSessionId, traceId)` = `<base>-reset-<trace8>`),`threadId=null` 强制走 `thread/start`
- 旧 session 的 `storageKind` 已被 AgentRun 标为 `evicted`HWLAB adapter 不再 reuse 旧 mapping。
- Workbench 把 `errorCode=session_storage_evicted` 归到 `session-blocked` categoryUI 文案与 `session_failed` 区分:「Code Agent session 存储已失效(PVC 被回收 / TTL 到期),HWLAB 已为你开新 sessionId,可继续发送下一条消息。」
禁止路径:
- 不允许 fake `thread/resume:completed`PR #78 已锁定的 v0.1 contract)。
- 不允许 `idleTimeoutMs` 拉成永驻当成本特性。
- 不允许 runner Job 启动后再做 copy/restore(本方案撤掉的路径,禁止复活)。
### 实现情况
| 规格项 | 状态 | 说明 |
| --- | --- | --- |
| 调 POST /api/v1/sessions 同步建 session + PVC | 已实现 | `code-agent-agentrun-adapter.ts::ensureAgentRunSessionPersistent` 默认 `sessionStorage=persistent`env `HWLAB_CODE_AGENT_AGENTRUN_SESSION_STORAGE=ephemeral` 可退回 metadata-only 模式。 |
| runner Job 直接挂载 PVC | 已实现/已通过 HWLAB v0.2 原入口复测 | runner Job manifest 由 AgentRun v0.1 渲染 per-session PVC 直接挂载;HWLAB 侧以同 session/thread/PVC resume 作为恢复证据,不走 copy/restore。 |
| eviction reset UX | 已实现 | submitAgentRunChatTurn 走两步尝试,第一步在 ensureSession 或 runner-jobs POST 收到 `session-store-evicted` 时用新 sessionId + `threadId=null` 重试。 |
| UX 错误码 `session_storage_evicted` | 目标状态/需专项复测 | `session-store-evicted` 必须归入 `session-blocked`,并向用户区分“PVC 被回收,需要新 session”与普通 provider/backend 失败;不能用 fake resume 或自动无感滚动掩盖。 |
## 规格的实现情况
| 规格项 | 状态 | 说明 |
| --- | --- | --- |
| Workbench 首屏 | 已实现 | 当前页面直接进入工作台,不是 landing page。 |
| cloud-api 同源代理 | 已实现 | 受 route policy 控制;device-pod job POST 必须与 Cloud API route policy 对齐。 |
| Code Agent UI/trace/result | 已实现 | 支持 provider profile、timeout、trace 轮询和取消。 |
| Code Agent 无锁 composer | 已实现 | Web/CLI 共享 composer policy;运行中输入框保持可编辑并自动走 steer。 |
| 账号 workspace hydrate/sync | 已实现 | 启动读取 `account_workspaces`Code Agent 请求携带 workspace revision,终态再同步 workspace。 |
| device-pod 面板 | 未完全实现 | 当前主要消费 fake/只读 device-pod payload。 |
| Admin Access 授权页面 | 目标状态 | 新增 admin-only ActivityRail 页面,管理 OpenFGA relation/tool capability/role status,并与 CLI `client access` 共用同一路径。 |
| 完整多用户 admin/user UI | 未完全实现 | 登录态存在;Keycloak/Web session/API key 需按 spec-v02-auth 收敛,权限 authority 仍需按 spec-user-access 收敛到 cloud-api。 |