244 lines
40 KiB
Markdown
244 lines
40 KiB
Markdown
# 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 Web;runner 使用映射到发起用户的 `HWLAB_API_KEY` 直连 Cloud API,Cloud 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 且没有拼接历史 prompt,replacement 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 Model(HWLAB #795 final)**:Workbench 不允许把 Code Agent 长任务总耗时当成失败条件,也不得在浏览器侧引入任何 total-timeout / hard cap / `codeAgentTimeoutMs * N` 兜底 / 轮询次数上限 / 按 wall-clock 缩小的 per-poll 窗口。Code Agent turn 的唯一 abort 信号是 **inactivity-timeout**:即 `fetchJson` 的 `timeoutMs`(用户配的 `codeAgentTimeoutMs`)窗口内没有任何新 activity(trace 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/blocker;session 是否仍 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 之前 abort;placeholder 消息用 `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)` 兜底 cap;PR #798 删 hard-cap 改成 `for(;;)`,但前端 fail 状态机没改(fail 消息只入本地 state,cloud-api 不知情)+ hydrate path 没有 re-attach 主动订阅。
|
||
- #802 收口:用 `subscribeToTrace` 一个入口统一 submit + hydrate;`onActivity` 在每次成功 poll 上调让 inactivity 永远不 fire;fail 消息也 `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 共 10s,inactivity 窗口 3s,request 必须活到 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 同路径只读 smoke,Code 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/启动新 runner;result 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 返回业务级 4xx,Cloud 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>`,1Gi,StorageClass 走 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` category,UI 文案与 `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。 |
|
||
|