diff --git a/.agents/skills/unidesk-webdev/SKILL.md b/.agents/skills/unidesk-webdev/SKILL.md index b94e370b..5a2b39dd 100644 --- a/.agents/skills/unidesk-webdev/SKILL.md +++ b/.agents/skills/unidesk-webdev/SKILL.md @@ -45,19 +45,24 @@ bun scripts/cli.ts hwlab nodes web-probe run --node D601 --lane v03 ```bash bun scripts/cli.ts hwlab nodes web-probe script --node D601 --lane v03 <<'JS' -export default async ({ page, gotoStable, screenshot, fetchJson, fetchApiMatrix, recordStep, safeEvaluate }) => { - await page.route('**/v1/agent/conversations**', route => setTimeout(() => route.continue(), 1000)); +export default async ({ gotoStable, screenshot, fetchJson, fetchApiMatrix, recordStep }) => { await gotoStable('/workbench'); - const workspaceResponse = await fetchJson('/v1/workbench/workspace?projectId=prj_hwpod_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/workspace?projectId=prj_hwpod_workbench', + '/v1/workbench/sessions?limit=5', + ...(sessionId ? [ + `/v1/workbench/sessions/${encodeURIComponent(sessionId)}`, + `/v1/workbench/sessions/${encodeURIComponent(sessionId)}/messages` + ] : []), '/auth/session' ]); - recordStep('workbench-ready', { url: page.url(), apiMatrixOk: apiMatrix.ok }); + recordStep('workbench-ready', { sessionsOk: sessionsResponse.ok, sessionId, apiMatrixOk: apiMatrix.ok }); return { - url: page.url(), - workspaceOk: workspaceResponse.ok, - workspace: workspaceResponse.body, + sessionsOk: sessionsResponse.ok, + sessionCount: sessionsResponse.body?.sessions?.length ?? sessionsResponse.body?.items?.length ?? null, + sessionId, screenshot: await screenshot('workbench.png') }; }; @@ -71,7 +76,9 @@ JS - web-probe 由 UniDesk CLI 从 YAML 声明的 bootstrap admin sourceRef 读取凭据并建立同源 `hwlab_session`;脚本不得自行读取、打印或复制 Web 登录凭据、cookie、token 或完整 API key。 - 脚本构造 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 conversationId、active tab、message card、trace row 或 workspace `selectedConversationId`。 +- 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 的通过条件。 +- 若 `web-probe run --fresh-session --message ...` 在当前 v0.3 上报 `session-not-selected`,且证据显示 helper 仍探测 `/v1/workbench/workspace` 或 `/v1/agent/conversations` 并得到 404,不要把它直接归因为投影超时或 Cloud Web 回归;改用 `web-probe script` 按当前 durable 端点提交/轮询/截图,并按 `$post-task` 去重登记工具链反馈。 - 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` 的延迟稳定值。 @@ -93,7 +100,7 @@ bun run e2e:workbench -- --project=chromium 运行面要求: - 在 issue/CLI 选中的 node/lane workspace 或其独立 worktree 上运行,不在 master server 本地运行。 -- 同源 fake-server 只提供正式 API 契约,覆盖 `/auth/*`、workspace、conversation list/detail、turn snapshot、trace event page、Workbench event stream、Performance summary 等当前用例所需端点。 +- 同源 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;关键路径可生成命名截图。 @@ -110,7 +117,7 @@ Workbench 至少覆盖: ## Fixture 采集与脱敏 -- 采集范围至少包括 workspace selection、conversation list、conversation detail、session/turn snapshot、trace event page、Workbench event stream 和 Web performance summary。 +- 采集范围至少包括 session list/detail/messages、session/turn snapshot、trace event page、Workbench event stream 和 Web performance summary;workspace 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。 @@ -121,19 +128,19 @@ 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 projection;trace event row 的 `completed` 只表示该 event/tool row 完成,不能终结 turn;message 文本为空只表示无可展示正文,不能生成占位 final response;list summary、session detail、turn snapshot、trace page 之间的差异只能作为 diagnostic,不得用优先级函数在读侧“选一个看起来合理的状态”。 - 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 脚本里做临时状态仲裁。 -- Session rail 会话集合只消费 `/v1/agent/conversations`;当前 conversation 消息本体只消费 `/v1/agent/conversations/{conversationId}`;turn 状态只消费 `/v1/agent/turns/{traceId}`;trace 阅读只消费 trace event page/result snapshot。不要让 workspace snapshot、列表摘要、localStorage、trace polling 或 result polling 互相覆盖。 +- 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/blocker,final response 展示区必须直接显示,不能只放在 trace rows 或详情面板中。 -- session 切换验收必须覆盖点击态和持久化恢复态:选择目标 session、确认 workspace `selectedConversationId` 持久化、刷新后同一 session 仍 active。 -- completed turn / Trace 重放 / deep link 验收必须用 fresh browser context 或等价 fresh login 直接打开 `/workbench/sessions/?projectId=`,不能只用同页 reload。 +- session 切换验收必须覆盖点击态和持久化恢复态:选择目标 session、确认 route/session list/detail/messages 归属同一 `sessionId`、刷新后同一 session 仍 active。 +- completed turn / Trace 重放 / deep link 验收必须用 fresh browser context 或等价 fresh login 直接打开 `/workbench/sessions/`,不能只用同页 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。 - Trace 实时性要用间隔采样,区分“加载中”和“思考中”;终态卡片不得继续保留“思考中”空态。 -- 用户反馈“当前 session 不刷新、切走再切回才刷新”时,证据必须成对采集:直达目标 URL、原地等待、切到其他 session、切回目标 session 的 DOM 截图;同时记录 `/v1/workbench/workspace`、conversation list with `includeConversationId`、conversation detail、turn snapshot 和 trace page 的状态。issue 正文要明确这是事件投影/重放问题,不能只登记成普通 deep link 或单接口 404。 +- 用户反馈“当前 session 不刷新、切走再切回才刷新”时,证据必须成对采集:直达目标 URL、原地等待、切到其他 session、切回目标 session 的 DOM 截图;同时记录 session list、session detail、messages、turn snapshot 和 trace event page 的状态。issue 正文要明确这是事件投影/重放问题,不能只登记成普通 deep link 或单接口 404。 ## Performance 页判定口径