# HWLAB Cloud Workbench Reference（历史路径）

本文不再承载 Cloud Workbench 需求规格正文。

统一规格出处是 UniDesk OA：

- [PJ2026-0104 客户端](https://github.com/pikasTech/unidesk/blob/master/project-management/PJ2026-01/specs/PJ2026-0104-client.md)
- [PJ2026-010401 Web工作台](https://github.com/pikasTech/unidesk/blob/master/project-management/PJ2026-01/specs/PJ2026-010401-web-workbench.md)
- [PJ2026-010402 HWLAB CLI](https://github.com/pikasTech/unidesk/blob/master/project-management/PJ2026-01/specs/PJ2026-010402-hwlab-cli.md)
- [PJ2026-010403 API契约](https://github.com/pikasTech/unidesk/blob/master/project-management/PJ2026-01/specs/PJ2026-010403-api-contract.md)

历史 layout smoke、浏览器排障和实现细节只作为运行参考，不定义需求边界。需要修改 Web 工作台、同源 CLI、trace/result 或公开入口口径时，只更新 UniDesk OA。

## Workbench 运行参考

Workbench 页面和组件必须明确区分“未加载完成”和“已加载但为空”。未加载完成时不得渲染由 workspace、localStorage、默认对象或 stub 记录拼出来的伪数据；这类数据只能作为内部恢复线索，不能在列表、表格、卡片或状态栏中冒充真实加载结果。已经确认后端返回且集合为空时，才显示空态文案。

Workbench 状态对象必须服从 UniDesk OA Web SPEC 中已经定下来的单一权威 API 绑定。Session rail 的会话集合只允许消费 `/v1/workbench/sessions` 成功返回的 Workbench session read model；当前 selected conversation 如果需要出现在左侧列表中，必须通过显式 query/path/body 传入稳定 conversation id，由该列表 API 在同一响应中返回，后端不得隐藏读取 workspace selected、localStorage、Web snapshot 或上一轮页面状态。前端不得用 workspace selected snapshot、stub、localStorage 或 route 状态补出 session tab，也不得把前端拼接出的 status、final response、markdown、running 动效或 stub 传回后端变成事实。Session 运行状态、turn 投影、trace 终态和 final response 的自动恢复必须以 Workbench read model 和 `/v1/workbench/sync` replay 为权威；`/v1/workbench/turns/:traceId`、`/v1/workbench/traces/:traceId/events` 与旧 `/v1/agent/*` detail 入口只能服务显式详情、历史查看或兼容调用，不能成为 Workbench 自动投影的第二条事实来源。任何权威 API 失败时只能保留上一份成功结果或显示未加载/错误态，不得切换到另一条 fallback 路径形成一条会话、旧 running 态或劣化 markdown。

Workbench 投影写路径必须做到 0 隐式 fallback。Admission、projection event、terminal/finalizer 等上游写入如果无法把 session/message/turn/checkpoint facts 写入 durable read model，不能 `catch` 后返回空值继续表现为成功；admission 阶段必须显式失败并把错误传给调用方，后台投影阶段必须至少写入 trace diagnostic 和 OTel error span。只有成功落库的 Workbench facts 才能驱动控制页、观察页、session rail、耗时和 final response；前端或 read path 不得用内存 trace、local optimistic state、历史 snapshot 或多来源仲裁去修补失败写入。

Workbench 的 trace/message/projection 运行时必须是独立模块边界。`workbench.ts` 只保留 session/route authority 校验、Pinia state commit、刷新调度和用户动作编排；trace snapshot、terminal result、message timing/status patch、agent error normalize、projection diagnostic 裁剪和 final response 文本提取等纯算法统一由 `web/hwlab-cloud-web/src/stores/workbench-message-projection-runtime.ts` 提供。新增或修复浏览器 smoke 时不得把这些 helper 重新散写回 store 或组件，也不得通过 reload、repair、localStorage truth、GET read-through、测试专用后门或删除 guard 来绕过真实投影问题。web-probe origin、视口、采样、命令超时、provider/lane 和报警阈值只从选中 node/lane 的受控 YAML/source-of-truth 进入验证命令，不在 SPEC 或前端 runtime 中写第二份数值。

Workbench terminal 三态必须作为一个不可拆分的投影不变量维护：session rail 状态、turn 卡片状态和 final response object 要么同时表现为完成且 final response 存在，要么同时表现为运行且 final response 不存在。持久 read model 是 session、messages、turn、trace tuple 的 source of truth；前端 `workbench-server-state` 只能做归一化缓存和单调合并，不能把 stale running 刷新覆盖到同一 trace 的 terminal authority 上。`session.status`、`turn.status`、`session.list/detail/messages`、`message.snapshot`、projection page merge、detail/history trace reads 和 SSE trace snapshot 都必须保留已 sealed terminal 的 `status`、`traceAutoLifecycle`、`text`、`finalResponse` 和 terminal timing；新 trace 的 running 可以让同一 session 进入下一轮运行，但同 trace 的 running/non-terminal 只能作为旧事件丢弃或合并为 runnerTrace 证据。判断 final response 是否存在时必须能从 `text`、`content`、`finalResponse` 或 `reply/finalText` 等权威字段提取非空文本，空对象、进度 assistant trace 文本或仅有 AgentRun completed 状态都不能 seal completed；Workbench read model、`/v1/workbench/sync` replay、projection writer、runtime store invariant、显式 turn/detail API 和前端缓存层必须共用这个 terminal seal gate。新增修复应优先补 `workbench-message-projection-runtime`、`workbench-server-state` 或对应后端 read-model 的最小单元测试，而不是用 UI 特判、reload 或额外 fallback 掩盖投影分叉。

MDTODO 发起 Workbench 执行时，HWPOD 执行上下文的唯一权威来源是 Project Management source registry。Workbench Launch 服务端必须通过 `taskRef -> sourceId/fileRef -> source` 解析 `launchContext.executionContext`，并把同一份 `contextFingerprint` 写入 session owner、Workbench facts、project-management link 和 OTel span；浏览器传入的 HWPOD 字段只能作为任务元数据，不能作为权威执行上下文。`sourceKind=hwpod-workspace` 但缺少 `hwpodId`、`nodeId` 或 `workspaceRootRef` 时，launch 必须显式失败，不能创建“看似成功但无法执行”的空 session。

MDTODO 首轮 Code Agent prompt 必须包含 `hwpodId`、`mdtodoRootRef` 和 `hwpodWorkspaceArgs`，并要求所有 `hwpod/hwpod-ctl` 命令携带该参数。Agent 不得猜测容器本地路径，不得创建、复制或修补本地 `.hwlab/hwpod-spec.yaml` fallback。Workbench session header 可以显示 source、HWPOD 和 MDTODO root 的只读短标识；workspace host path 的长期可观测性默认使用 basename/label、hash 或 redacted 形态。

Cloud Web 的通用加载态使用 `web/hwlab-cloud-web/src/components/common/LoadingState.vue`。新增或修复页面加载态时优先复用该组件，并通过明确的 ready/loading 状态控制展示；不要在每个组件里重新实现一套 spinner、点状动画或默认占位数据。紧凑区域可以使用组件的 compact 形态，文案默认保持“加载中”。

Session rail 是该规则的高频区域。`/v1/workbench/sessions` 还未返回时，即使 workspace 中已有 `selectedConversationId`、sessionId、traceId 或 selected conversation snapshot，也不能把选中 session stub 渲染成单条 `.session-tab`，更不能让它占满整个 session 列表高度。加载窗口应只显示 `LoadingState`，并隐藏当前 trace 元信息、复制/删除等依赖真实 active tab 的动作；待 Workbench session list ready 后再渲染真实 session tabs，或在真实空集合时显示空态。

Session rail 的后台恢复刷新必须有硬边界。显式用户动作或强一致操作（例如选择会话、删除当前会话）可以立即刷新会话列表；SSE error、active trace sync replay、terminal refresh、detail/history trace reads 等后台补偿路径不得绕过 session list 的冷却/合并机制去强制刷新完整列表。后台路径应优先补当前 trace、turn status、message projection 和必要的 trace events；需要刷新 session rail 时走统一的 scheduled refresh，并按 session/list key 合并已有 timer，避免网络抖动或 EventSource error storm 把 `/v1/workbench/sessions` 放大成浏览器内存和 CDP responsiveness 红灯。

Workbench realtime 恢复与 sync replay 必须统一接入已经迁移的 OpenCode-style runtime 模块。`workbench-stream-transport` 只拥有 SSE lifecycle、cursor 和 recovery reason；`workbench-realtime-plan` 只把 transport action 转成纯 plan；`workbench-refresh-runtime`、keyed singleflight、scheduled task runtime、trace history queue、server-state reducer 和 session cache 是恢复读取的唯一 substrate。`workbench.ts` 不得再持有新的 in-flight map、timer map、cursor map、detail-read recovery queue 或第二套 recovery coordinator。`force` 是用户显式操作和恢复优先级语义，不得绕过同 key 的 singleflight、cooldown、min interval 或 storm budget；这些预算、退避、并发、页数、重试和窗口参数只由 node/lane YAML-backed runtime policy 注入，SPEC 只声明字段族、责任边界和验收读取方式，不写死数值。

Workbench Realtime Authority v2 的自动恢复只允许消费 SSE typed event 和 `/v1/workbench/sync` replay。`/workbench/sync` 返回的 durable `delta.messages`、`delta.turns` 等 family object 必须在前端 authority 层投影成带 `realtimeAuthority`、entity family/id/version 和 projection revision 的 `message.snapshot`、`turn.snapshot` 等 typed event，再进入统一 reducer；不得把 family delta 当成不可应用的普通对象，也不得用 `/v1/workbench/sessions/:id/messages`、`/v1/workbench/turns/:id` 或 `/v1/workbench/traces/:id/events` 自动 fan-out 补洞。跨 tab/page 的 `session-projection` signal 只能触发同一 `/workbench/sync` replay 或临时 optimistic echo，最终必须由 durable sync delta 覆盖并让 control/observer 页在同一个 session 上收敛到相同 messages/turn projection。`workbench-server-state` 的 `message.snapshot` guard 只能拒绝无 messageId、无 trace 或当前会话没有同 trace 上下文的孤儿 agent snapshot；durable user fact 之后到达的同 trace agent snapshot 必须可追加。新增修复应覆盖“observer 从空消息状态应用 sync replay 后得到 user/agent messages 与 turn status”的最小测试，并用 web-probe `observe analyze` 确认没有 persistent `cross-page-projection-divergence` 或 automatic recovery legacy fan-out 红项。

Workbench 只能维护一条会话恢复与提交路径。首次打开、新建后继续、从左侧 session rail 切换、直接进入 `/workbench/sessions/<conversationId>` 恢复时，都必须以当前 route/active conversation id 作为会话真相，并通过同一条 conversation detail read 路径得到 messages、turn state、trace/status 和 markdown 渲染输入；不得另写只消费列表 snapshot、workspace stub 或 localStorage selected id 的恢复分支。

Workbench 的 URL 反射必须服从用户当前导航和组件生命周期。`activeConversationId`、hydrate、select conversation 或列表刷新等异步状态只能在当前 route 仍属于 Workbench section、路径仍是 `/workbench`/`/workspace` 系列且 Workbench 组件仍 active 时，才允许把 URL 反射到 `/workbench/sessions/<conversationId>`；用户已经点击 Dashboard、API Keys、Admin、Settings 或其他非 Workbench 导航后，晚到的 Workbench 响应只能更新 store，不得再调用 `router.replace`/`router.push` 把全局 route 拉回 Workbench。新增 session 恢复或 URL 反射入口时必须复用共享路由守卫，例如 `web/hwlab-cloud-web/src/router/workbench-navigation.ts`，不要在业务组件里各自手写跳转判断。

Workspace 中的 `selectedConversationId`、`selectedAgentSessionId` 和 selected snapshot 只能作为恢复线索。只有它们的 conversation id 与当前 active conversation id 完全一致，且已经拿到真实 detail 或真实 messages snapshot 时，才允许驱动 composer、message list、final response markdown 和 trace 入口。conversation id 不一致、detail 仍在加载、或 snapshot 只有 trace/session stub 时，不得覆盖当前消息、pending user message、pending agent message、final response markdown 或右上角运行详情入口。

提交消息必须形成新的 selection epoch。`submitMessage()` 开始后，前端应先把 workspace selection 对齐到当前 route/active conversation，再插入本轮 pending user message 和 pending agent message；任何更早的 hydrate/select/list 响应如果属于旧 epoch，不能再覆盖本轮消息、composer 所属会话或 final response 渲染结果。向 `/v1/agent/chat` 发起请求时，payload 中的 `conversationId` 必须等于当前 route/active conversation id；如果本地 workspace selected session 仍指向旧会话，应先对齐本地状态，而不是把提交落到旧会话。

Session rail 的运行中状态以目标 conversation 的真实 in-flight turn 为准。某个会话正在执行 Code Agent 请求时，对应 `.session-tab` 保持原有单行标题和最近用户消息时间，但必须暴露 `data-running="true"` 并显示执行中动效；turn 进入 completed、failed 或 canceled 后清除动效并回到 `data-running="false"`。切换会话、恢复会话或取消请求不能改变 session tab 的标题来源：左侧只展示用户第一句话和以最后一条用户消息发送时间计算的更新时间。

关闭 Workbench 加载态问题时，浏览器验收应从当前 node/lane 的 public origin 进入，并在同源会话中对目标列表 API 施加短暂延迟，观察 in-flight DOM 和恢复 DOM。延迟窗口应能看到 `#session-tabs[data-loading="true"]`、`.session-tab` 数量为 0、`.loading-state` 和 `.loading-spinner` 存在、`#session-status` 为“加载中”；延迟结束后应恢复为真实数据或真实空态。延迟时间应低于前端请求超时，避免把接口 timeout 后的降级状态误判为恢复态。

关闭 Workbench 多轮卡死或刷新风暴问题时，浏览器验收必须从新的 Workbench session 开始，并显式选择目标 provider；不要复用未知状态的旧 session，否则旧 in-flight turn、失败 trace 或投影滞后可能把验收误判为 HTTP 409 或会话漂移。验收至少核对两层证据：turn-summary 中用户复现步骤全部 terminal 且 final response 可见；observe analyze 中没有 browser memory、Playwright responsiveness、CDP metrics timeout 这类红灯。若只有 amber 的 requestfailed、console 或 DOM lag，要结合 turn-summary 和 archive red 判断是否仍影响多轮连续工作，不得把非阻塞噪声当作卡死复发。
