Files
pikasTech-unidesk/.agents/skills/unidesk-webdev/references/full.md
T

289 lines
58 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.
---
name: unidesk-webdev
description: UniDesk Web 开发与浏览器验证技能。用户处理 UniDesk/HWLAB Cloud Web、Workbench、Performance 页、前端状态投影、Playwright、fake-server、web-probe、截图、响应式布局、Web E2E 回归或线上 Web bug 复测/提 issue 时使用。
---
# UniDesk WebDev
本技能是 UniDesk/HWLAB Web 开发、浏览器复测和 Playwright 回归的唯一操作面。需求真相源仍是 UniDesk OA 规格,例如 `project-management/PJ2026-01/specs/PJ2026-010401-web-workbench.md``PJ2026-01060505-workbench-performance.md`;本技能只规定如何开发、采集、复现和验收。
## P0 防分叉原则
- 单一权威入口:先按 issue/CLI 明确的 node + lane 解析 workspace、web-probe origin、public origin、namespace、sourceRef 和目标分支。没有明确目标时才读取受控 YAML;不得把 D601、G14、v0.2、v0.3、旧端口、ClusterIP 或本地 dev server 写成隐藏默认。
- 防分叉原则:Web/Workbench 修复必须先确定唯一 authority、唯一 API 契约和唯一状态投影入口;不得写成“当字段缺失/请求失败/状态不符/超时/刷新后,再改走另一个 endpoint、旧 workspace/conversation 模型、localStorage、trace polling、result polling、GET read-through repair、内部 manager、legacy route 或测试专用后门”。字段缺失、状态滞后或投影不完整时,修 schema、projector、read model 或正式 mutation 源头;代码里只能保留单一路径内的输入规范化、校验和错误暴露,不能用 `A ?? B``if missing then fallback`、兼容分支或双写双读把缺口遮住。
- 禁止状态竞争行为:不得在代码、API、reducer、read model、fake-server 或测试中保留两个事实源,再用“覆盖、压过、override、优先级、优先采用、以 A 为准否则 B、A 赢过 B”等仲裁规则决定展示状态;这就是条件分叉和竞争投影。必须先定义唯一投影对象、唯一归属字段和唯一生成时机,让 UI/API 只消费该投影对象的单一状态;其他原始字段只能作为审计输入、诊断字段或待收敛数据,不能参与展示状态仲裁。
- 严禁读侧推理:REST GET、SSE consumer、compat wrapper、CLI renderer、Web reducer/selectors、Trace renderer、fake-server 和测试断言都不能从 trace tail、message text、tool event、result cache、session summary、list row、workspace snapshot 或 localStorage 推断 turn/session/message 的 lifecycle、terminal、final response 或 running 状态。读侧只能读取唯一投影对象已经写好的字段;字段缺失、投影滞后或多字段矛盾时必须暴露 diagnostic/blocker 并修 projection writer/finalizer/schema,不得用 `result?.status ?? trace?.status`、最后一条 event `completed`、message fallback、session fallback、elapsed timeout 或 UI heuristic 补造事实。
- Sealed final response 不可被读侧诊断覆盖:assistant final response 一旦由唯一 durable projection 写入 terminal/sealed 字段,主消息正文、finalResponse、message status 和 turn terminal 就是 sealed 用户结果;turn polling、trace hydration、SSE gap、realtime timeout、transport close、compat wrapper error 或 read model lag 只能进入 trace detail、transport diagnostics、projection diagnostics、session health 或消息详情入口,不能替换主 timeline 正文。
- 无多路径、无 fallback:这是本技能的核心原则。Web、CLI、fake-server 和线上 web-probe 必须复用同一套 API 契约、同一 route/session/conversation/trace authority 和同一状态投影;不得为了让测试通过增加第二套 dispatcher、localStorage 真相、workspace snapshot 真相、内部 manager 调用、旧 lane 端口、raw request、临时兼容路径或“测试专用后门”。
- 严禁事后 repair / 0repairWeb、Workbench、fake-server 和 web-probe 不得通过 reload、切换 session、realign、`sessionRepair`、workspace selection repair、active tab repair、GET read-through、SSE gap repair、localStorage truth 或测试 helper 自动点击,把已经分裂的 route/session/message/trace 状态补成看起来正确。合法页面必须在首次进入、刷新、deep link、session 切换和 SSE 重连时按同一权威自然收敛;不一致必须暴露为 blocker/diagnostic,并修 schema、projector、read model、reducer 或 route authority 源头。允许 session-bound hydrate/gap-fill 只写目标 session 自己的 cache/read model,不得改变 active selection、URL 或当前消息区。
- Workbench GET 纯读投影:`GET /v1/workbench/*` 只能读取已持久化的 Workbench 投影事实并组装摘要;不得在 GET 中调用 AgentRun、Code Agent 或其他上游执行面做同步、补事件、修 trace、finalize terminal、改 running/completed 或写 message/session。投影滞后必须显式暴露 `projectionStatus``lastProjectedSeq``sourceRunId``blocker` 或等价诊断字段,并由后台 projector/finalizer 或显式受控 mutation 处理;刷新页面、切换 session 或打开详情不能成为事实推进入口。
- Workbench session lifecycle:空消息/无活动 session 的回收只能由后端 lifecycle GC、后台 finalizer 或显式受控 mutation 完成,TTL 和开关从选中 node/lane 的受控配置进入运行面;Web、fake-server 和 web-probe 不得靠隐藏 tab、前端 DELETE timer、GET read-through 或 deep link repair 清理。归档/删除后的 session deep link 必须观察同一 authority 的 404/archived diagnostic,不能补建或复活 session。
- Workbench 前端无破坏性投影权:删除 session、清空 active session、清空当前消息页、清空 tabs、把 composer 降成 `session_required`,或把 route session 标记为 not-found/archived/deleted,都是 lifecycle mutationWeb reducer/selectors、route hydrate、GET/list/detail/messages/SSE consumer、fake-server 和测试 helper 都不能由读侧失败、404、空列表、网络瞬断或 late response 触发这些 mutation。只有用户显式 mutation 成功,或后端 canonical lifecycle projection,能改变 session lifecycle。需求真相见 UniDesk OA `PJ2026-010401 Web工作台` 的“破坏性投影权”。
- fake-server 不是第二后端:它只按正式 API 契约重放脱敏 fixture 和必要边界变形。未 mock 的 `/auth/*``/v1/*``/health*` 请求应失败并暴露 path;不得访问 live Cloud API、AgentRun、HWPOD、数据库或 Kubernetes 作为通过条件。
- 真实数据优先:fixture seed 优先从目标 node/lane 的受控真实样本采集。合成 fixture 只补真实样本难以稳定覆盖的边界,并标明 `derivedFrom``syntheticReason`
- 原入口闭环:fake-server Playwright 用例负责可重复红灯和源码回归;线上 `web-probe` 负责同一 node/lane public origin 的 P4 原入口验收。二者不能互相替代。
- Master server 禁重型验证:不要在 master server 跑仓库级 check、Web build、Playwright/browser smoke 或镜像构建。HWLAB Web 验证走目标 node/lane workspace、k3s/Tekton、D601 runner 或受控 web-probe。
- 禁止裸写 PlaywrightUniDesk/HWLAB Web 复现、截图、DOM/API 采样、长程 Workbench 观测和线上 closeout 默认必须走 `web-probe run|script|observe`;不得直接写 `playwright-cli`、临时 Node Playwright 脚本或本地 browser daemon 作为正式证据入口。确有 web-probe 不覆盖的短生命周期需求时,必须说明例外原因,且可复用动作要回收进 web-probe。
## 工作流
1. 定位目标:确认 repo、node、lane、workspace、public origin 和目标 SPEC。HWLAB D601 v0.3 例子是 `/home/ubuntu/workspace/hwlab-v03` + `https://hwlab.pikapython.com`,但只能在 issue/CLI 指向该目标时使用。
2. 读规则:进入目标 workspace 前读取目标 `AGENTS.md`;涉及规格或测试设计时读取 UniDesk OA 对应 SPEC。
3. 线上复现:短动作用 `web-probe run|script`,长程 Workbench/session 观测用 `web-probe observe start|command|status|stop|analyze`;桌面版默认使用 `1920x1080` 视口,除非用户、issue 或 SPEC 明确指定其他尺寸;保存 `scriptSha256``runDir`、observer id、stateDir、截图名、截图 SHA、URL、DOM/API 摘要、prompt hash、trace/session/run id。
4. 建红灯:修 Workbench/Performance 用户可见 bug 前,在目标 HWLAB workspace 的 `web/hwlab-cloud-web` fake-server Playwright 套件中补确定性用例;fixture 从真实采集样本脱敏产生。
5. 修源码:保持状态读写单一路径。状态投影类修复优先收敛 server-state/reducer/projection,不在 UI 组件、trace polling、result polling 或 localStorage 中新增竞争事实。
6. 验证:先跑 fake-server Playwright 目标用例,再回到同一 node/lane public origin 用 web-probe 复测;多轮任务必须用同一个 observer/session 采样到终态。截图、report hash 和 analyze finding 作为证据回传,不进入源码仓库。
7. 写 issue/closeout:问题登记和收口必须写明目标合并分支、node/lane、fixture 来源、Playwright fake-server 用例、线上 web-probe 命令/脚本 SHA、截图 SHA 和剩余边界。
## HWLAB Web Probe
线上 Cloud Web DOM 验收优先使用受控入口:
```bash
bun scripts/cli.ts web-probe run --node D601 --lane v03
```
web-probe 入口分三类:
- `run`repo-owned 标准 DOM probe,适合固定 P4 验收和已有脚本。
- `script`:受控 Playwright 托管脚本,适合一轮 55 秒内完成的 DOM/API 断言、截图、route/intercept 和边界采样。
- `observe`:纯客户端长程观测,适合同一 Workbench session 多轮任务、realtime/projection 问题、长时间 trace/DOM/network 采样和无副作用报告生成。长程 Workbench 观测默认同时打开两个浏览器页面:control 页面只执行显式 `observe command` 用户动作,observer 页面只打开同一个 session 做被动观察,并默认每 3 分钟整页刷新一次同一 session,模拟用户离开后返回,用来抓多用户/多页面下同一 session 的投影差异、历史 trace 丢失、耗时跳变和 loading 差异。
工测优先级:Workbench trace 乱序、完成行位置、耗时不一致、时间跳变、final response flicker、session 不刷新、性能慢路径和多轮可靠性问题,默认先用 `observe` 采样并跑 `observe analyze`;不要把临时 Playwright spec、裸 DOM 调试脚本或单次 `script` 结果当作主要工测证据。`script/run` 只作为短路径断言、截图/API 摘要或 observe/analyze 后的补充复核。
需要 Playwright route/intercept、延迟 API、读取 in-flight DOM 或截图时仍使用受控 `web-probe script`,不要裸写 Playwright
```bash
bun scripts/cli.ts web-probe script --node D601 --lane v03 <<'JS'
export default async ({ gotoStable, screenshot, fetchJson, fetchApiMatrix, recordStep }) => {
await gotoStable('/workbench');
const sessionsResponse = await fetchJson('/v1/workbench/sessions?limit=5');
const firstSession = sessionsResponse.body?.sessions?.[0] ?? sessionsResponse.body?.items?.[0] ?? null;
const sessionId = firstSession?.sessionId ?? firstSession?.id ?? null;
const apiMatrix = await fetchApiMatrix([
'/v1/workbench/sessions?limit=5',
...(sessionId ? [
`/v1/workbench/sessions/${encodeURIComponent(sessionId)}`,
`/v1/workbench/sessions/${encodeURIComponent(sessionId)}/messages`
] : []),
'/auth/session'
]);
recordStep('workbench-ready', { sessionsOk: sessionsResponse.ok, sessionId, apiMatrixOk: apiMatrix.ok });
return {
sessionsOk: sessionsResponse.ok,
sessionCount: sessionsResponse.body?.sessions?.length ?? sessionsResponse.body?.items?.length ?? null,
sessionId,
screenshot: await screenshot('workbench.png')
};
};
JS
```
长程 Workbench 观测使用 `observe` 命令组:
```bash
bun scripts/cli.ts web-probe observe start --node D601 --lane v03 --target-path /workbench --sample-interval-ms 5000 --screenshot-interval-ms 60000 --command-timeout-seconds 55
bun scripts/cli.ts web-probe observe command webobs-xxxx --type newSession
bun scripts/cli.ts web-probe observe command webobs-xxxx --type selectProvider --provider codex-api
bun scripts/cli.ts web-probe observe command webobs-xxxx --type sendPrompt --text 'ping'
bun scripts/cli.ts web-probe observe command webobs-xxxx --type steer --text '继续观察当前 trace'
bun scripts/cli.ts web-probe observe command webobs-xxxx --type cancel
bun scripts/cli.ts web-probe observe command webobs-xxxx --type sendPrompt --text-stdin <<'EOF'
long prompt
EOF
bun scripts/cli.ts web-probe observe status webobs-xxxx --tail-lines 6
bun scripts/cli.ts web-probe observe collect webobs-xxxx --view turn-summary
bun scripts/cli.ts web-probe observe collect webobs-xxxx --view timeline --command-id cmd-xxxx
bun scripts/cli.ts web-probe observe collect webobs-xxxx --view trace-frame --trace-id trc_xxx --sample-seq 42
bun scripts/cli.ts web-probe observe stop webobs-xxxx
bun scripts/cli.ts web-probe observe analyze webobs-xxxx
bun scripts/cli.ts web-probe sentinel plan --node D601 --lane v03 --dry-run
bun scripts/cli.ts web-probe sentinel status --node D601 --lane v03
bun scripts/cli.ts web-probe sentinel image status --node D601 --lane v03
bun scripts/cli.ts web-probe sentinel image build --node D601 --lane v03 --dry-run
bun scripts/cli.ts web-probe sentinel control-plane plan --node D601 --lane v03 --dry-run
bun scripts/cli.ts web-probe sentinel control-plane status --node D601 --lane v03
bun scripts/cli.ts web-probe sentinel control-plane trigger-current --node D601 --lane v03 --dry-run
bun scripts/web-probe-sentinel-service.ts --node D601 --lane v03 --state-root .state/web-probe-sentinel-smoke --scheduler-disabled --once
```
`observe analyze` 的 duplicate final response 判定必须以 trace-frame 可见行事实为准。`observe collect --view trace-frame` 固定渲染的 `Final Response` 区块是 summary,不是第二条业务 assistant message;只有同一 trace-frame 中出现两个可见 assistant final rows 且内容重复时,才应报告 duplicate finding,并在证据中写明 `finalResponseSummaryBlockCounted=false`
`observe collect --view timeline` 用于 issue closeout 前的 artifact drill-down:它只读取现有 `control.jsonl``samples.jsonl``commands/{pending,processing,done,failed,abandoned}/*.json`,默认输出 bounded timeline、关键 metadata、脱敏 disclosure 和下一步命令。按 `--command-id``--turn``--trace-id``--sample-seq``--timestamp``--window-ms` 缩小窗口;需要完整原始内容时必须显式使用 `--raw``--view files --file ...`
项目管理 / MDTODO 页面同样优先使用 `observe`,不要退回一次性 Playwright 脚本。MDTODO 主动编辑验收必须把常见动作沉淀成 `observe command`,同一 observer 串联 source 配置、HWPOD probe/reindex、文件/任务选择、Rxx 树操作、编辑写回和 Workbench launch
```bash
bun scripts/cli.ts web-probe observe start --node D601 --lane v03 --target-path /projects/mdtodo --sample-interval-ms 5000 --screenshot-interval-ms 60000 --command-timeout-seconds 55
bun scripts/cli.ts web-probe observe command webobs-xxxx --type gotoProjectMdtodo
bun scripts/cli.ts web-probe observe command webobs-xxxx --type configureMdtodoHwpodSource --hwpod-id d601-f103-v2 --node-id node-d601-f103-v2 --root docs/MDTODO/
bun scripts/cli.ts web-probe observe command webobs-xxxx --type probeMdtodoSource
bun scripts/cli.ts web-probe observe command webobs-xxxx --type reindexMdtodoSource
bun scripts/cli.ts web-probe observe command webobs-xxxx --type closeMdtodoSourceConfig
bun scripts/cli.ts web-probe observe command webobs-xxxx --type selectMdtodoSource --source-id <opaque-source-id>
bun scripts/cli.ts web-probe observe command webobs-xxxx --type selectMdtodoFile --file-ref <opaque-file-ref>
bun scripts/cli.ts web-probe observe command webobs-xxxx --type selectMdtodoFile --filename <direct-mdtodo-file-name.md>
bun scripts/cli.ts web-probe observe command webobs-xxxx --type selectMdtodoTask --task-ref <opaque-task-ref-or-rxx>
bun scripts/cli.ts web-probe observe command webobs-xxxx --type openMdtodoReportPreview --task <rxx-or-task-ref> --link <report-label-or-path-fragment>
bun scripts/cli.ts web-probe observe command webobs-xxxx --type toggleMdtodoReportFullscreen --text toggle
bun scripts/cli.ts web-probe observe command webobs-xxxx --type editMdtodoTaskInline --task <rxx-or-task-ref> --field title --text 'web-probe interactive edit acceptance'
bun scripts/cli.ts web-probe observe command webobs-xxxx --type editMdtodoTaskInline --task <rxx-or-task-ref> --field body --text 'body updated through web-probe command'
bun scripts/cli.ts web-probe observe command webobs-xxxx --type editMdtodoTaskTitle --task <rxx-or-task-ref> --text 'web-probe interactive edit acceptance'
bun scripts/cli.ts web-probe observe command webobs-xxxx --type editMdtodoTaskBody --task <rxx-or-task-ref> --text 'body updated through web-probe command'
bun scripts/cli.ts web-probe observe command webobs-xxxx --type toggleMdtodoTaskStatus --task <rxx-or-task-ref> --status completed
bun scripts/cli.ts web-probe observe command webobs-xxxx --type addMdtodoSubTask --parent <rxx-or-task-ref> --title 'web-probe command subtask'
bun scripts/cli.ts web-probe observe command webobs-xxxx --type continueMdtodoTask --task <rxx-or-task-ref> --title 'web-probe sibling task'
bun scripts/cli.ts web-probe observe command webobs-xxxx --type deleteMdtodoTask --task <rxx-or-task-ref>
bun scripts/cli.ts web-probe observe command webobs-xxxx --type launchWorkbenchFromMdtodo --task <rxx-or-task-ref>
bun scripts/cli.ts web-probe observe collect webobs-xxxx --view project-mdtodo-summary
bun scripts/cli.ts web-probe observe analyze webobs-xxxx
```
`launchWorkbenchFromMdtodo` 是 Project Management 到 Workbench 的正式用户动作复现入口。传入 `--filename` / `--file-ref``--task` / `--task-ref` 时,runner 必须通过公开 source/file/task 控件完成选择;若 control 页停在 `/projects/mdtodo` 根路由,应先选择默认 source,再选择文件和任务。`source` 是 observe command provenance 字段,不能当成 MDTODO source id;需要指定 source 时只能使用显式 `--source-id`。显式文件或任务目标选不中时,命令必须失败并输出结构化 before/after project snapshot、目标摘要和选项样本,禁止静默选第一项或退回任意任务。
MDTODO → Workbench launch closeout 必须引用 command result 中的 `launchStatus``sessionId``workbenchUrl``chatStatus`、业务 trace、`otelTraceId` / `chatOtelTraceId`、文件/任务选择摘要和 control URL。`project-mdtodo-summary``LAUNCH ok/fail``OTEL` 计数是第一层汇总;若 analyzer 的 archive red 与当前 command result 冲突,以当前 command result、control 页面截图和最新 project summary 为准,并说明 archive red 来自旧样本或旧规则。
MDTODO 或 Project Management 的 Web 重写/布局 closeout 不能只引用组件 diff、PipelineRun 成功或一张默认截图。最小证据应按 SPEC 覆盖:默认深链任务正文可见、Source/File 下拉显示 direct MDTODO 文件名、Rxx 任务树为 outline 辅助视图、右侧报告栏可打开且不挤爆主工作区、报告全屏可读并可关闭。涉及报告渲染时,先用有 `reportLinkCount > 0` 的任务执行 `openMdtodoReportPreview`,再执行 `toggleMdtodoReportFullscreen`,并在 closeout 中记录 command id、`reportPreviewVisible``reportFullscreenVisible`、报告 deep link、截图 SHA 和 analyze report SHA。
`observe` 的 Project Management 采样可能同时包含 control 页和 observer 页。control 页是显式用户动作所在页面;observer 页用于被动对照和周期刷新。若 `project-mdtodo-summary``observe analyze` 的最新汇总被 observer 周期刷新、stop 命令或根路由样本带成 `SRC=0/FILES=0/TASKS=0`,不得直接推翻已完成 command result 和 control 页面截图;必须按 `pageRole`、command JSON、control URL、screenshot path/SHA 和相邻 samples 解释。反过来,若 control command 失败、command backlog 未清空或截图 URL 不在目标 source/file/task/report 深链上,也不能用 observer 的健康样本替代用户入口证据。
`observe command --type newSession` 是显式用户/control action:它通过当前 Workbench UI 点击 `#session-create` 创建新 session,等待 active session id 改变和 composer ready,并把前后 snapshot 写入 control log。它只能用于采样开始时建立目标 session,或作为用户明确的新建会话动作;不得在 route/session mismatch 后当作 repair 手段。
约束:
- `web-probe script` 不运行默认探针,必须通过 stdin heredoc 或 `--script-file <path>` 提供脚本;只需要 repo-owned 标准 DOM probe 时使用 `web-probe run`
- `web-probe run|script|observe start` 的默认 URL、browser proxy mode、observe/analyze 报警阈值和 project-management 采样/命令 allowlist 必须来自 `config/hwlab-node-lanes.yaml``webProbe`;需要排除公网/FRP/跨国 proxy 抖动时,在 YAML 里把目标 node/lane 的 `webProbe.defaultOrigin` 配成内部 Service ClusterIP origin,不要在命令行长期手写 `--url` 或裸 Playwright。
- 任何桌面版截图、视觉复测或布局采样没有显式尺寸要求时,默认视口必须是 `1920x1080`;若受控 CLI 暂无直接参数,应在脚本内调用正式 viewport helper 或等价页面上下文设置,并在 closeout 记录最终视口。
- `web-probe observe start` 默认是被动观测:记录 DOM 摘要、自然页面 request/response/requestfailed、截图和 performance 样本,不主动 fetch Workbench API、不切换 control session、不拦截路由、不调用 repair helper。长程 Workbench 观测必须保留 control/observer 双页面模型:control 页面执行显式 commandobserver 页面只同步到同一 session URL 后被动采样,并按默认 180000ms 周期整页刷新同一 session 来模拟用户往返;周期刷新只作用于 observer,不得改变 control active session 或作为通过条件。两页的 `pageRole``pageId``sampleGroupSeq` 必须进入样本和 analyzer 报表。任何 `newSession``selectProvider``sendPrompt``steer``cancel``goto``screenshot``mark``stop` 都必须通过 `observe command` 显式下发,并进入 `control.jsonl`;长 prompt 必须优先用 `sendPrompt --text-stdin``steer --text-stdin`,不要为了绕开 shell quoting 退回裸 Playwright 或临时脚本。MDTODO 高频操作也必须优先沉淀为 `observe command`;同类动作第二次出现时不要继续写临时 `web-probe script`
- `observe command --type steer``--type cancel` 是显式用户/control actionsteer 复用当前 Workbench composer 的运行中 turn 引导路径,cancel 复用同一 composer 主按钮的取消路径。二者必须进入 `control.jsonl`,不能用后端私有 API、AgentRun direct cancel 或测试后门替代。`configureMdtodoHwpodSource``probeMdtodoSource``reindexMdtodoSource``closeMdtodoSourceConfig``selectMdtodoSource``selectMdtodoFile``selectMdtodoTask``editMdtodoTaskTitle``editMdtodoTaskBody``toggleMdtodoTaskStatus``addMdtodoSubTask``continueMdtodoTask``deleteMdtodoTask``launchWorkbenchFromMdtodo` 也是显式用户/control action,只能使用页面公开 `data-*` id、正式按钮和 YAML 允许的自然 API;它们通过 public source/file/task id 与 Workbench 关联,不能读取内部 store、私有后端或把 MDTODO 页面包含进 Workbench。
- `observe collect --view turn-summary` 是第一层 CLI 阅读视图:只从 `samples.jsonl``control.jsonl` 和已有 `analysis/report.json` 按需渲染同一 session 的多 turn 摘要,包含用户消息 preview/hash、traceId、状态、耗时/最近更新时间、steer/cancel 标记和 Final Response 摘要。`observe collect --view trace-frame --trace-id <id> --sample-seq <n>` 是第二层 CLI 阅读视图:从同一采样帧渲染单帧 trace 文字截图,并固定输出 `Final Response` 区块。`observe collect --view project-summary|project-mdtodo-summary` 从同一 artifact 渲染项目管理 / MDTODO DOM 采样、Source/File/Task 计数、command/mutation 结果、Workbench launch、捕获到的 `x-hwlab-otel-trace-id` 和 OTel/Tempo drill-down 线索;project collect 的远端 payload 必须保持 bounded compact rows,由本地 renderer 生成表格,避免 `trans` stdout 截断后 JSON parse 失败。collect 视图不是采样器新增保存物,不构成第二事实源。
- `observe start/status/command/collect/analyze` 默认输出包含 `Wrapper contract` 区块;该区块证明 Web 哨兵只能 wrap 现有 observe CLI verb、现有 runner/analyzer 和既有 artifact contract,不新增第二套 Playwright runner、analyzer、状态机或私有 web-probe API。
- `web-probe sentinel plan|status` 只读取 `observability.webProbe.sentinel.enabled/configRefs` 和 owning YAML,渲染 redacted 配置引用图、文件 hash、缺失字段和跨 ref 冲突;`web-probe sentinel image|control-plane` 继续从 owning YAML 渲染 image、GitOps、Argo 和 manifest 计划,并在远端 publish job 接通前拒绝报告部署 mutation。它不启动浏览器、不读取 Secret 值、不保存采样结果,也不是第二套 runner/analyzer。真正的采样和判定仍以 `observe start|command|collect|analyze` artifacts 为准。
- Web 哨兵 public dashboard/origin 必须以 issue/SPEC/YAML 既定计划为准;当前 P6 计划沿用 `monitor.pikapython.com`,不要未经明确变更改成 `hwlab-monitor.pikapython.com` 或其他新域名。验证 report 时记录 `publicOrigin`,但不要把域名硬编码到 runner/analyzer 逻辑里。
- 验证 sentinel public dashboard **页面 DOM/截图**(区别于 CI/CD `sentinel status|plan|image|control-plane`)时,优先使用 `web-probe sentinel dashboard verify|screenshot --node <node> --lane <lane> --sentinel <id>`;该受控入口会从 YAML public exposure 进入 dashboard 页面,使用目标 node/lane 远程浏览器,并输出 bounded DOM/截图证据。`web-probe script` 的默认 origin 仍是 lane 的 HWLAB Cloud Web,不是 sentinel dashboard origin;只有受控 dashboard 命令暂时覆盖不到的一次性探索才用显式 `page.goto('<sentinel publicBaseUrl>/')`,同类动作第二次出现时必须沉淀回 sentinel dashboard 命令。
- `scripts/web-probe-sentinel-service.ts` 是 Web 哨兵 Pod entrypoint`--once` 只做 config/PVC/SQLite/scheduler/analyzer-command health 快照,`--scheduler-disabled` 仅用于本地服务健康冒烟,不能作为生产运行参数。HTTP 服务只提供 `/api/health``/api/status``/api/runs``/api/maintenance``/metrics` 和 redacted dashboard 外壳,底层采样仍只能经 observe CLI adapter。
- `trace-frame` 出现 `(无 trace rows;这是 blocker...)` 时,必须先看同一输出中的 `TRACE DIAGNOSTIC`:记录 pageRole/pageId、traceRows/turns/messages 数量、sampleTraceIds、尾部 traceRow/turn/message 归属。若目标 trace 的 turn/message/final 存在但 traceRows 全部属于旧 trace,应按 Workbench read model authority 分裂登记到架构/业务 issue(例:HWLAB #2124),不得把旧 traceRows 当作新 turn 通过证据,也不得让 analyzer 的聚合计数压过 CLI trace 视图。
- analyzer finding 不得压过 CLI `trace-frame` 人工视图。尤其 `trace-assistant-message-duplicates-final-response` 只有在 `trace-frame` 中同一 completed turn 可见多条相同 assistant final rows 时才按业务 bug 处理;如果 `trace-frame` 只有一条 assistant final row、后面固定 `Final Response` 区块正确且 API messages/turns 对齐,该 amber 归类为 analyzer 精度问题,应登记/修工具,不得阻止业务 closeout。
-`observe status` 显示 PID still alive 但 heartbeat/sample 不推进、`commands/pending/*.json` 不被消费,或 `observe stop --force` 只是继续排队 stop command,应先按 web-probe runner 工具缺陷处理(例:UniDesk #874),用 route 只读确认 PID/heartbeat 后清理进程;不要把 pending command、未触发的 cancel 或 runner stale 混入 Workbench 业务结论。
- `web-probe observe` 的 issue evidence 优先记录 observer id、stateDir、report JSON/Markdown SHA、samples/control/network/artifact 计数、routeSessionId、activeSessionId、prompt hash/textBytes、traceId、AgentRun runId/commandId、最终 status 和必要摘要;不要把 prompt 原文、assistant 大段正文、完整 stdout/stderr 或 provider payload 粘贴到 issue。
- 多轮 Workbench 采样必须证明同一个 `sessionId` 连续承载所有轮次;每轮至少记录 prompt hash、traceId、终态、最终回答摘要和性能/产物表。若 Web UI 投影卡住但 Code Agent/AgentRun result 已 terminal,应同时登记“执行终态”和“Workbench 投影未收敛”,不得用 `goto`、reload、切 session 或 result polling 把 UI 失败伪装成通过。
- `observe command --type sendPrompt` 是普通新 turn 路径,composer 主按钮应处于 `data-action=turn`;如果仍是 `steer``cancel`,必须作为工具/页面状态不一致失败暴露,不能点击后再等待错误接口。空输入状态下 submit disabled 可以为 true,不能把 disabled 当作新 turn 不可用的唯一依据;应先看 input present/enabled、warning absent 和 action=turn,再由正式填入 prompt 后提交。
- `observe analyze` 是离线分析,只读取 artifact JSONL 并写 `analysis/report.md``analysis/report.json`,不访问 Workbench API、不驱动浏览器。`observe start` 每次启动必须先把同一 stateDir 中已有的根目录 JSONL 轮转到带时间戳的 `archive/` 文件;`observe analyze` 默认只分析当前根目录 JSONL,不扫描历史 archive,只有显式指定 archive prefix 时才分析历史轮转窗口。报告必须输出采样点 vs 每个 turn 的总耗时/最近更新时间表、trace row 视觉顺序异常、terminal/轮次完成 row 是否最后、Code Agent 卡片耗时与 trace/轮次完成总耗时一致性、可见“加载中”的数量/归属/并发 owner/连续出现区间、DOM diagnostic/HTTP/console/requestfailed/runtime execution error 分组、page asset provenance segment、同源 API Resource Timing 分位表和超过 YAML `webProbe.alertThresholds` budget 的慢路径 finding;项目管理页还必须输出 DOM readiness、source/file/task 计数、缺失 public task ref、Workbench launch success/failure、captured OTel trace header、自然 project-management API 分组和超过 YAML `webProbe.projectManagement.slowApiBudgetMs` 的慢路径 finding。页面/API 加载、可见“加载中”、长连接打开耗时、turn timing 跳变、trace row 顺序、卡片耗时/轮次完成耗时一致性、session fallback 标题比例和项目管理 API 慢路径阈值只能改 YAML,不能在 analyzer/renderer 中写死。修复必须降低真实请求、投影、渲染或后端路径耗时,禁止为了减少“加载中”出现时间而提前展示未加载完的内容,也不能靠下游 retry/reload/fallback 掩盖。报告里的 `trace-row-order-nonmonotonic``trace-completion-row-not-last``round-completion-elapsed-mismatch``code-agent-card-duration-underreported``final-response-flicker``uncommanded-visible-state-change``mdtodo-workbench-launch-otel-trace-missing``project-management-api-slow`、session changed、network 503 等 finding 是排障线索;用于 closeout 时必须结合原始 session/trace/DOM 证据解释,避免把采样噪声直接当作业务结论。
- `sentinel validate --quick-verify` 超过 120s 是严重超时,必须保持 warning/red 并优先从 envreuse、git mirror、warm runner 复用、first tool execution、Workbench 投影和 observe/analyze 开销排查;不要通过提高 budget、减少轮数、放宽 analyzer 或绕过 CLI trace 视图来让场景变绿。quick-verify 等待每轮终态时应读取既有 sampler artifacts 和 bounded collect 视图,不能反复启动完整 collect 或新增第二份“trace 截图”保存来源。
- 自定义 `web-probe script` 仍运行在 UniDesk `trans` 60s 最外层短连接约束内;能在一轮内完成的 P4 验收优先把 `--command-timeout-seconds` 控制在 55 秒以内,并减少无界 selector/network 等待。确需等待更久时,改用 `web-probe run` 的异步 job/status 语义,或把动作拆成“提交/采样/截图/状态读取”多次短 probe。若输出出现 `UNIDESK_SSH_RUNTIME_TIMEOUT` 但同时恢复了 `reportPath``reportSha256`、screenshots 或 DOM steps,先按远端报告判断脚本/页面实际状态;最终关闭证据仍优先用一次未触发短连接超时的 bounded rerun。
- issue closeout 优先引用 `web-probe script` 输出的顶层 `issueEvidence``summary.issueEvidence`;只有需要展开调查时才粘贴 `probe.script.result``probe.steps` 或完整 `reportPath`,避免 stdout、summary 和 report 多层重复同一证据。
- stdin heredoc 与 `--script-file` 都按 ES module 加载,脚本必须导出 `export default async ({ page, gotoStable, recordStep, ... }) => { ... }`;不要在模块顶层直接写 `return`。失败为 `Illegal return statement``does not provide an export named default` 或 finalUrl 仍是 `about:blank` 且 stepCount=0 时,先按 probe 脚本入口误用处理,不要归因成 Cloud Web 行为失败。
- 自定义脚本需要主动失败时,优先返回 `{ ok: false, failedCondition: "..." }` 或抛出错误;兼容返回 `{ pass: false }``{ success: false }`,且 `{ ok: true, pass: false }` 仍按失败处理。`failedCondition``errorMessage``message``error``reason``summary` 会进入失败摘要;不要只把失败埋在普通业务字段里。
- OpenCode iframe 或 direct-host smoke 优先使用 typed `web-probe opencode-smoke --node <node> --lane <lane>`,不要重复手写一次性 Playwright 片段。该命令负责登录 HWLAB public origin、打开 `/opencode`、定位 iframe/direct origin、打开 project/composer、点击可见 `[data-action='prompt-submit']`、采集 EventSource,并断言 DOM assistant 文本与 update/finish/idle 终态。只有 typed 命令覆盖不到的额外 API 或 UI 细节才退回 `web-probe script`
- 自定义 OpenCode smoke 必须等到 OpenCode frame/direct origin 可用后再操作 composer;填入 prompt 后优先点击可见的 `[data-action='prompt-submit']`,不要把 Enter-only 当成稳定提交路径。关闭 OpenCode 对话类 issue 时,`web-probe script` 顶层 `ok` 不能只是“脚本没抛错”;必须断言 DOM body 出现最终 assistant 文本、没有残留 `Thinking`,并记录 EventSource 或等价事件探针看到 `message.part.updated``step-finish``session.idle`。手工 EventSource 探针可以证明 `/global/event` 消息到达,但不能替代 UI DOM 终态证据。
- web-probe 由 UniDesk CLI 从 YAML 声明的 bootstrap admin sourceRef 读取凭据并建立同源 `hwlab_session`;脚本不得自行读取、打印或复制 Web 登录凭据、cookie、token 或完整 API key。
- 需要禁用 `EventSource`、mock `Date`/clock、注入 preload hook 或修改浏览器启动前全局对象时,`page.addInitScript()` 必须在目标页面整页加载前注册,并用 `page.goto(new URL(path, origin).toString(), { waitUntil: "domcontentloaded" })` 进入目标 deep link;不要依赖 `gotoStable()` 复用当前 SPA 页面后再期待 init script 生效。若只在已加载页面上 route/block 请求,既有 SSE 或 store 状态可能继续更新,不能作为“无后端刷新依赖”的验收。
- 脚本构造 URL 时使用 `new URL(path, baseUrl).toString()`;不要拼出 `//v1/...`
- 先通过 `gotoStable``waitWorkbenchReady` 或等价导航进入目标 origin,再用 `fetchJson` 读同源 API;不要在 `about:blank` 上请求 `/v1/...``fetchJson` 返回包装对象 `{ ok, status, body, failureKind, ... }`,业务字段必须从 `.body` 读取,并同时记录 `ok/status/failureKind`;如果返回 `status=0` 且页面还不是目标 origin,先按 probe 脚本误用处理,不要归因成 Cloud API 或 Workbench 投影失败。
- Workbench 有 SSE/长轮询时不要用 `networkidle` 判断通过;用明确 DOM/API 条件,如 final URL、route sessionId、active tab、message card、trace row 或 durable session detail/messages。
- 当前 HWLAB v0.3 Workbench durable read model 的验收端点是 `/v1/workbench/sessions``/v1/workbench/sessions/{sessionId}``/v1/workbench/sessions/{sessionId}/messages``/v1/workbench/turns/{traceId}``/v1/workbench/traces/{traceId}/events`;提交 mutation 可用正式 `/v1/agent/chat` 产生真实 turn。`/v1/workbench/workspace` 只在当前 API 契约声明为选择/config 快照时采集,不作为消息或 trace authority`/v1/agent/conversations` 属于旧模型,不得作为 v0.3 Workbench durable projection 的通过条件。
- 需要稳定 running message、timing、SSE 或 projection 样本时,优先通过正式同源 mutation 创建样本:`POST /v1/agent/sessions` 建 session,再按前端同一路径提交 `/v1/agent/chat`,轮询 `/v1/workbench/sessions/{sessionId}/messages` 等待 durable projection。不要把历史 session 是否仍 running 当作验收前提,也不要用旧 conversation/workspace 端点造样本。验收结束后能取消的 turn 必须调用 `/v1/agent/chat/cancel` 清理,并在 issue evidence 只记录 trace/session、status、script/report SHA 和必要摘要,避免粘贴完整 provider payload。
- HWLAB v0.3 `fa2ad4845a111e0f9e86d473eb4204ed1b2b0a3c` 后,repo-owned `scripts/web-live-dom-probe.mjs` 的默认 `web-probe run --fresh-session` 已使用 durable Workbench session 端点,不应再依赖 `/v1/workbench/workspace``/v1/agent/conversations` 作为消息/trace authority。若 `session-not-selected` 复现,先确认目标 workspace 已包含该提交,再对 helper 做旧端点扫描;只有扫描仍命中旧端点时才按工具链回归处理。
- `web-probe run --fresh-session` 空消息路径的基线证据是 D601 v0.3 job `web-live-dom-probe-1781842424721-44066``freshSessionAligned=true``messageCount=0`、report SHA `d34c6765943e14bbd639b91bfc17139d582f670ed5cf409a957acd40fae7871f`、screenshot SHA `8d341b7c30293baa9be8f1e63e147ea0fdafbdbcfc43f0b05e4fb9f316ef996c`
- 若带 `--message` 的 fresh-session 路径已经达到 `freshSessionAligned=true``promptSubmitted=true``messageCount>0` 和 session API 200,但随后报 `trace-id-missing``Workbench turn is not visible to the current actor` 或 trace 面板同类不可见文案,应登记为 Workbench turn/trace actor visibility 问题,不再归因到 fresh-session 选择 helper 或旧端点。
- Playwright `page.evaluate` 只能传一个可序列化参数;多个值包成对象,或用 `safeEvaluate(fn, { a, b })`
- `safeEvaluate` 返回结构化包装对象;脚本断言前必须解包,例如 `const evaluated = await safeEvaluate(...); const dom = evaluated?.value ?? evaluated;``recordStep` 可以记录包装对象用于诊断,但行为断言不要直接读包装对象的业务字段,避免把已通过的 DOM 误判成 `undefined`
- 脚本用 `page.evaluate` 模拟滚动、resize、click 或派发 DOM event 后,若断言 Vue/React 绑定出的 `data-*`、ARIA、按钮状态或 scroll-follow 状态,必须先等待一次浏览器/框架更新,例如 `await wait(300)``page.waitForFunction(...)` 或下一次明确 DOM 条件;不要在同一个 evaluate 内写完事件就立刻读取状态,否则会把尚未 flush 的响应式状态误判为业务失败。验证用户滚动暂停/恢复时优先记录操作前后 scroll metrics 和 `data-following` 的延迟稳定值。
- `web-probe script` 顶层/default export 运行在 Node 上下文,浏览器全局 `window``document``location``CSS` 只在 `safeEvaluate`/`page.evaluate` 回调内可用。Node 侧 deep link 用 `const origin = new URL(page.url()).origin; await page.goto(new URL('/workbench/sessions/' + encodeURIComponent(id), origin).toString())`,或直接用 `gotoStable('/workbench/sessions/<id>')`;不要在 Node 侧写 `location.origin``CSS.escape(...)`,也不要用没有 baseURL 的 `page.goto('/relative/path')`。若失败为 `location is not defined``CSS is not defined` 或相对 URL invalid,先按脚本误用处理;工具提示改进跟踪见 [pikasTech/unidesk#479](https://github.com/pikasTech/unidesk/issues/479)。
- 脚本中用 `recordStep(name, data)` 保存关键 DOM/API partial evidenceAPI 批量探测优先用 `fetchApiMatrix(paths)`,单个 API 失败不应让后续证据丢失。
- deleted/archived/not-found session deep link 验收用 `web-probe script` 直接 `gotoStable('/workbench/sessions/<id>')`,再用同源 `fetchJson('/v1/workbench/sessions/<id>')`、messages 和 list API 断言 404/不在列表;不要调用要求 active session selection、composer readiness 或 workspace repair 的 helper 作为通过条件。
- 验证 Cloud Web runtime config 或 HTML 注入时,优先加 cache-busting query 和 `cache-control: no-cache`,并按层拆开判断:Deployment env、Pod 内 `127.0.0.1` HTML、公网原始 HTML、浏览器 DOM。不要只凭一次 web-probe DOM 缺字段就判定 rollout 失败;先确认是运行面未渲染、Pod 未滚动、边缘缓存,还是脚本读取层级错误。
- web-probe 只能观察、截图和断言,不得用 `sessionRepair``realignFreshSession`、自动点击 session、reload 循环或 workspace selection repair 作为通过条件。发现 `routeSessionId``activeSessionId``activeConversationId`、message session 或 trace session 不一致时,必须记录 mismatch 并失败;截图和 API 摘要是证据,不是修复动作。
- 失败证据至少保留 `failureKind``errorMessage``scriptSha256``runDir``lastUrl``lastScreenshot``probe.summary``reportPath`/`reportSha256`;默认失败截图是 `failure.png`
## Fake-Server Playwright
Workbench 回归入口在目标 HWLAB repo 的 `web/hwlab-cloud-web`
```bash
bun run e2e:workbench -- --project=chromium
```
运行面要求:
- 在 issue/CLI 选中的 node/lane workspace 或其独立 worktree 上运行,不在 master server 本地运行。
- 同源 fake-server 只提供正式 API 契约,覆盖 `/auth/*`、session list/detail/messages、turn snapshot、trace event page、Workbench event stream、Performance summary 等当前用例所需端点。
- 测试断言优先使用用户可见文本、ARIA role 和稳定 DOM 标识;不要通过内部 store、localStorage、数据库或 live API 直接判定通过。
- 非终态瞬时不变量必须用立即 DOM 快照断言:例如 running/pending 阶段要求 final response 为空时,先定位当前 agent card,再用 `locator.evaluate` / `page.evaluate` 直接读取 `.message-markdown.message-text` 数组并立刻判断;不要用会自动等待的 `expect(locator).toHaveCount(0)``not.toContainText` 或类似负向 locator 断言,否则测试可能等到 terminal DOM 替换后误通过,掩盖 running 阶段污染。
- 失败必须保留截图、Playwright trace 或等价 artifact;关键路径可生成命名截图。
Workbench 至少覆盖:
- session 切换后主工作区 loading 和目标 conversation 恢复;
- 刷新页面后以同一 message/turn/trace 标识还原 timeline
- SSE 断线、丢事件或重连后通过 REST snapshot 与 trace page 补洞;
- session tab、workspace card、message card、composer 主按钮、Trace detail 的 `running`/`completed`/`failed`/`canceled` 一致性;
- completed assistant 已 sealed 后,turn snapshot、trace event page、SSE 或 realtime diagnostic 失败不覆盖主消息正文,诊断只进入详情/感叹号/transport health
- Trace 阅读按事件顺序渲染可读 row,正确处理分页、终态、失败、自动展开和终态折叠;
- deep link 与普通点击 session 走同一 authority path
- Performance 页的摘要、错误态、刷新、移动端表格可达性和低基数脱敏展示。
## Fixture 采集与脱敏
- 采集范围至少包括 session list/detail/messages、session/turn snapshot、trace event page、Workbench event stream 和 Web performance summaryworkspace selection/config 只有在当前 API 契约仍声明该读模型时才采集。
- 产物必须记录 `capturedFrom``capturedAt`、schema/redaction 版本和 `valuesPrinted=false`
- 使用稳定伪 ID 映射保留 conversation、session、thread、turn、trace、message、sourceSeq 的关系。
- 删除或替换 `HWLAB_API_KEY`、cookie、Authorization header、DB DSN、provider token、真实用户身份、非公开 prompt、stdout/stderr 大段原文和完整 provider payload。
- 真实样本难以覆盖的延迟响应、SSE 断线、分页缺口、列表缺少当前选中项、可选字段异常、空集合和特定 HTTP 错误可以合成,但必须说明来源和原因。
## Workbench 判定口径
- Workbench API 的 GET 路径不是修复入口。若 session/messages/turn/trace 出现状态不一致,应登记为投影唯一性或投影延迟问题,修 backend projector/read model;不得用前端轮询、session 切换、reload、read-through sync 或 GET 内部修复把问题掩盖。
- Workbench 读侧不得推理生命周期。`turn.status``message.status``session.running``trace terminal``finalResponse``projectionStatus` 只能来自唯一 durable projectiontrace event row 的 `completed` 只表示该 event/tool row 完成,不能终结 turn;message 文本为空只表示无可展示正文,不能生成占位 final responselist summary、session detail、turn snapshot、trace page 之间的差异只能作为 diagnostic,不得用优先级函数在读侧“选一个看起来合理的状态”。
- Workbench sealed final response 必须与读侧诊断分仓。`messageProjection` 承载 role/status/text/finalResponse/sealedAt`traceDetail` 承载 events/page hydration/trace diagnostics`sessionStatus` 承载 rail/card 运行摘要,`transportDiagnostics` 承载 SSE/poll/hydration timeout 等可见性诊断;等价结构可以使用不同命名,但主消息投影和 diagnostic 不能竞争同一字段。`showMessageText()`、message diagnostic selector 和组件模板不得让 diagnostic 压过 sealed final response。
- Workbench trace 视觉顺序的唯一权威是 durable projection 分配的 `projectedSeq``projectedSeq` 必须由持久化 runtime store 在 trace 内全局递增分配,并按 `traceId + sourceEventId` 幂等复用;本地 `event.seq``sourceSeq`、事件到达顺序、时间戳、DOM 顺序和 renderer 输入顺序只能作为审计输入或源游标,不能直接派生视觉位置或参与仲裁。重复源事件不能分配新 `projectedSeq`;不同源事件即使局部 `event.seq` 重置为 1 也必须分配新的全局 `projectedSeq`,这个幂等性需要有最小单元测试覆盖。
- Workbench trace 时间字段的 source truth 是后端和 trace event page 暴露的 ISO/UTC 时间戳;后端 projector、durable read model 和共享 renderer 默认行为不得改成本地时区字符串。Web 展示北京时间或其他用户时区时,只能在 Vue/Web 渲染层从 runtime `displayTime` 注入 formatterCLI、CaseRun 和原始 trace API 仍显示/返回 UTC,不通过多字段仲裁、默认 fallback 或后端补写时区字符串修显示。
- Trace event page 检测到同一 trace 内历史 `projectedSeq` collision 时必须暴露 `projectionStatus=blocked``projectionHealth=degraded` 和稳定 blocker code,不能把损坏序列交给 CLI/Web renderer 继续展示。CLI/Web trace renderer 只能按 projection 已给定的位置一次渲染;不得通过 assistant 文本相等/包含、final response 匹配、尾部排序、terminal row 合并或 completion row 追加来事后修正视觉顺序。
- Workbench durable projection 验收必须覆盖同一 session/trace 的 list、detail、messages、turn、trace events 和 DOM session tab/message card;尤其要用 fresh browser 或 runtime 重启后的历史 terminal 样本证明 read model 能从 durable trace events 恢复终态。若内存 trace 仍显示 running 而 durable trace 已 terminal,应修唯一 read model 的投影对象,不得在 GET handler、前端 reducer 或 probe 脚本里做临时状态仲裁。
- HWLAB #1585/#1596/#1690 是 Workbench 投影禁令的固定判例:AgentRun 已 terminal 但 Workbench 停在旧 seq 时,不得用 GET/read-side 补 resultturn/card completed 但 TraceEventPage seq/range 不合法时,不得用 completed 状态压过 trace 不可读事实;Trace timeline 乱序或 terminal row 重排时,不得让 `sourceSeq`、局部 `event.seq`、输入顺序和 renderer 文本匹配多来源仲裁。合法修复是后台 projector/resumer 补 durable projection,在同一 trace event 快照内修正分页/顺序契约,或暴露 historical projection blocker;不得推断 lifecycle、补写 GET、切 session/reload 修页面、添加多来源优先级或在 renderer 做事后 repair。
- Session rail 会话集合只消费 `/v1/workbench/sessions`;当前 session detail 只消费 `/v1/workbench/sessions/{sessionId}`;消息本体只消费 `/v1/workbench/sessions/{sessionId}/messages`turn 状态只消费 `/v1/workbench/turns/{traceId}`trace 阅读只消费 `/v1/workbench/traces/{traceId}/events` 或正式 trace event page。不要让 workspace snapshot、列表摘要、localStorage、trace polling 或 result polling 互相覆盖。
- 空消息 session 回收验收必须同时证明:未超过 TTL 的空 session 保留,有消息、running/admitting、terminal result 或 active trace 的 session 不被回收;超过 TTL 的空 idle session 由后端 GC 归档/删除后,rail/list/detail/messages/deep link 都按同一 canonical lifecycle 状态表现。
- UI transient state 只保存 route、显式选择、composer draft、scroll、展开状态和临时交互状态;服务端事实进入 server-state/reducer/projection。
- running turn 必须同时反映在主任务区、composer 主按钮、session tab 和可取消入口;清空、切换、刷新不得把 running trace 隐藏成空态。
- 显式新建或选择 session 是 active selection 的 mutation authority`POST /v1/agent/sessions` 返回 201 或用户点击目标 session 后,URL、active tab、当前消息区和 composer 必须立即归属目标 session。目标 session detail/messages 慢只能表现为该目标 session 的 loading/empty 态;旧 session 的 list refresh、detail、turn、trace、SSE 迟到事件只能更新旧 session 自己的 cache/status,不得继续占据或夺回 active route、message area、composer/cancel 状态。
- 读侧没有破坏性投影权。`GET /v1/workbench/sessions`、session detail、messages、SSE、route hydrate 或 refreshSessions 的失败、404、空列表、网络错误和迟到响应,只能显示 loading/degraded/unknown/blocker 或保留上一份成功投影;不得 `forgetSession()``replaceActiveSessionSelection(null)`、清空 messages/tabs,或把 composer 改成 `session_required`。deleted/archived/not-found 必须来自后端 canonical lifecycle projection,或来自用户显式 DELETE mutation 成功后的展示更新。
- terminal failure、blocked、canceled 都是最终结果;若 API 返回可读 error/blockerfinal response 展示区必须直接显示,不能只放在 trace rows 或详情面板中。
- completed、failed、canceled、blocked 的 sealed final result 一旦显示,后续 polling/hydration/SSE/realtime 的 timeout、500、gap、close 和 lag 只能改变诊断区,不得把已显示 final response 闪回 `Code Agent 代理暂时无法连接上游``turn 超时无新活动``projection-resume:sync-failed` 或同类读侧错误态。
- session 切换验收必须覆盖点击态和持久化恢复态:选择目标 session、确认 route/session list/detail/messages 归属同一 `sessionId`、刷新后同一 session 仍 active。
- completed turn / Trace 重放 / deep link 验收必须用 fresh browser context 或等价 fresh login 直接打开 `/workbench/sessions/<sessionId>`,不能只用同页 reload。
- 0repair 验收必须用 fresh context 直达 session/deep link,或从当前 session 显式切换到目标 session;不得依赖测试 helper 选择、reload、切走再切回、`sessionRepair``realignFreshSession` 修正页面后再判通过。旧 session 的延迟 detail、turn、trace、SSE 或 refreshSessions 晚到,只能更新该 session cache/list,不能改变 active URL、active tab、current messages 或 composer。
- Workbench reducer/selectors/0repair 收敛类 closeout 必须做负向源码扫描并在 issue/PR 证据里写明结果;至少覆盖直接写服务端事实和旧 repair 名称,例如 `messages.value =``messages.value.push``sessions.value =``previousMessages``previousSessionId``restorePrevious``restoreMessagesTraceAuthority``repairWorkbenchMessagePage``findMessageTextFallback``projectionCatchup``PROJECTION_CATCHUP` 和组件侧 `workbench.messages`。这些扫描是辅助证据,不能替代 fake-server Playwright 与 public web-probe。
- Workbench 浏览器卡死、CDP metrics timeout 或内存持续增长时,优先排查并修应用侧真实压力源,例如请求风暴、未清理的 `PerformanceObserver` / `ResourceTiming` buffer、长列表同步扫描、低层 performance beacon 重试和高基数字段聚合;不得通过降低 web-probe 采样、改浏览器 baseline、自动刷新、fallback、隐藏红灯或放宽 analyzer 让 smoke 通过。使用 `browser-process.jsonl` 取证时按 `pageRole`/`pageId` 和单页面口径分析,预算阈值、baseline 扣除和 kill/red 策略只从 YAML/SPEC 读取,不能把 control+observer 双页面总 RSS 直接当成单页超标。
- Workbench RUM/API timing 采集必须保持低基数且有界:同源 API route label 要模板化或脱敏,不能把 session/trace/query 写入聚合标签;从浏览器 ResourceTiming enrich 请求耗时时,应单 pass 选择候选、读完后清理或轮转 resource timing buffer,并用最小单元测试覆盖 buffer 清理和 route label 归一化,避免多轮 Workbench 把诊断采样本身变成内存增长或主线程卡顿来源。
- Trace 实时性要用间隔采样,区分“加载中”和“思考中”;终态卡片不得继续保留“思考中”空态。
- 用户反馈“当前 session 不刷新、切走再切回才刷新”时,证据必须成对采集:直达目标 URL、原地等待、切到其他 session、切回目标 session 的 DOM 截图;同时记录 session list、session detail、messages、turn snapshot 和 trace event page 的状态。issue 正文要明确这是事件投影/重放问题,不能只登记成普通 deep link 或单接口 404。
## Performance 页判定口径
- `/performance` 只展示低基数、脱敏的 Workbench/RUM/API/long task 汇总;不得泄露 `conversationId``sessionId``traceId``runId`、prompt、assistant 正文、tool 参数、stdout/stderr、Secret 或用户身份。
- 单位以 API `unit` 为准;后端若以 seconds 存储,前端显示 ms 时必须只做一次换算。
- 移动端和窄屏断言要检查目标表格/按钮是否可见、可点击、layout rect 非零且可触达;不要只用 desktop selector readiness 代表所有 viewport。
- 刷新按钮应有明确 loading/error/empty 状态,重复点击不能制造 stuck loading、旧数据冒充新数据或未捕获 console/page error。
## UniDesk Frontend E2E
UniDesk 主前端交付门禁仍由 `bun scripts/cli.ts e2e run` 承担;本技能规定浏览器侧如何选取和解释验证。
- 先跑最小相关选择集,例如 `bun scripts/cli.ts e2e run --only frontend:pipeline`;目标检查通过后再跑完整 `bun scripts/cli.ts e2e run`。选择集是真执行,不是输出过滤。
- 浏览器必须打开 `config.json` / publicHost 派生的公开 frontend 或 dev frontend proxy;不要用 localhost、Docker internal URL 或后端 API 直调替代用户入口。
- 前端断言优先使用用户可见文本、ARIA role、稳定 DOM 标识和 React 控件状态;禁止用 raw JSON、内部 store、localStorage 或后端数据库绕过 UI。
- 高信息密度区域如 Pipeline 右侧栏、Trace timeline、详情抽屉、甘特图坐标、资源监控表和 Performance 面板,必须显式检查总高度、横向滚动条、关键控件可见可点、文本不重叠。
- 用户服务页必须等待真实后端数据,而不是只看页面骨架;Todo Note、OA Event Flow、Code Queue、FindJob、Pipeline、MET Nonlinear、ClaudeQQ 等页面的通过条件以当前 E2E check 名称和 issue scope 为准,不在文档里复制所有字段清单。
- raw JSON 只能在用户显式点击 `查看原始JSON` 后出现;默认页面必须把 JSON 渲染为表格、卡片、徽章、树、图或其他可读控件。
- 任何 frontend E2E 失败都应保留截图路径、resultPath、failedChecks 和关键 console/page error 摘要;不要把 missing browser/deps 当作 skip,按锁文件和目标执行面补依赖后继续。
## 受控 web-probe 代替裸 Playwright
UniDesk/HWLAB Web 工作不再把裸 Playwright 当作默认操作面。需要截图、DOM 断言、API matrix、route/intercept、SSE 降级、长程 session 采样、性能页面复测或 Workbench 多轮任务时,统一选择 `web-probe run|script|observe`
`web-probe observe analyze` 必须把 Workbench session 列表标题纳入默认采样与报告:可见列表中 `Session ses_...` fallback 标题超过一半时输出红灯 finding。修复方向必须让上游 session list projection 直接携带名称;点击详情后的下游补名、reload repair 或多来源仲裁不能作为修复。
`playwright-cli` 和临时 Node Playwright 脚本只允许作为 web-probe 尚未覆盖且一次性不可复用的诊断例外。例外使用前必须写明为什么 `web-probe run|script|observe|screenshot` 不适用;同类动作出现第二次就应补进 web-probe CLI 或 repo-owned probe。
需要把截图回传到本机时,优先用 `web-probe screenshot`,或长程观测中使用 `observe command --type screenshot --label <label>`,并在 issue 中引用截图 SHA、report SHA、observer id 和 stateDir。不要把本地 Playwright artifact 当作 HWLAB node/lane 原入口验收替代品。