29 KiB
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 走 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 与
hwlab-cli client必须共享同一组非视觉业务 API。浏览器遇到的 Code Agent continuation、trace/result、device-pod list/status 和 device-pod job 问题,必须能通过hwlab-cli client走同一19666Cloud Web path 复现;不能让 CLI 长期绕到19667Cloud API 后把 Web 路径缺口误判为业务已通过。 - Cloud Web 只承担浏览器 UI 和
hwlab-cli client的同源代理。AgentRun runner 内的hwpod不走 Cloud Web;runner 使用映射到发起用户的HWLAB_API_KEY直连 Cloud API,Cloud Web 不保留 device-pod lease 路由。 - 浏览器启动后必须从
GET /v1/workbench/workspacehydrate 账号 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”。只要复用同一 AgentRunSessionRef/PVC/thread 且没有拼接历史 prompt,replacement run/job 可以作为 session 持久化恢复证据;它不替代 T2.2 的同 run/runner reuse 目标。 - AgentRun 会话连续性只有一个标准路径:Cloud Web/CLI 提交的
threadId必须经 Cloud API adapter 写入 AgentRun commandpayload.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 拼接来源。
内部架构
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-pollgetAgentChatResult(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>)+submitMessageupdateActivity()on submit +subscribeToTrace的onActivity在每次成功 poll 上调(保证 inactivity 不被误触)。 - Runaway 保护责任:用户在 Web 上有 cancel / steer / 关 tab 三个明确逃生口;CLI 上有
--timeout-ms让调用方自行决定。不在浏览器侧加隐式硬上限。 - Hydrate 路径必须 re-attach running trace:
useTraceReattach在 mount /activeTraceId变化时通过subscribeToTrace主动订阅到 terminal;不在useEffectcleanup 之前 abort;placeholder 消息用nextProtocolId("msg")创建。 - POST 失败分类:cold-start runner / network down →
onInfrastructureError走persistConversationfail 分支;不要把"POST 超时"和"backend 5xx"写死成同一字符串。
不变量(用于 #802 / 未来回归测试):
bun run --cwd web/hwlab-cloud-web check全过:bun run scripts/tsc-check.ts严格 React TSX 0 explicit any;bun test5 pass / 0 fail。scripts/fetchJson-inactivity.test.ts含elapsed >= 10_000断言(10s 持续活动窗口内永远活)。state/runner-trace.ts只能导出subscribeToTrace;waitForAgentResult/pollRunnerTrace/TRACE_HARD_CAP_ATTEMPTS必须不存在。state/trace-reattach.ts必须存在并被workbench.ts引用;workbench.ts不再直接await subscribeToTrace以外的方式做 trace 轮询。- live
http://74.48.78.17:19666/app.js不含Math.max(t*4、while (...)配totalTimeoutMs的>=比较、> 120之类的硬上限 pattern。 - 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)兜底 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-pollgetAgentChatResult(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>)+submitMessagesubmit kickoff +pollRunnerTrace在新 snapshot 时 mutate ref。runner-trace.ts不直接调updateActivity,避免与state/workbench.ts的submitActivityRef重复。 - Runaway 保护责任:用户在 Web 上有 cancel / steer / 关 tab 三个明确逃生口;CLI 上有
--timeout-ms让调用方自行决定。不在浏览器侧加隐式硬上限。
不变量(用于 #795 / 未来回归测试):
bun run --cwd web/hwlab-cloud-web check全过:bun run scripts/tsc-check.ts严格 React TSX 0 explicit any;bun test5 pass / 0 fail(scripts/fetchJson-inactivity.test.ts3 case + dist-contract 2 case)。scripts/fetchJson-inactivity.test.ts必须包含 "持续 activity > inactivity 窗口" 用例:例如 200ms cadence 推 activity 共 10s,inactivity 窗口 3s,request 必须活到 10s+ 才因 activity stop 而 abort。elapsed >= 10000是不变量;不变量失效即视为本节契约被破坏。- 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。
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。 |
GET/POST /v1/api-keys... |
同源代理到 cloud-api 的 API key 管理入口;短期测试允许当前用户重复查看默认 key 明文。 |
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/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、browser.browserSource、browser.executablePath、browser.fallbackUsed、failureCode 和 remediation;浏览器不可用、auth bootstrap 未完成和真实 layout 失败不得再统一表现成 #command-input hidden 超时。
Live smoke 登录前必须等待前端 auth bootstrap 结束(body[data-auth-state] 不再是 checking,且 login submit 已可见/可用)再填表;登录后必须断言 URL query 不含 username 或 password,防止原生 form submit 泄漏凭据并伪装成 layout 超时。
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/启动新 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 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 返回业务级 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 的环境中执行。
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-blockedcategory,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。 |
| 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/user UI | 未完全实现 | 登录态存在;Keycloak/Web session/API key 需按 spec-v02-auth 收敛,权限 authority 仍需按 spec-user-access 收敛到 cloud-api。 |