Files
pikasTech-HWLAB/docs/reference/spec-v02-hwlab-cloud-web.md
T
2026-06-08 10:37:56 +08:00

244 lines
40 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/`
Provider API Key 配置入口也归属 Cloud Web:左侧顶级导航必须提供“管理”页面,具体路由、状态展示、API Key 写入表单、验证结果展示和脱敏规则见 [spec-v02-provider-management.md](spec-v02-provider-management.md)。Cloud Web 不直接调用 AgentRun,不保存完整 API Key,也不把 AgentRun token 暴露给浏览器。
## 在系统中的职责划分
- 向用户提供 Cloud Workbench、Code Agent 对话、live status、HWPOD node-ops 右侧面板、trace 展示和帮助内容。
- 只消费 `hwlab-cloud-api`,不直接访问 Postgres、gateway、host/node 资源、FRP、Kubernetes 或 provider Secret。
- 为浏览器提供同源代理,避免前端直接跨域调用内部 ClusterIP。
- Web 登录按 [spec-v02-auth.md](spec-v02-auth.md) 走 Keycloak OIDC;未登录用户进入 Keycloak 登录/注册,callback 后由 cloud-api 发行 24 小时 `hwlab_session`
- Cloud Web 提供 API key 管理入口,让用户查看默认 API key、创建新 key、revoke 或 regenerate;浏览器日常请求仍使用 Web session,不要求用户手动输入 API key。
- Cloud Web 提供 admin-only Access 页面,让管理员按用户管理 role/status、Code Agent session 可见性和工具 capability;页面只调用 cloud-api `/v1/admin/access*` 同源 API,不直接访问 OpenFGA、Postgres、Kubernetes 或 Keycloak admin API。
- Cloud Web 提供 admin-only Provider 管理页面,让管理员通过 cloud-api `/v1/admin/provider-profiles*` 同源 API 配置 AgentRun provider API Key;页面不得直接访问 AgentRun、Kubernetes Secret、Moon Bridge 或 provider upstream。
- Cloud Web 与 `hwlab-cli client` 必须共享同一组非视觉业务 API。浏览器遇到的 Code Agent continuation、trace/result 和 HWPOD node-ops 问题,必须能通过 `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 不保留 HWPOD 运行路由。
- 浏览器启动后必须从 `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。AgentRun dispatch、dynamic profile slug 和 nested child `spawn` env-only 继承的权威规则见 [agentrun-code-agent-dispatch.md](agentrun-code-agent-dispatch.md)。
- 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`
- Steer 是短连接控制动作,不是等待目标 turn terminal 的长请求。`POST /v1/agent/chat/steer` 在 cloud-api 成功创建 AgentRun `type=steer` command 后必须立即以 HTTP 202 返回,并在响应中暴露 `accepted=true``shortConnection=true``route=/v1/agent/chat/steer``traceId``steerTraceId``agentRun.runId``agentRun.targetCommandId``agentRun.steerCommandId`。接口不得为了 owner/session/workspace/conversation 持久化写回而延迟 202;这些写回失败只能作为后台可观测问题处理,不能把已 accepted 的 steer 重新标成请求失败。
- Workbench 对 steer submit 的失败分类必须以目标 trace 的真实状态为准:如果 steer POST 发生 timeout/ECONNRESET/transport failure,但目标 trace/result 仍是 running 或其他非 terminal 状态,前端必须释放本次 submit lock、保留原 agent message 的 running/steerable 状态并继续 trace 轮询,不得把原 turn 改成 failed 或展示“Code Agent 请求失败”。只有目标 trace/result、cloud-api 或 AgentRun 返回 terminal failed/blocker/canceled 时,才把目标消息标为相应终态。
- 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 拼接来源。
## Workbench 首屏性能契约
`/``#/workspace` 是 Cloud Web 的用户业务首屏;性能页、构建检查、本地 mock、CLI-only 请求或 sidecar 存活不能替代该入口。首屏性能目标用于用户体感回归判断,不作为新的发布旧门禁:真实浏览器打开 `http://74.48.78.17:19666/#/workspace` 后,LCP 应稳定低于 `3s`CLS 应低于 `0.1``#command-input` 必须可见且可输入。
首屏 bootstrap 路径必须保持窄路径:
- HTML 在 `/``#/workspace` 首屏可以在解析期提前发起一次 `GET /auth/workspace-bootstrap?projectId=prj_device_pod_workbench`。React auth/workbench 状态必须优先消费 `window.HWLAB_CLOUD_WEB_EARLY_WORKSPACE_BOOTSTRAP` 的 promise;同一首屏不得再额外发起 `/auth/bootstrap``/auth/session``/v1/workbench/workspace` 来重复 hydrate 同一账号 workspace。
- `/auth/workspace-bootstrap` 返回的 user/actor/workspace 只能作为首屏 seed。账号 workspace、session、conversation、provider profile 和 revision authority 仍在 `hwlab-cloud-api` / Postgres;浏览器 localStorage 仍只是 actor-bound cache,不能成为 authority。
- 首屏允许加载当前工作台可见所需的 conversations、device-pods 和 live-builds 摘要,但 trace replay、`events?limit=120`、性能 summary、重型右侧面板、旧事件列表和非当前视口内容必须延后到首屏稳定后、用户展开后或具体业务需要时加载。
历史消息渲染必须优先保证首屏稳定,不得牺牲业务语义:
- 首屏 hydrate 里已经存在的历史 Code Agent 消息先以纯文本 fallback 展示,文本必须按 React 默认规则转义,不能执行 raw HTML。Markdown/GFM renderer 必须通过独立 `MarkdownRenderer` chunk 懒加载,首屏 LCP 观察窗口内不应加载该 chunk。
- 首屏历史 final response 必须带稳定标识并用本地滚动边界约束高度,避免长历史回复继续驱动页面级 LCP/CLS。约束只影响首屏已有历史消息的初始展示,不得删除、截断、改写 conversation history,也不得影响新消息、用户显式打开的详情、trace/result 原始证据或首屏后 Markdown 升级。
- 不得用隐藏历史消息、禁用 composer、删除 Code Agent 结果、减少真实业务能力或把历史 prompt 拼回下一轮消息的方式换取 LCP 数字。历史消息只用于 UI 展示和 trace/inspect 可见性,不能替代 AgentRun/Codex 原生 thread/resume。
测试和门禁只表达当前契约:旧同步 Markdown table 断言、旧 exact-string bundle guard、旧 layout smoke、旧 M3 evidence、旧 DEV/D601 browser gate 和与当前首屏目标无关的历史断言一律拆除,不迁移、不兼容保留。默认 `bun run --cwd web/hwlab-cloud-web check` 只保留轻量源码层保障:早发 workspace bootstrap 被消费、首屏历史 Markdown 延迟、历史 final response 有稳定高度边界、lazy renderer 仍能在升级后支持 Markdown/GFM 且 raw HTML 不执行。真实 LCP/CLS、请求数量和 RUM/Prometheus 改善必须通过专项 live browser closeout 验证。
## 内部架构
## 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 的过程):
- React 收敛前的旧单页 `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 的过程):
- React 收敛前的旧单页 `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/src/App.tsx` 是浏览器端主入口,和 `src/components/**``src/state/**``src/services/**``src/types/**` 共同组成实际 bundle 输入集合,组织 Workbench 状态、Code Agent 会话缓存、trace 轮询和 HWPOD node-ops 面板。
- `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、授予/撤销 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 是否可接收,并在 AgentRun steer command 创建后短连接 202 返回。 |
| `POST /v1/hwpod-node-ops` | 受控同源代理到 cloud-api 的 HWPOD node-ops 入口;用于 Web/CLI 同路径只读 smokeCode Agent 正式业务入口仍是 runner 内 `hwpod` 直达 Cloud API。 |
| `POST /v1/web-performance` | 浏览器 RUM 上报入口;只允许低基数性能事件和数值,Cloud API 聚合后进入 Prometheus,详见 [spec-v02-observability-monitoring.md](spec-v02-observability-monitoring.md)。 |
| `GET /v1/web-performance/summary` | 性能监控顶级页读取的同源摘要接口;返回低基数 WebUI 体感性能 JSON,包含样本数、route p95、Web Vitals、long task 和错误/超时问题队列,不返回 Prometheus 原始文本或高基数 trace/session/conversation/thread/user 标识。 |
| `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 的高基数排障证据。
Workbench 首屏性能 issue 的 closeout 必须用 repo-owned browser launcher / live DOM probe 或其专项扩展访问 `http://74.48.78.17:19666/#/workspace`,并记录 LCP、CLS、command input 可用性和首屏请求清单。若现有探针缺少 LCP/CLS 或 request counting,可先扩展该探针;不要在 issue closeout 里长期手写临时 Playwright launcher 或绑定偶然存在的 Playwright browser cache。
Cloud Web 顶级性能页是观测面,不是业务工作台。访问 `/performance` / `#/performance` 时不得初始化 Workbench store 的 workspace hydrate 或 live refresh,也不得触发 HWPOD node-ops、agent conversations、live-builds、system.health 等 workspace-only 请求。性能页只允许主动请求 `GET /v1/web-performance/summary` 和静态资源;如果需要调查 workspace 或 HWPOD 性能,必须从 workspace 原入口触发样本,而不是让性能页自己制造业务流量。
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 端口。
## T1.1
阅读 docs/reference/spec-v02-hwlab-cloud-web.md 和 docs/reference/spec-v02-observability-monitoring.md,然后用真实浏览器或 repo-owned live DOM probe 打开 `http://74.48.78.17:19666/#/workspace`。验收必须记录:LCP `<3s`、CLS `<0.1``#command-input` 可见且未 disabled、首屏只有一次 `/auth/workspace-bootstrap?projectId=prj_device_pod_workbench`,并且首屏观察窗口内 `/auth/bootstrap``/auth/session``/v1/workbench/workspace`、trace replay、`events?limit=120``MarkdownRenderer` chunk 均未出现。随后用同源 `/v1/web-performance/summary` 和受控 Prometheus 查询确认 `/workspace` WebUI RUM 样本存在;summary 仍处于 warming 时必须如实说明,不能用性能页自身样本替代 workspace 样本。
## 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`;成功路径必须在短请求内返回 HTTP 202、`accepted=true``shortConnection=true``agentRun.steerCommandId`,再通过原 trace 的 `result/trace --render web` 看到 `agentrun:steer:accepted``agentrun:steer:command-created`。Web 层不能返回 `serviceId=hwlab-cloud-web` 的 404;目标不存在、非运行中或 runner 拒绝时必须透传 cloud-api/AgentRun 的结构化业务状态,不得把仍 running 的原 turn 误标 failed。
阅读 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 HWPOD node-ops 面板,确认 status/freshness/blocker 显示来自 `/v1/hwpod-node-ops`,未登录或未授权时必须显示认证/授权 blocker,不得把 fixture 或 blocked fallback 写成真实硬件 DEV-LIVE。
## T3.1
阅读 docs/reference/spec-v02-hwlab-cloud-web.md,然后用 cli 手动测试以下内容:通过 `19666` Cloud Web 同源 path 对 `/v1/hwpod-node-ops` 提交只读 `node.health` plan,确认与 `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。通过页面授予/撤销一次 `tool:hwpod` capability 后,`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。 |
| Workbench 首屏性能契约 | 已实现 | 首屏通过 early workspace bootstrap、seed 复用、历史 Markdown 延迟升级和历史 final response 稳定高度边界降低 LCP/CLS;关闭性能 issue 前必须用 `19666/#/workspace` 原入口复测。 |
| cloud-api 同源代理 | 已实现 | 受 route policy 控制;HWPOD node-ops 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。 |
| HWPOD node-ops 面板 | 已实现 | 当前消费 `/v1/hwpod-node-ops` 的只读健康 plan,展示 node/action/result/blocker 摘要。 |
| Admin Access 授权页面 | 目标状态 | 新增 admin-only ActivityRail 页面,管理 OpenFGA relation/tool capability/role status,并与 CLI `client access` 共用同一路径。 |
| Provider API Key 管理页面 | 已实现 | `#/management` admin-only 管理页面走 `/v1/admin/provider-profiles*` 同源 API 并委托 AgentRun;CLI 同路径入口用于非视觉验收。 |
| 完整多用户 admin/user UI | 未完全实现 | 登录态存在;Keycloak/Web session/API key 需按 spec-v02-auth 收敛,权限 authority 仍需按 spec-user-access 收敛到 cloud-api。 |