merge: docs/conversation-intent-dual-tab

This commit is contained in:
weichen
2026-09-15 06:22:58 +00:00
21 changed files with 1596 additions and 31 deletions
@@ -1,11 +1,11 @@
---
title: "修复查询误触发写任务与任务状态刷新退化"
status: in_progress
status: done
created: 2026-09-13
updated: 2026-09-13
owner: Codex
branch: fix/conversation-intent-state
worktree: ".worktrees/fix-conversation-intent-state"
updated: 2026-09-15
owner: Cursor
branch: docs/conversation-intent-dual-tab
worktree: "-"
mutex: intent
---
@@ -34,24 +34,26 @@ mutex: intent
3. [x] 收紧写任务动作识别并修复 busy 错误分型
4. [x] 实现任务/租约状态刷新恢复与统一提示
5. [x] 实现命名文档的服务端鉴权只读总结(“总结”不止是不写入)
6. [ ] 执行前端、Go、PG 与真实 PI 双标签页验收
6. [x] 执行前端、Go、PG 与真实 PI 双标签页验收
- [x] Go、前端、构建、dev 重建与真实 PI 只读总结
- [x] 服务端 busy 预检不创建 run,并持久化无重试状态卡
- [ ] 浏览器刷新恢复与双标签页 busy 可见性
- [x] 浏览器刷新恢复与双标签页 busy 可见性(项目 6275;禁止项目 143)
## 验收标准
- [ ] [整改方案](../../remediation/2026-09-13-conversation-intent-runtime-state-remediation.md) 全部验收项有证据
- [ ] `CHAT-INTENT-STATE` 由 FAIL 更新为 PASS
- [ ] test-report 归档并回链
- [x] [整改方案](../../remediation/2026-09-13-conversation-intent-runtime-state-remediation.md) 全部验收项有证据
- [x] `CHAT-INTENT-STATE` 由 FAIL 更新为 PASS
- [x] test-report 归档并回链
## 相关链接
- Checklist: 本任务验收标准
- Handoff: 与 `fix/chat-history-refresh` 合并前复核 `App.tsx/chatWindow.ts`
- Handoff: [../../ops/handoffs/2026-09-15-conversation-intent-dual-tab.md](../../ops/handoffs/2026-09-15-conversation-intent-dual-tab.md)
- Test report: [../../test-reports/2026-09-13-conversation-intent-runtime-state-test-report.md](../../test-reports/2026-09-13-conversation-intent-runtime-state-test-report.md)
## 证据 / 备注
- Audit: [../../audit/2026-09-13-conversation-intent-runtime-state.md](../../audit/2026-09-13-conversation-intent-runtime-state.md)
- Remediation: [../../remediation/2026-09-13-conversation-intent-runtime-state-remediation.md](../../remediation/2026-09-13-conversation-intent-runtime-state-remediation.md)
- 双标签活验:项目 **6275** / `sess-p6275-u10` / `run_1789452602295343365`;产物 `.doc/test-reports/assets/2026-09-15-conversation-intent-dual-tab/`
- 未改写项目 143,未删除 `sess-p143-u9` 探针
+6 -4
View File
@@ -1,11 +1,11 @@
# 当前焦点
**更新**: 2026-09-15
**production_ready**: no(`CHAT-INTENT-STATE` 等无关 blocker 仍开放。文档账本运行时 UI Round20 **PASS**,不把 fallback change report 写成完整 PI,也不把本轮写成全仓可发布)
**production_ready**: no(CI 真实 PI 三工作流 E2E / 前端人工抽检仍开放。`CHAT-INTENT-STATE` 双标签 busy **PASS**。文档账本运行时 UI Round20 **PASS**,不把 fallback change report 写成完整 PI,也不把本轮写成全仓可发布)
## 焦点
1. **CHAT-INTENT**: [对话意图运行时状态](../audit/2026-09-13-conversation-intent-runtime-state.md)(`fix/conversation-intent-state` 已是 `dev` 祖先;浏览器双标签 busy 待 Cursor 写 handoff 后接管)
1. **CHAT-INTENT**: [对话意图运行时状态](../test-reports/2026-09-13-conversation-intent-runtime-state-test-report.md)(双标签 busy 已签:项目 **6275** / `DOC_BUSY` 单 run;不删 `sess-p143-u9` 探针)
2. **DOC-RELIABILITY**: [U11–U12 组合根与传输门禁](../archive/ops/2026-09-15-app-composition-u11-u12.md)(已合入 `dev@bf59cce`;rebuild digest `a8e3e940…`;schema 38)
3. **CHAT**: [对话历史刷新活验已补](../test-reports/2026-09-13-chat-history-refresh-test-report.md)(项目 6252 闲聊刷新同集 + guest 403;编制任务链仍开放)
@@ -24,7 +24,8 @@
| implementer_cursor | controlled-content-canonical-hash | fix/controlled-content-canonical-hash | .worktrees/fix-controlled-content-canonical-hash | docgov | done |
| Codex | project143-controlled-content-404-diagnosis | fix/project143-controlled-content-404-diagnosis | - | docgov-content-diagnosis | done |
| Codex | software-project-bootstrap-plan | feature/software-project-bootstrap-plan | - | workflow-design | done |
| Codex | conversation-intent-runtime-state | fix/conversation-intent-state | .worktrees/fix-conversation-intent-state | intent | in_progress |
| Codex | conversation-intent-runtime-state | fix/conversation-intent-state | .worktrees/fix-conversation-intent-state | intent | done |
| Cursor | conversation-intent-runtime-state | docs/conversation-intent-dual-tab | - | intent | done |
| Cursor | document-revision-wave1-6-implementation | feature/document-revision-management | - | docgov | done |
| Cursor-Auto | document-revision-runtime-ui03 | fix/document-revision-runtime-ui03 | - | docgov | done |
| Cursor-Auto | document-revision-sse-idempotency | fix/document-revision-sse-idempotency | - | docgov | done |
@@ -58,7 +59,7 @@
> 新任务开始前登记。`branch` 必须为 `feature/<slug>` 或 `fix/<slug>`,**禁止** `dev` / `test` / `main`。同一 `mutex` 同时仅一个 `in_progress`。
> workflow-plan 409 与治理鉴权 G1–G5 已收口(见 [test-report](../test-reports/2026-09-14-governance-action-level-authz-test-report.md))。禁止终止或改写项目 143。
> `document-reliability-pilot` U11–U12 已合入 `dev@bf59cce` 并 rebuild。禁止 worktree 内 `env-service`。项目 143 只读,禁止终止。intent 浏览器门下一步由 Cursor 写 handoff 接管,不覆盖 Codex worktree。
> `document-reliability-pilot` U11–U12 已合入 `dev@bf59cce` 并 rebuild。`CHAT-INTENT-STATE` 双标签 busy 已 PASS(项目 6275)。禁止 worktree 内 `env-service`。项目 143 只读,禁止终止。
## 集成槽位
@@ -87,6 +88,7 @@
| 发布缺口 | [gap-matrix](../remediation/gap-matrix.md) |
| 测试报告 | [test-reports/](../test-reports/) |
| Agent 手册 | [AGENTS.md](../../AGENTS.md) |
| 意图双标签 busy 交接 | [./handoffs/2026-09-15-conversation-intent-dual-tab.md](./handoffs/2026-09-15-conversation-intent-dual-tab.md) |
| 受控正文 hash 漂移 W1–W6 | [../ops/handoffs/2026-09-14-controlled-content-canonical-hash.md](../ops/handoffs/2026-09-14-controlled-content-canonical-hash.md) |
| DOCREV ADR | [../adr/2026-09-13-document-version-boundary.md](../adr/2026-09-13-document-version-boundary.md) |
| DOCREV 管理测试 | [../test-reports/2026-09-13-document-revision-management-test-report.md](../test-reports/2026-09-13-document-revision-management-test-report.md) |
@@ -0,0 +1,67 @@
---
title: "意图 mutex 从 Codex 转 Cursor:仅剩浏览器双标签门"
created: 2026-09-15
from: Codex
to: Cursor
status: closed
---
# 交接:对话意图运行时状态 — 浏览器双标签门
## 目标(下一位 Agent 应完成什么)
在 U11–U12 已合入并 rebuild 的 `:8080` 上,用**新建项目**补齐真实 PI + 双标签 busy / 刷新恢复证据。通过则关 `CHAT-INTENT-STATE` 并 archive;失败则从最新 `dev` 新开 `fix/conversation-intent-dual-tab`,**不要**在已合入的 `fix/conversation-intent-state` 上追加。
## 已完成
- Wave 0–4 服务端/单测已在 `dev`(`fix/conversation-intent-state` 是祖先)。
- 前端 5s 租约轮询:`web/src/App.tsx`。
- `DOC_BUSY` 预检:`internal/server/task_run_handlers.go`(不创建第二 run,`error_info.actions=[]`)。
- UI busy 文案:`BlockRenderer` / `DeckDrawer`「编辑中」。
- 只读总结真实 PI(用例 H)已签;项目 143 诊断污染未删。
## 未完成
- 浏览器刷新恢复与双标签租约可见性(报告用例 E)。
- 整改 Wave 3–4 未勾项:运行中刷新、busy 自动解除、并发写不新增 run 的**活验证据**。
## 认领 / 槽位状态
| 项 | 值 |
|----|-----|
| mutex 已释放 | no(`intent` 转 Cursor `in_progress`) |
| 集成槽位 holder | idle(U11–U12 已释放;本门禁不占槽,除非要合入 UI 补丁) |
| branch | 编码已在 `dev`;活验文档用 `docs/conversation-intent-dual-tab` |
| worktree | `.worktrees/fix-conversation-intent-state` **禁止** `env-service` |
> 用户已要求关掉 CURRENT 两行 `in_progress`,授权 Cursor 接管剩余验收。不覆盖 Codex worktree 未合并代码。
## 关键文件
| 路径 | 说明 |
|------|------|
| `.doc/ops/short-term/2026-09-13-conversation-intent-runtime-state.md` | 任务文 |
| `.doc/test-reports/2026-09-13-conversation-intent-runtime-state-test-report.md` | 用例 E 待补 |
| `.doc/remediation/2026-09-13-conversation-intent-runtime-state-remediation.md` | Wave 3–4 |
| `web/src/App.tsx` | 5s lease 轮询 |
| `internal/server/task_run_handlers.go` | DOC_BUSY 预检 |
## 如何验证
```bash
# 主仓已 rebuild 的 dev,禁止 worktree 内 env-service
curl -sf http://127.0.0.1:8080/ready
# 新建项目 + 隔离 session;禁止项目 143;禁止 sess-p143-u9 探针
```
## 风险 / Blocker
- 项目 143 只读:不终止、不改写、不打探针。
- 客户端在 lease 已投影后会本地拦截写请求(不发 POST);HTTP 409 须在轮询窗口内或走 API 第二通道取证。
- 诊断污染消息仍不擅自删除。
## 相关链接
- CURRENT: [CURRENT.md](../CURRENT.md)
- Task: [../short-term/2026-09-13-conversation-intent-runtime-state.md](../short-term/2026-09-13-conversation-intent-runtime-state.md)
- Test report: [../../test-reports/2026-09-13-conversation-intent-runtime-state-test-report.md](../../test-reports/2026-09-13-conversation-intent-runtime-state-test-report.md)
@@ -1,8 +1,8 @@
---
title: "对话意图、可恢复任务状态与文档占用可见性整改"
status: in_progress
status: done
created: 2026-09-13
updated: 2026-09-13
updated: 2026-09-15
---
# 对话意图、可恢复任务状态与文档占用可见性整改
@@ -30,11 +30,10 @@ updated: 2026-09-13
## 验收标准
- [x] “总结一下用户初始需求表”返回已有文档摘要,不创建 `task_runs`。
- [ ] “开始编写用户初始需求表”只创建一个 run;运行中再次写请求不创建失败 run。
- [ ] 任务失败后刷新,错误码、诊断摘要及允许动作与刷新前一致。
- [ ] 运行中刷新,聊天占位、阶段进度、文档 busy 标识和输入保护均恢复。
- [ ] 租约释放/任务终态后 UI 自动解除 busy,无需用户手动刷新。
- [x] “开始编写用户初始需求表”只创建一个 run;运行中再次写请求不创建失败 run。
- [x] 任务失败后刷新,错误码、诊断摘要及允许动作与刷新前一致。
- [x] 运行中刷新,聊天占位、阶段进度、文档 busy 标识和输入保护均恢复。
- [x] 租约释放/任务终态后 UI 自动解除 busy,无需用户手动刷新。
- [x] 测试使用专用项目/会话并在 teardown 清理;禁止 `debug/test` 探针进入非 fixture 会话。
## 相关链接
+3 -3
View File
@@ -2,7 +2,7 @@
**更新**: 2026-09-14
**分支**: dev
**production_ready**: no(`CHAT-INTENT-STATE` 等无关 blocker、CI 真实 PI 三工作流 E2E / 前端人工抽检未完成;文档账本运行时 UI Round20 已 PASS,不把 fallback 报告当成完整 PI)
**production_ready**: no(CI 真实 PI 三工作流 E2E / 前端人工抽检未完成。`CHAT-INTENT-STATE` 双标签 busy 已 PASS。文档账本运行时 UI Round20 已 PASS,不把 fallback 报告当成完整 PI)
**协作焦点**: [../ops/CURRENT.md](../ops/CURRENT.md)
**Agent 手册**: [AGENTS.md](../AGENTS.md) · [PROJECT.md](../PROJECT.md)
@@ -60,7 +60,7 @@
| WORKFLOW-PLAN-FROZEN-OPTIONAL-DUTIES | active 项目仍展示可编辑职责表单,409 提示在长表单视口外;现有规划又不支持非必需文档“三权全空”,且后端缺少职责人在任成员复核 | **PASS(设置页)** | [audit](../audit/2026-09-14-workflow-plan-409-frozen-and-optional-duties.md);[test-report](../test-reports/2026-09-14-workflow-plan-409-frozen-and-optional-duties-test-report.md)。项目 143 只读 frozen、A full+none 激活、B partial 不发 PUT、C 零治理拒绝、双标签转 frozen、成员无配置入口、刷新后 143 仍 frozen | 真实 PI 生成 full/none 并走审批/阶段不阻塞未纳入本轮页面矩阵,不作为本 gap 关闭的替代证据,另立项 |
| DOC-CONTENT-CANONICAL-HASH-DRIFT | 旧受控文档 bootstrap 裁剪 Markdown 双空格硬换行并将 canonical SHA 写入账本/DocGov,但未同步文件;正文读取把完整性冲突误报 404,lease/list sync 又放大并制造刷新后加载死锁 | **PASS(真实 UI)** | [audit](../audit/2026-09-14-project143-controlled-content-404.md);[UI report](../test-reports/2026-09-14-controlled-content-ui-acceptance.md)。代码已合 dev、schema 32、项目 143 定向 `migration_repair`;项目 143 `clean-run3` 与新项目 `full-chain-final` 主链及 restart 全部纯页面 PASS,目标 404=0 | [remediation](./2026-09-14-controlled-content-canonical-hash-remediation.md)。隔离 `TestReconcile_LegacyRepair_PG` 未执行,按用户“本轮只允许页面测试”的边界保留为工程测试债,不作为本轮 UI PASS 的替代证据 |
| CHAT-HISTORY-REFRESH | 工作流激活后刷新,对话正文被阶段过滤器清空,但分类数量与锚点仍按全量返回 | **PASS** | [test-report](../test-reports/2026-09-13-chat-history-refresh-test-report.md);活验 [live.json](../test-reports/assets/2026-09-15-chat-history-refresh-live/live.json)(项目 6252,window=categories=2,guest 403);代码:`message_scope.go`、`chat_history_auth.go`、goose 00026/00027 | 闲聊刷新已补。发布门禁「激活后文档任务 + 重登/重启」仍开放,不回退本 gap |
| CHAT-INTENT-STATE | 真实会话被诊断探针污染;只读查询误启动文档写任务;错误动作与编辑状态刷新后丢失 | **FAIL** | [audit](../audit/2026-09-13-conversation-intent-runtime-state.md);dev `sess-p143-u9` / task run / lease 现场 | [remediation](./2026-09-13-conversation-intent-runtime-state-remediation.md) |
| CHAT-INTENT-STATE | 真实会话被诊断探针污染;只读查询误启动文档写任务;错误动作与编辑状态刷新后丢失 | **PASS** | [audit](../audit/2026-09-13-conversation-intent-runtime-state.md);[test-report](../test-reports/2026-09-13-conversation-intent-runtime-state-test-report.md);双标签活验项目 **6275** / `sess-p6275-u10`(`live.json` + Docker 截图)。**不**删除 `sess-p143-u9` 历史探针 | [remediation](./2026-09-13-conversation-intent-runtime-state-remediation.md)。Wave 3–4 浏览器门已签;诊断污染消息仍不擅自删除 |
| ENV-RESTART-SOURCE | `docker compose restart` 只重启旧镜像,健康检查无法证明合入代码已部署 | **PASS** | `env-service.sh rebuild` + 真实摘要重测 | 集成冒烟统一使用 `dev rebuild` |
| DOCREV-RUNTIME-404 | revision-state 404;账本内存丢失;块保存/overlay 无效;agent 假报告 | **PASS** | [Round20 报告](../test-reports/2026-09-14-document-revision-runtime-ui-test-report.md);[audit](../audit/2026-09-13-document-revision-runtime-404.md) | 运行时 UI 已闭环。change report 仍为 fallback,不冒充 PI。历史失败目录不删 |
| DOCREV-PG-ID-REMAP | 首次生成 PG 已写 snapshot/candidate,commit 因内存误删 baseline 而 not found | **PASS** | [audit](../audit/2026-09-13-document-revision-pg-id-remap.md);Round19 归档 `schema31-round19-ui03-advisor-briefing-docrevision-not-found`(candidate id=175 / output snapshot 250,禁止写成 candidate 250);Round20 UI-03 V2/r0 | 已合入 `dev@b258ab8`;不忽略 ErrNotFound;不删证据 |
@@ -88,4 +88,4 @@
□ 前端:版本冲突刷新、终止 preview 人工抽检
```
**结论**: 文档账本运行时真实 UI Round20 **PASS**(见 [test-report](../test-reports/2026-09-14-document-revision-runtime-ui-test-report.md))。**发布 blocker 仍包括 `CHAT-INTENT-STATE`、CI 真实 PI 三工作流 E2E 与前端人工抽检**;`-race` 全包并行 TempDir flake 为测试基建残留。不得把 fallback change report 写成完整 PI 叙事。
**结论**: 文档账本运行时真实 UI Round20 **PASS**(见 [test-report](../test-reports/2026-09-14-document-revision-runtime-ui-test-report.md))。`CHAT-INTENT-STATE` 双标签 busy **PASS**(项目 6275)。**发布 blocker 仍包括 CI 真实 PI 三工作流 E2E 与前端人工抽检**;`-race` 全包并行 TempDir flake 为测试基建残留。不得把 fallback change report 写成完整 PI 叙事。
+2 -2
View File
@@ -15,7 +15,7 @@
| 模块 | 能力 | 状态 | 证据 | 缺口/后续 | mutex |
|------|------|------|------|-----------|-------|
| 意图识别 L1 | 18 类 Intent + 三分支消解 | partial | [system-design/intent-recognition](../system-design/intent-recognition/);[现场专项审计](../audit/2026-09-13-conversation-intent-runtime-state.md) | 明确写动作与只读总结已分流;完整 hard-negative/eval 集仍待补齐 | `intent` |
| 意图识别 L1 | 18 类 Intent + 三分支消解 | partial | [system-design/intent-recognition](../system-design/intent-recognition/);[现场专项审计](../audit/2026-09-13-conversation-intent-runtime-state.md);[运行态报告](../test-reports/2026-09-13-conversation-intent-runtime-state-test-report.md);[双标签活验](../test-reports/assets/2026-09-15-conversation-intent-dual-tab/live.json) | 明确写动作与只读总结已分流;双标签 `DOC_BUSY` 已签(项目 6275)。完整 hard-negative/eval 集仍待补齐 | `intent` |
| 对话 / ctxmgr | 多轮上下文、记忆、摘要 | partial | [system-design/multi-turn-dialogue](../system-design/multi-turn-dialogue/);[刷新恢复专项审计](../audit/2026-09-13-chat-history-refresh-disappears.md);[U10 端口化](../archive/ops/2026-09-15-chatlog-contextasm-u10.md);[U10 报告](../test-reports/2026-09-15-chatlog-contextasm-u10-test-report.md) | `CHAT-HISTORY-REFRESH` 闲聊刷新活验已补。U10 原地端口 **停止扩面**(未物理拆包)。语义摘要(D5 backlog) | `chat` |
| 智能体小队 | 角色-任务-工作区-黑板 | done | [system-design/agent-squad](../system-design/agent-squad/);[test 2026-09-07](../test-reports/2026-09-07-dynamic-squad-planning-test-report.md) | 非关键步骤降级(A5/A6 backlog) | `squad` |
| 工作流治理 | 三正式工作流 + 阶段门禁 | partial | [remediation/three-workflows](../remediation/2026-09-12-three-workflows-strict-governance-remediation.md);[test 2026-09-12](../test-reports/2026-09-12-three-workflows-governance-remediation-test-report.md);[冻结态/职责可选整改](../remediation/2026-09-14-workflow-plan-409-frozen-and-optional-duties-remediation.md);[Wave 5 页面矩阵](../test-reports/2026-09-14-workflow-plan-409-frozen-and-optional-duties-test-report.md);[职责-动作鉴权](../test-reports/2026-09-14-governance-action-level-authz-test-report.md) | 设置页/DutyMode + 动作级鉴权/未激活 202-blocked/关闭创建直通已落地。后续:none 文档真实 PI 审批长链路、CI 三工作流 E2E | `workflow` |
@@ -23,7 +23,7 @@
| IAM / 鉴权 | 项目级 API 鉴权 + RBAC | partial | [audit/authorization](../audit/authorization-matrix-v2.md);[刷新恢复专项审计](../audit/2026-09-13-chat-history-refresh-disappears.md) | 历史读取端点补统一会话 owner + 阶段授权;旧 API 410 标注(BUG-11 partial) | `authz` |
| 文档治理 | 法定文档生命周期 + 租约 + 受控正文读取 | done | [test docgov](../test-reports/2026-09-08-docgov-lifecycle-and-claiming-framework-test-report.md);[项目 143 正文 404 审计](../audit/2026-09-14-project143-controlled-content-404.md);[受控正文真实 UI 验收报告](../test-reports/2026-09-14-controlled-content-ui-acceptance.md);[UI-06 缓存竞争审计](../audit/2026-09-14-document-revision-refresh-cache-race.md);[整改](../remediation/2026-09-14-controlled-content-canonical-hash-remediation.md) | 生命周期/租约保留。W1–W6 + 审查修补 + UI hydrate 资格 + 晚到 list sync 修复均已合入 dev。项目 143 纯 UI 回归(clean-run3)与新项目全链路(main + post-restart)已全量真实页面验证通过(PASS) | `docgov` |
| 文档版本与修改认知 | 智能体版本边界、人工修订标记、任意版本比较、变更报告与负责人通知 | partial | [管理测试](../test-reports/2026-09-13-document-revision-management-test-report.md);[运行时 UI Round20](../test-reports/2026-09-14-document-revision-runtime-ui-test-report.md);[运行时审计](../audit/2026-09-13-document-revision-runtime-404.md);[ADR](../adr/2026-09-13-document-version-boundary.md) | **运行时 UI PASS**(6159 / V3/r2)。仍 `partial`:change report=`fallback`(真实 diff+lineage,非完整 PI 叙事)。系统级 `CHAT-INTENT` 见 gap-matrix | `docgov` |
| 前端工作台 | 三栏 + 块文档 + SSE | partial | [test workbench](../test-reports/2026-09-08-three-column-workbench-and-block-document-test-report.md);[现场专项审计](../audit/2026-09-13-conversation-intent-runtime-state.md) | 错误卡片/任务进度/文档 busy 状态刷新恢复与人工抽检待完成 | `web` |
| 前端工作台 | 三栏 + 块文档 + SSE | partial | [test workbench](../test-reports/2026-09-08-three-column-workbench-and-block-document-test-report.md);[现场专项审计](../audit/2026-09-13-conversation-intent-runtime-state.md);[双标签截图](../test-reports/assets/2026-09-15-conversation-intent-dual-tab/screenshots/tab-b-busy.png) | 错误卡片/任务进度/文档 busy 刷新恢复已有项目 6275 证据。前端人工抽检仍开放 | `web` |
| PI Runner / 配置链 | pi-agent + MiniMax 真实链路 | done | [test piagent](../test-reports/2026-09-13-codex-audit-p0-remediation-test-report.md);[container-env](../test-reports/2026-09-13-container-env-configuration-test-report.md) | — | `pi-agent` |
| 三环境运维 | dev/test/main + 共享 KB | done | [PROJECT.md](../../PROJECT.md) §4;四环境 healthy | — | `infra` |
| CI / E2E | PG 集成 + 真实 PI E2E | partial | [gap-matrix BUG-08](../remediation/gap-matrix.md);[test pg-ci](../test-reports/2026-09-12-pg-ci-soak-e2e-test-report.md);[运行时 UI Round20](../test-reports/2026-09-14-document-revision-runtime-ui-test-report.md) | CI `TEST_DATABASE_URL` 隔离库仍缺;文档运行时真实 UI 已按 UI_PHASE 分文件且 post-restart 断言最终 A1 正文。`-race` 全包并行 TempDir flake 为基建残留 | `ci` |
@@ -59,9 +59,18 @@
### 用例 E:真实 PI + 浏览器刷新/双标签页
- **Input**: 真实 PI 只读总结已执行;浏览器刷新/双标签页待执行
- **Actual Output**: 只读总结见用例 H;刷新和 busy 可见性尚无浏览器证据
- **Evaluation**: PARTIAL
- **Input**: 新建项目 `INTENT双标签-061002`(id **6275**,**禁止 143**)/ `sess-p6275-u10`;Tab A `POST /api/v1/tasks/runs`「请开始编写用户初始需求表」真实 `pi-agent`;租约出现后 Tab B 再 POST;浏览器双标签打开同一项目
- **Actual Output**:
- health/ready schema **38**、`agent_backend=pi-agent`
- Tab A `202` `run_1789452602295343365` status=running;2ms 内 `lease_holder_type=squad`
- Tab B HTTP **409** `DOC_BUSY`,响应无 `run_id`;PG `task_runs` 仅 1 行
- 聊天窗 assistant `error_info.code=DOC_BUSY` 且 `actions=[]`,另有 `task_status=running` 占位;再次 GET window 同集
- 真实 PI **completed**(约 280s,质量 100);租约在终态探测后 **1ms** 已空(无需客户端刷新)
- 终态后再 GET window:DOC_BUSY 卡仍 `actions=[]`,无重试动作
- 浏览器(Docker Playwright,宿主机缺 libatk):双标签可见「本文档正由【项目助手】编辑/生成中」、聊天 `DOC_BUSY`、发送按钮 disabled、刷新后仍无重试。截图 `assets/2026-09-15-conversation-intent-dual-tab/screenshots/`
- **Evaluation**: **PASS**
- **性能与耗时**: API 活验 281s;Docker UI 4.4s。产物:`assets/2026-09-15-conversation-intent-dual-tab/live.json`、`playwright-docker.json`
- 诊断污染:`sess-p143-u9` **未**写入、未删除历史探针
### 用例 F:dev 集成冒烟
@@ -106,4 +115,4 @@ git diff --check PASS
## 七、 测试结论与后续优化建议 (Conclusion & Next Steps)
代码级、构建门禁及真实 PI 只读总结已通过;原句不会创建任务或租约。最终 dev 镜像重建后 `/ready` 为 HTTP 200;只读复核确认用户原会话最大消息 ID 仍为 567、隔离 fixture 会话/消息/run 均为 0、project 143 无有效文档租约。尚不能标记发布完成:浏览器刷新恢复、双标签页租约可见性与并发写“不新增第二 run”仍需补录。真实会话中的诊断消息与误启动产物只记录证据,不在未获确认时删除或回滚。
代码级、构建门禁、真实 PI 只读总结与 **新建项目双标签 busy** 已通过。项目 **6275** / `run_1789452602295343365`:并发写 409 `DOC_BUSY` 不新增第二 run;刷新后 `actions=[]`;PI 终态后租约自动解除。浏览器双标签截图见 `assets/2026-09-15-conversation-intent-dual-tab/screenshots/`。真实会话 `sess-p143-u9` 中的诊断消息与误启动产物只记录证据,不在未获确认时删除或回滚。`production_ready` 仍为 no(CI 三工作流 E2E / 前端抽检等其它 blocker)。
@@ -0,0 +1,500 @@
#!/usr/bin/env python3
"""Isolated dual-tab DOC_BUSY live check against rebuilt dev. Never touches project 143."""
from __future__ import annotations
import json
import os
import subprocess
import time
import urllib.error
import urllib.request
from datetime import datetime
BASE = os.environ.get("BASE_URL", "http://127.0.0.1:8080").rstrip("/")
OWNER_USER = os.environ.get("OWNER_USERNAME", "zhanggong")
OWNER_PASS = os.environ.get("OWNER_PASSWORD", "123456")
REVIEWER_USER = os.environ.get("REVIEWER_USERNAME", "lizhuguan")
APPROVER_USER = os.environ.get("APPROVER_USERNAME", "wangzong")
ASSETS = os.environ.get(
"LIVE_ASSETS",
os.path.dirname(os.path.abspath(__file__)),
)
OUT = os.environ.get("LIVE_OUT", os.path.join(ASSETS, "live.json"))
STATE = os.path.join(ASSETS, "state.json")
FORBIDDEN_PROJECT = 143
def req(method, path, token=None, body=None, timeout=180):
data = None
headers = {"Accept": "application/json"}
if body is not None:
data = json.dumps(body, ensure_ascii=False).encode()
headers["Content-Type"] = "application/json"
if token:
headers["Authorization"] = f"Bearer {token}"
headers["X-Session-Token"] = token
r = urllib.request.Request(BASE + path, data=data, headers=headers, method=method)
t0 = time.time()
try:
with urllib.request.urlopen(r, timeout=timeout) as resp:
raw = resp.read().decode()
parsed = json.loads(raw) if raw.strip() else None
return resp.status, parsed, raw, int((time.time() - t0) * 1000)
except urllib.error.HTTPError as e:
raw = e.read().decode()
try:
parsed = json.loads(raw) if raw.strip() else None
except json.JSONDecodeError:
parsed = None
return e.code, parsed, raw, int((time.time() - t0) * 1000)
def login(username, password):
status, parsed, raw, ms = req("POST", "/api/v1/auth/login", body={"username": username, "password": password})
if status >= 400 or not parsed or not parsed.get("token"):
raise SystemExit(f"login failed for {username}: {status} {raw[:240]}")
user = parsed.get("user") or {}
return parsed["token"], int(user.get("id") or 0), ms
def flatten_window(body):
out = []
for day in (body or {}).get("days") or []:
for m in day.get("messages") or []:
out.append(m)
return out
def assistant_error_cards(messages):
cards = []
for m in messages:
if m.get("role") != "assistant":
continue
meta = m.get("metadata") or {}
err = meta.get("error_info") if isinstance(meta, dict) else None
if not isinstance(err, dict):
continue
cards.append({
"id": m.get("id"),
"code": err.get("code") or err.get("error_code"),
"actions": err.get("actions") if err.get("actions") is not None else [],
"task_status": meta.get("task_status"),
"content": (m.get("content") or "")[:160],
})
return cards
def running_placeholders(messages):
found = []
for m in messages:
if m.get("role") != "assistant":
continue
meta = m.get("metadata") or {}
status = meta.get("task_status") if isinstance(meta, dict) else None
if status in ("running", "pending", "streaming"):
found.append({"id": m.get("id"), "task_status": status, "content": (m.get("content") or "")[:120]})
return found
def workflow_leases(docs):
leases = []
for d in docs or []:
if d.get("lease_holder_type"):
leases.append({
"doc_id": d.get("doc_id"),
"title": d.get("title"),
"lease_holder_type": d.get("lease_holder_type"),
"lease_holder_id": d.get("lease_holder_id"),
"lease_expires_at": d.get("lease_expires_at"),
})
return leases
def count_runs(project_id):
sql = (
"SELECT run_id, status, session_id FROM task_runs "
f"WHERE project_id = {int(project_id)} ORDER BY created_at"
)
cmd = [
"docker", "exec", "shuimu-context-pg-dev",
"psql", "-U", "app", "-d", "agent_system_dev", "-t", "-A", "-F", "\t",
"-c", sql,
]
p = subprocess.run(cmd, capture_output=True, text=True)
rows = []
for line in (p.stdout or "").splitlines():
line = line.strip()
if not line:
continue
parts = line.split("\t")
if len(parts) >= 3:
rows.append({"run_id": parts[0], "status": parts[1], "session_id": parts[2]})
return p.returncode, rows, (p.stderr or "")[:300]
def write_json(path, obj):
os.makedirs(os.path.dirname(path), exist_ok=True)
with open(path, "w", encoding="utf-8") as f:
json.dump(obj, f, ensure_ascii=False, indent=2)
f.write("\n")
def main():
suffix = datetime.now().strftime("%H%M%S")
cases = []
t0 = time.time()
health_s, health, _, health_ms = req("GET", "/health")
ready_s, ready, _, _ = req("GET", "/ready")
cases.append({
"id": "health",
"verdict": "PASS" if health_s == 200 and ready_s == 200 and (health or {}).get("agent_backend") == "pi-agent" else "FAIL",
"duration_ms": health_ms,
"output": {
"schema_version": (health or {}).get("schema_version"),
"agent_backend": (health or {}).get("agent_backend"),
},
})
owner_token, owner_id, login_ms = login(OWNER_USER, OWNER_PASS)
reviewer_token, reviewer_id, _ = login(REVIEWER_USER, OWNER_PASS)
approver_token, approver_id, _ = login(APPROVER_USER, OWNER_PASS)
cases.append({
"id": "login",
"verdict": "PASS" if owner_id > 0 else "FAIL",
"duration_ms": login_ms,
"output": {"owner_id": owner_id, "reviewer_id": reviewer_id, "approver_id": approver_id},
})
proj_name = f"INTENT双标签-{suffix}"
st, proj, raw, ms = req("POST", "/api/v1/projects", owner_token, {
"name": proj_name,
"device_type": "超声软组织手术吸引设备",
"intended_use": "intent dual-tab busy live",
"target_market": "中国 NMPA",
})
project_id = int(proj["id"]) if isinstance(proj, dict) and proj.get("id") else 0
if project_id == FORBIDDEN_PROJECT:
raise SystemExit("refusing to use project 143")
cases.append({
"id": "create_project",
"verdict": "PASS" if st in (200, 201) and project_id > 0 and project_id != FORBIDDEN_PROJECT else "FAIL",
"http": st,
"duration_ms": ms,
"output": {"project_id": project_id, "name": proj_name},
})
if project_id <= 0:
raise SystemExit(f"create project failed: {st} {raw[:240]}")
member_specs = (
(reviewer_id, "reviewer"),
(approver_id, "approver"),
)
for uid, duty in member_specs:
ms_s, _, ms_raw, _ = req("POST", f"/api/v1/projects/{project_id}/members", owner_token, {
"user_id": uid,
"project_role": "member",
"document_duty": duty,
})
if ms_s >= 400:
# POST 可能不接受 duty;改为先加成员再 PUT
add_s, _, add_raw, _ = req("POST", f"/api/v1/projects/{project_id}/members", owner_token, {
"user_id": uid,
"project_role": "member",
})
if add_s >= 400:
raise SystemExit(f"add member {uid}: {ms_s}/{add_s} {ms_raw[:200]} {add_raw[:200]}")
put_s, _, put_raw, _ = req("PUT", f"/api/v1/projects/{project_id}/members", owner_token, {
"user_id": uid,
"project_role": "member",
"document_duty": duty,
})
if put_s >= 400:
raise SystemExit(f"put member duty {uid}: {put_s} {put_raw[:240]}")
sess = f"sess-p{project_id}-u{owner_id}"
req("POST", "/api/v1/chat/sessions", owner_token, {
"session_id": sess,
"title": proj_name,
"user_id": owner_id,
"project_id": project_id,
})
default_s, plan, default_raw, _ = req(
"GET",
f"/api/v1/projects/{project_id}/workflow-plan/default?workflow_key=main&version=0",
owner_token,
)
if default_s != 200 or not isinstance(plan, dict):
raise SystemExit(f"default plan failed: {default_s} {default_raw[:240]}")
for stg in plan.get("stages") or []:
for dt in stg.get("doc_tasks") or []:
dt["duties"] = {
"author_user_id": owner_id,
"reviewer_user_id": reviewer_id,
"approver_user_id": approver_id,
}
save_s, _, save_raw, _ = req("PUT", f"/api/v1/projects/{project_id}/workflow-plan", owner_token, {
"plan": plan,
"expected_plan_revision": 0,
"expected_selection_version": 1,
})
prev_s, preview, prev_raw, _ = req("POST", f"/api/v1/projects/{project_id}/activation-preview", owner_token, {})
activate_s, act, act_raw, _ = (0, None, "", 0)
if prev_s == 200 and isinstance(preview, dict):
activate_s, act, act_raw, _ = req("POST", f"/api/v1/projects/{project_id}/workflow-instances/activate", owner_token, {
"confirmation_token": preview.get("confirmation_token"),
"expected_project_version": preview.get("expected_project_version"),
"expected_plan_revision": preview.get("expected_plan_revision"),
})
cases.append({
"id": "activate",
"verdict": "PASS" if save_s == 200 and activate_s == 200 else "FAIL",
"http": activate_s,
"output": {"save_http": save_s, "preview_http": prev_s, "activate_http": activate_s},
})
if activate_s != 200:
raise SystemExit(f"activate failed: {activate_s} {act_raw[:300]} save={save_s} {save_raw[:200]}")
_, runs_before, _ = count_runs(project_id)
write_prompt = "请开始编写用户初始需求表"
run_s, run, run_raw, run_ms = req("POST", "/api/v1/tasks/runs", owner_token, {
"session_id": sess,
"client_turn_id": f"intent-tab-a-{suffix}",
"user_message": write_prompt,
"task_key": "intake",
"project_id": project_id,
"project_name": proj_name,
"device_type": "超声软组织手术吸引设备",
"intended_use": "intent dual-tab busy live",
"author_id": owner_id,
"author_name": "张工",
"reviewer_id": reviewer_id,
"reviewer_name": "李主管",
"approver_id": approver_id,
"approver_name": "王总监",
"target_market": "中国 NMPA",
}, timeout=30)
run_id = (run or {}).get("run_id") if isinstance(run, dict) else None
cases.append({
"id": "tab_a_start_write",
"verdict": "PASS" if run_s in (200, 202) and run_id else "FAIL",
"http": run_s,
"duration_ms": run_ms,
"output": {"run_id": run_id, "status": (run or {}).get("status") if isinstance(run, dict) else None},
})
if not run_id:
raise SystemExit(f"tab A write failed: {run_s} {run_raw[:400]}")
lease_wait_s = 0
leases = []
docs = []
lt0 = time.time()
for _ in range(60):
ds, docs, _, _ = req("GET", f"/api/v1/projects/{project_id}/documents?category=workflow", owner_token)
leases = workflow_leases(docs if isinstance(docs, list) else [])
lease_wait_s = int((time.time() - lt0) * 1000)
if leases:
break
time.sleep(1)
cases.append({
"id": "tab_b_lease_visible",
"verdict": "PASS" if leases else "FAIL",
"duration_ms": lease_wait_s,
"output": {"leases": leases, "doc_count": len(docs) if isinstance(docs, list) else 0},
})
if not leases:
gs, gb, _, _ = req("GET", f"/api/v1/tasks/runs/{run_id}", owner_token, timeout=30)
write_json(OUT, {"overall": "FAIL", "reason": "lease never appeared", "run": gb, "docs": docs, "cases": cases})
raise SystemExit(f"lease never appeared; run={gb}")
busy_s, busy, busy_raw, busy_ms = req("POST", "/api/v1/tasks/runs", owner_token, {
"session_id": sess,
"client_turn_id": f"intent-tab-b-{suffix}",
"user_message": write_prompt,
"task_key": "intake",
"project_id": project_id,
"project_name": proj_name,
"device_type": "超声软组织手术吸引设备",
"intended_use": "intent dual-tab busy live",
"target_market": "中国 NMPA",
"author_id": owner_id,
"reviewer_id": reviewer_id,
"approver_id": approver_id,
}, timeout=30)
busy_code = (busy or {}).get("error_code") if isinstance(busy, dict) else None
has_run_id = isinstance(busy, dict) and "run_id" in busy and busy.get("run_id")
_, runs_after_busy, count_err = count_runs(project_id)
cases.append({
"id": "tab_b_doc_busy",
"verdict": "PASS" if busy_s == 409 and busy_code == "DOC_BUSY" and not has_run_id and len(runs_after_busy) == len(runs_before) + 1 else "FAIL",
"http": busy_s,
"duration_ms": busy_ms,
"output": {
"error_code": busy_code,
"has_run_id": bool(has_run_id),
"runs_before": runs_before,
"runs_after": runs_after_busy,
"count_err": count_err,
"body_snip": (busy_raw or "")[:400],
},
})
win_s, win, _, win_ms = req("GET", f"/api/v1/chat/messages/window?session_id={sess}&days=7", owner_token)
msgs = flatten_window(win)
cards = assistant_error_cards(msgs)
running = running_placeholders(msgs)
busy_cards = [c for c in cards if c.get("code") == "DOC_BUSY" and c.get("actions") == []]
write_json(os.path.join(ASSETS, "window-before-refresh.json"), win)
cases.append({
"id": "busy_card_before_refresh",
"verdict": "PASS" if win_s == 200 and busy_cards and (running or run_id) else "FAIL",
"http": win_s,
"duration_ms": win_ms,
"output": {"busy_cards": busy_cards, "running": running, "msg_count": len(msgs)},
})
win2_s, win2, _, _ = req("GET", f"/api/v1/chat/messages/window?session_id={sess}&days=7", owner_token)
msgs2 = flatten_window(win2)
cards2 = assistant_error_cards(msgs2)
running2 = running_placeholders(msgs2)
write_json(os.path.join(ASSETS, "window-after-refresh.json"), win2)
same_actions = [c.get("actions") for c in cards] == [c.get("actions") for c in cards2]
cases.append({
"id": "refresh_same_set",
"verdict": "PASS" if win2_s == 200 and same_actions and busy_cards and assistant_error_cards(msgs2) else "FAIL",
"http": win2_s,
"output": {
"busy_cards_after": [c for c in cards2 if c.get("code") == "DOC_BUSY"],
"running_after": running2,
"same_actions": same_actions,
},
})
state = {
"project_id": project_id,
"project_name": proj_name,
"session_id": sess,
"run_id": run_id,
"owner_id": owner_id,
"forbidden_project": FORBIDDEN_PROJECT,
"phase": "busy",
}
write_json(STATE, state)
observe_cmd = os.environ.get("OBSERVE_CMD")
observe = {"skipped": True}
if observe_cmd:
env = os.environ.copy()
env.update({
"INTENT_DUAL_TAB": "1",
"INTENT_PROJECT_NAME": proj_name,
"INTENT_PROJECT_ID": str(project_id),
"INTENT_ASSETS": ASSETS,
"OWNER_USERNAME": OWNER_USER,
"OWNER_PASSWORD": OWNER_PASS,
})
ot0 = time.time()
op = subprocess.run(observe_cmd, shell=True, cwd=os.environ.get("WEB_DIR", ""), env=env, capture_output=True, text=True)
observe = {
"skipped": False,
"returncode": op.returncode,
"duration_ms": int((time.time() - ot0) * 1000),
"stdout_tail": (op.stdout or "")[-2000:],
"stderr_tail": (op.stderr or "")[-1500:],
}
write_json(os.path.join(ASSETS, "playwright-observe.json"), observe)
cases.append({
"id": "browser_observe",
"verdict": "PASS" if observe.get("skipped") or observe.get("returncode") == 0 else "FAIL",
"output": {
"skipped": observe.get("skipped"),
"returncode": observe.get("returncode"),
"duration_ms": observe.get("duration_ms"),
"has_stdout": bool(observe.get("stdout_tail")),
},
})
final_status = None
poll_ms = 0
pt0 = time.time()
for _ in range(72):
gs, gb, _, _ = req("GET", f"/api/v1/tasks/runs/{run_id}", owner_token, timeout=30)
if gs == 200 and isinstance(gb, dict):
final_status = gb.get("status") or gb.get("Status")
if str(final_status).lower() in (
"completed", "failed", "cancelled", "interrupted",
"pending_author_confirmation", "quality_failed", "blocked_pending_governance",
):
break
time.sleep(10)
poll_ms = int((time.time() - pt0) * 1000)
clear_ms = 0
leases_after = leases
ct0 = time.time()
auto_cleared = False
for i in range(12):
ds, docs, _, _ = req("GET", f"/api/v1/projects/{project_id}/documents?category=workflow", owner_token)
leases_after = workflow_leases(docs if isinstance(docs, list) else [])
clear_ms = int((time.time() - ct0) * 1000)
if not leases_after:
auto_cleared = True
break
time.sleep(5)
cases.append({
"id": "tab_a_terminal_and_lease_clear",
"verdict": "PASS" if final_status and auto_cleared else "FAIL",
"duration_ms": poll_ms + clear_ms,
"output": {
"final_status": final_status,
"poll_ms": poll_ms,
"lease_clear_ms_after_terminal_poll": clear_ms,
"auto_cleared_without_client_reload": auto_cleared,
"leases_after": leases_after,
},
})
win3_s, win3, _, _ = req("GET", f"/api/v1/chat/messages/window?session_id={sess}&days=7", owner_token)
write_json(os.path.join(ASSETS, "window-after-terminal.json"), win3)
cards3 = assistant_error_cards(flatten_window(win3))
still_empty_actions = all(c.get("actions") == [] for c in cards3 if c.get("code") == "DOC_BUSY")
cases.append({
"id": "busy_card_survives_terminal_refresh",
"verdict": "PASS" if win3_s == 200 and still_empty_actions and any(c.get("code") == "DOC_BUSY" for c in cards3) else "FAIL",
"http": win3_s,
"output": {"busy_cards": [c for c in cards3 if c.get("code") == "DOC_BUSY"]},
})
_, runs_final, _ = count_runs(project_id)
cases.append({
"id": "single_run_final",
"verdict": "PASS" if len(runs_final) == 1 and runs_final[0]["run_id"] == run_id else "FAIL",
"output": {"runs": runs_final},
})
state["phase"] = "terminal"
state["final_status"] = final_status
write_json(STATE, state)
overall = "PASS" if all(c["verdict"] == "PASS" for c in cases if c["id"] != "browser_observe") else "FAIL"
if any(c["id"] == "browser_observe" and c["verdict"] == "FAIL" for c in cases):
overall = "FAIL"
result = {
"overall": overall,
"duration_ms": int((time.time() - t0) * 1000),
"project_id": project_id,
"project_name": proj_name,
"session_id": sess,
"run_id": run_id,
"forbidden_project_untouched": True,
"cases": cases,
"browser_observe": observe,
}
write_json(OUT, result)
print(json.dumps({"overall": overall, "project_id": project_id, "run_id": run_id, "out": OUT}, ensure_ascii=False))
raise SystemExit(0 if overall == "PASS" else 1)
if __name__ == "__main__":
main()
@@ -0,0 +1,204 @@
{
"overall": "FAIL",
"duration_ms": 281149,
"project_id": 6275,
"project_name": "INTENT双标签-061002",
"session_id": "sess-p6275-u10",
"run_id": "run_1789452602295343365",
"forbidden_project_untouched": true,
"cases": [
{
"id": "health",
"verdict": "PASS",
"duration_ms": 6,
"output": {
"schema_version": 38,
"agent_backend": "pi-agent"
}
},
{
"id": "login",
"verdict": "PASS",
"duration_ms": 42,
"output": {
"owner_id": 10,
"reviewer_id": 11,
"approver_id": 12
}
},
{
"id": "create_project",
"verdict": "PASS",
"http": 201,
"duration_ms": 19,
"output": {
"project_id": 6275,
"name": "INTENT双标签-061002"
}
},
{
"id": "activate",
"verdict": "PASS",
"http": 200,
"output": {
"save_http": 200,
"preview_http": 200,
"activate_http": 200
}
},
{
"id": "tab_a_start_write",
"verdict": "PASS",
"http": 202,
"duration_ms": 11,
"output": {
"run_id": "run_1789452602295343365",
"status": "running"
}
},
{
"id": "tab_b_lease_visible",
"verdict": "PASS",
"duration_ms": 2,
"output": {
"leases": [
{
"doc_id": "doc_proj6275_intake",
"title": "用户初始需求收集表",
"lease_holder_type": "squad",
"lease_holder_id": "run_1789452602295343365",
"lease_expires_at": "2026-09-15T07:10:02.305032Z"
}
],
"doc_count": 1
}
},
{
"id": "tab_b_doc_busy",
"verdict": "PASS",
"http": 409,
"duration_ms": 6,
"output": {
"error_code": "DOC_BUSY",
"has_run_id": false,
"runs_before": [],
"runs_after": [
{
"run_id": "run_1789452602295343365",
"status": "running",
"session_id": "sess-p6275-u10"
}
],
"count_err": "",
"body_snip": "{\"detail\":{\"doc_id\":\"doc_proj6275_intake\",\"holder_type\":\"squad\",\"holder_id\":\"run_1789452602295343365\",\"expires_at\":\"2026-09-15T07:10:02.305032737Z\",\"reason\":\"文档【用户初始需求收集表】正在由 squad(run_1789452602295343365) 编辑中,当前请求未创建新任务\"},\"error\":\"document_busy\",\"error_code\":\"DOC_BUSY\",\"message\":\"文档【用户初始需求收集表】正在由 squad(run_1789452602295343365) 编辑中,当前请求未创建新任务\"}\n"
}
},
{
"id": "busy_card_before_refresh",
"verdict": "PASS",
"http": 200,
"duration_ms": 3,
"output": {
"busy_cards": [
{
"id": 2322,
"code": "DOC_BUSY",
"actions": [],
"task_status": "failed",
"content": "文档【用户初始需求收集表】正在由 squad(run_1789452602295343365) 编辑中,当前请求未创建新任务"
}
],
"running": [
{
"id": 2320,
"task_status": "running",
"content": "任务执行中…"
}
],
"msg_count": 4
}
},
{
"id": "refresh_same_set",
"verdict": "PASS",
"http": 200,
"output": {
"busy_cards_after": [
{
"id": 2322,
"code": "DOC_BUSY",
"actions": [],
"task_status": "failed",
"content": "文档【用户初始需求收集表】正在由 squad(run_1789452602295343365) 编辑中,当前请求未创建新任务"
}
],
"running_after": [
{
"id": 2320,
"task_status": "running",
"content": "任务执行中…"
}
],
"same_actions": true
}
},
{
"id": "browser_observe",
"verdict": "FAIL",
"output": {
"skipped": false,
"returncode": 1,
"duration_ms": 675,
"has_stdout": true
}
},
{
"id": "tab_a_terminal_and_lease_clear",
"verdict": "PASS",
"duration_ms": 280110,
"output": {
"final_status": "completed",
"poll_ms": 280109,
"lease_clear_ms_after_terminal_poll": 1,
"auto_cleared_without_client_reload": true,
"leases_after": []
}
},
{
"id": "busy_card_survives_terminal_refresh",
"verdict": "PASS",
"http": 200,
"output": {
"busy_cards": [
{
"id": 2322,
"code": "DOC_BUSY",
"actions": [],
"task_status": "failed",
"content": "文档【用户初始需求收集表】正在由 squad(run_1789452602295343365) 编辑中,当前请求未创建新任务"
}
]
}
},
{
"id": "single_run_final",
"verdict": "PASS",
"output": {
"runs": [
{
"run_id": "run_1789452602295343365",
"status": "completed",
"session_id": "sess-p6275-u10"
}
]
}
}
],
"browser_observe": {
"skipped": false,
"returncode": 1,
"duration_ms": 675,
"stdout_tail": "EdgeUpdateLaunchServicesPreferredVersion --enable-features=CDPScreenshotNewSurface --allow-pre-commit-input --disable-hang-monitor --disable-ipc-flooding-protection --disable-popup-blocking --disable-prompt-on-repost --disable-renderer-backgrounding --disable-updater-scheduler --force-color-profile=srgb --metrics-recording-only --no-first-run --password-store=basic --use-mock-keychain --no-service-autorun --export-tagged-pdf --disable-search-engine-choice-screen --unsafely-disable-devtools-self-xss-warnings --edge-skip-compat-layer-relaunch --disable-infobars --disable-search-engine-choice-screen --disable-sync --enable-unsafe-swiftshader --headless --hide-scrollbars --mute-audio --blink-settings=primaryHoverType=2,availableHoverTypes=2,primaryPointerType=4,availablePointerTypes=4 --no-sandbox --user-data-dir=/tmp/playwright_chromiumdev_profile-klGxjP --remote-debugging-pipe --no-startup-window\n - <launched> pid=3847953\n - [pid=3847953][err] /home/weichen/.cache/ms-playwright/chromium_headless_shell-1243/chrome-headless-shell-linux64/chrome-headless-shell: error while loading shared libraries: libatk-1.0.so.0: cannot open shared object file: No such file or directory\n - [pid=3847953] <gracefully close start>\n - [pid=3847953] <kill>\n - [pid=3847953] <will force kill>\n - [pid=3847953] exception while trying to kill process: Error: kill ESRCH\n - [pid=3847953] <process did exit: exitCode=127, signal=null>\n - [pid=3847953] starting temporary directories cleanup\n - [pid=3847953] finished temporary directories cleanup\n - [pid=3847953] <gracefully close end>\n\n\n Error Context: ../.doc/test-reports/assets/2026-09-14-controlled-content-ui-acceptance/playwright-output-main/conversation-intent-dual-t-ac365-oad-and-keeps-empty-actions-chromium/error-context.md\n\n 1 failed\n [chromium] › e2e/conversation-intent-dual-tab.spec.ts:36:3 › conversation intent dual-tab busy › tab B shows busy without reload and keeps empty actions \n",
"stderr_tail": "(node:3847921) Warning: The 'NO_COLOR' env is ignored due to the 'FORCE_COLOR' env being set.\n(Use `node --trace-warnings ...` to show where the warning was created)\n"
}
}
@@ -0,0 +1,23 @@
{
"verdict": "PASS",
"driver": "docker mcr.microsoft.com/playwright:v1.63.0-noble --network host",
"project_id": 6275,
"project_name": "INTENT双标签-061002",
"session_id": "sess-p6275-u10",
"forbidden_project": 143,
"duration_ms": 4400,
"assertions": [
"banner 当前项目 INTENT双标签-061002",
"编辑器条 本文档正由【项目助手】编辑/生成中",
"聊天卡 DOC_BUSY 且无重试按钮",
"发送按钮 disabled(输入保护)",
"刷新后 DOC_BUSY 仍在且无重试"
],
"screenshots": [
"screenshots/tab-a-running.png",
"screenshots/tab-b-busy.png",
"screenshots/tab-b-after-write.png",
"screenshots/tab-b-after-reload.png"
],
"notes": "宿主机 Playwright Chromium 缺 libatk,改用官方镜像。双标签共享同一登录上下文。API 409/单 run 证据见 live.json(真实 PI 编制约 280s 后 completed,lease 自动清除)。"
}
@@ -0,0 +1,4 @@
{
"posts": [],
"project": "INTENT双标签-061002"
}
@@ -0,0 +1,7 @@
{
"skipped": false,
"returncode": 1,
"duration_ms": 675,
"stdout_tail": "EdgeUpdateLaunchServicesPreferredVersion --enable-features=CDPScreenshotNewSurface --allow-pre-commit-input --disable-hang-monitor --disable-ipc-flooding-protection --disable-popup-blocking --disable-prompt-on-repost --disable-renderer-backgrounding --disable-updater-scheduler --force-color-profile=srgb --metrics-recording-only --no-first-run --password-store=basic --use-mock-keychain --no-service-autorun --export-tagged-pdf --disable-search-engine-choice-screen --unsafely-disable-devtools-self-xss-warnings --edge-skip-compat-layer-relaunch --disable-infobars --disable-search-engine-choice-screen --disable-sync --enable-unsafe-swiftshader --headless --hide-scrollbars --mute-audio --blink-settings=primaryHoverType=2,availableHoverTypes=2,primaryPointerType=4,availablePointerTypes=4 --no-sandbox --user-data-dir=/tmp/playwright_chromiumdev_profile-klGxjP --remote-debugging-pipe --no-startup-window\n - <launched> pid=3847953\n - [pid=3847953][err] /home/weichen/.cache/ms-playwright/chromium_headless_shell-1243/chrome-headless-shell-linux64/chrome-headless-shell: error while loading shared libraries: libatk-1.0.so.0: cannot open shared object file: No such file or directory\n - [pid=3847953] <gracefully close start>\n - [pid=3847953] <kill>\n - [pid=3847953] <will force kill>\n - [pid=3847953] exception while trying to kill process: Error: kill ESRCH\n - [pid=3847953] <process did exit: exitCode=127, signal=null>\n - [pid=3847953] starting temporary directories cleanup\n - [pid=3847953] finished temporary directories cleanup\n - [pid=3847953] <gracefully close end>\n\n\n Error Context: ../.doc/test-reports/assets/2026-09-14-controlled-content-ui-acceptance/playwright-output-main/conversation-intent-dual-t-ac365-oad-and-keeps-empty-actions-chromium/error-context.md\n\n 1 failed\n [chromium] › e2e/conversation-intent-dual-tab.spec.ts:36:3 › conversation intent dual-tab busy › tab B shows busy without reload and keeps empty actions \n",
"stderr_tail": "(node:3847921) Warning: The 'NO_COLOR' env is ignored due to the 'FORCE_COLOR' env being set.\n(Use `node --trace-warnings ...` to show where the warning was created)\n"
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 284 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 176 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 284 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 284 KiB

@@ -0,0 +1,10 @@
{
"project_id": 6275,
"project_name": "INTENT双标签-061002",
"session_id": "sess-p6275-u10",
"run_id": "run_1789452602295343365",
"owner_id": 10,
"forbidden_project": 143,
"phase": "terminal",
"final_status": "completed"
}
@@ -0,0 +1,209 @@
{
"session_id": "sess-p6275-u10",
"days": [
{
"day_key": "2026-09-15",
"count": 4,
"loaded_count": 4,
"truncated": false,
"first_message_id": 2319,
"last_message_id": 2322,
"messages": [
{
"id": 2319,
"session_id": "sess-p6275-u10",
"user_id": 10,
"role": "user",
"content": "请开始编写用户初始需求表",
"intent": "TRIGGER_TASK",
"metadata": {
"client_turn_id": "intent-tab-a-061002",
"project_id": 6275,
"scope_kind": "workflow_stage",
"stage_instance_id": 295,
"stage_key": "V0.1_triage",
"task_key": "intake",
"workflow_instance_id": 211
},
"day_key": "2026-09-15",
"category": "task",
"content_kind": "conversational_text",
"export_policy": "none",
"content_revision": 1,
"content_sha256": "341b927d12b80c545ceb319a5ef0db38a0900c33cab0925ef3bd8aa32394881a",
"workflow_instance_id": 211,
"stage_instance_id": 295,
"stage_key": "V0.1_triage",
"scope_kind": "workflow_stage",
"export_capability": {
"policy": "none",
"formats": [],
"body_selector": "content_only",
"reason_code": "NOT_ASSISTANT",
"content_revision": 1,
"content_sha256": "341b927d12b80c545ceb319a5ef0db38a0900c33cab0925ef3bd8aa32394881a"
},
"created_at": "2026-09-15T06:10:02.29734Z"
},
{
"id": 2320,
"session_id": "sess-p6275-u10",
"user_id": 0,
"role": "assistant",
"content": "任务执行中…",
"intent": "TRIGGER_TASK",
"metadata": {
"client_turn_id": "intent-tab-a-061002",
"project_id": 6275,
"run_id": "run_1789452602295343365",
"scope_kind": "workflow_stage",
"stage_instance_id": 295,
"stage_key": "V0.1_triage",
"task_key": "intake",
"task_request": {
"client_turn_id": "intent-tab-a-061002",
"customer_name": "",
"device_type": "超声软组织手术吸引设备",
"doc_id": "",
"intended_use": "intent dual-tab busy live",
"project_id": 6275,
"project_name": "INTENT双标签-061002",
"session_id": "sess-p6275-u10",
"target_market": "中国 NMPA",
"task_key": "intake",
"user_id": 10,
"user_message": "请开始编写用户初始需求表"
},
"task_status": "running",
"workflow_instance_id": 211
},
"day_key": "2026-09-15",
"category": "task",
"content_kind": "document_text",
"export_policy": "full",
"content_revision": 1,
"content_sha256": "e98386b7da0d395fed20132b53e21283fcc11efdb7bae134edb24bcb3930f535",
"workflow_instance_id": 211,
"stage_instance_id": 295,
"stage_key": "V0.1_triage",
"scope_kind": "workflow_stage",
"export_capability": {
"policy": "full",
"formats": [
"rich_clipboard",
"markdown",
"docx",
"pdf",
"private_document"
],
"body_selector": "content_only",
"content_revision": 1,
"content_sha256": "e98386b7da0d395fed20132b53e21283fcc11efdb7bae134edb24bcb3930f535"
},
"created_at": "2026-09-15T06:10:02.30037Z"
},
{
"id": 2321,
"session_id": "sess-p6275-u10",
"user_id": 10,
"role": "user",
"content": "请开始编写用户初始需求表",
"intent": "TRIGGER_TASK",
"metadata": {
"client_turn_id": "intent-tab-b-061002",
"project_id": 6275,
"scope_kind": "workflow_stage",
"stage_instance_id": 295,
"stage_key": "V0.1_triage",
"task_key": "intake",
"workflow_instance_id": 211
},
"day_key": "2026-09-15",
"category": "task",
"content_kind": "conversational_text",
"export_policy": "none",
"content_revision": 1,
"content_sha256": "341b927d12b80c545ceb319a5ef0db38a0900c33cab0925ef3bd8aa32394881a",
"workflow_instance_id": 211,
"stage_instance_id": 295,
"stage_key": "V0.1_triage",
"scope_kind": "workflow_stage",
"export_capability": {
"policy": "none",
"formats": [],
"body_selector": "content_only",
"reason_code": "NOT_ASSISTANT",
"content_revision": 1,
"content_sha256": "341b927d12b80c545ceb319a5ef0db38a0900c33cab0925ef3bd8aa32394881a"
},
"created_at": "2026-09-15T06:10:02.309173Z"
},
{
"id": 2322,
"session_id": "sess-p6275-u10",
"user_id": 0,
"role": "assistant",
"content": "文档【用户初始需求收集表】正在由 squad(run_1789452602295343365) 编辑中,当前请求未创建新任务",
"intent": "TRIGGER_TASK",
"metadata": {
"client_turn_id": "intent-tab-b-061002",
"error_info": {
"actions": [],
"class": "fatal",
"code": "DOC_BUSY",
"session_id": "sess-p6275-u10",
"user_message": "文档【用户初始需求收集表】正在由 squad(run_1789452602295343365) 编辑中,当前请求未创建新任务"
},
"project_id": 6275,
"scope_kind": "workflow_stage",
"stage_instance_id": 295,
"stage_key": "V0.1_triage",
"task_key": "intake",
"task_request": {
"client_turn_id": "intent-tab-b-061002",
"customer_name": "",
"device_type": "超声软组织手术吸引设备",
"doc_id": "",
"intended_use": "intent dual-tab busy live",
"project_id": 6275,
"project_name": "INTENT双标签-061002",
"session_id": "sess-p6275-u10",
"target_market": "中国 NMPA",
"task_key": "intake",
"user_id": 10,
"user_message": "请开始编写用户初始需求表"
},
"task_status": "failed",
"workflow_instance_id": 211
},
"day_key": "2026-09-15",
"category": "task",
"content_kind": "document_text",
"export_policy": "full",
"content_revision": 1,
"content_sha256": "5137c046ff6011eaa672af24e6fea41c83cc9e0e598287aad1a21b0ccea9915d",
"workflow_instance_id": 211,
"stage_instance_id": 295,
"stage_key": "V0.1_triage",
"scope_kind": "workflow_stage",
"export_capability": {
"policy": "full",
"formats": [
"rich_clipboard",
"markdown",
"docx",
"pdf",
"private_document"
],
"body_selector": "content_only",
"content_revision": 1,
"content_sha256": "5137c046ff6011eaa672af24e6fea41c83cc9e0e598287aad1a21b0ccea9915d"
},
"created_at": "2026-09-15T06:10:02.311768Z"
}
]
}
],
"has_more_before": false,
"oldest_loaded_day": "2026-09-15"
}
@@ -0,0 +1,221 @@
{
"session_id": "sess-p6275-u10",
"days": [
{
"day_key": "2026-09-15",
"count": 4,
"loaded_count": 4,
"truncated": false,
"first_message_id": 2319,
"last_message_id": 2322,
"messages": [
{
"id": 2319,
"session_id": "sess-p6275-u10",
"user_id": 10,
"role": "user",
"content": "请开始编写用户初始需求表",
"intent": "TRIGGER_TASK",
"metadata": {
"client_turn_id": "intent-tab-a-061002",
"project_id": 6275,
"scope_kind": "workflow_stage",
"stage_instance_id": 295,
"stage_key": "V0.1_triage",
"task_key": "intake",
"workflow_instance_id": 211
},
"day_key": "2026-09-15",
"category": "task",
"content_kind": "conversational_text",
"export_policy": "none",
"content_revision": 1,
"content_sha256": "341b927d12b80c545ceb319a5ef0db38a0900c33cab0925ef3bd8aa32394881a",
"workflow_instance_id": 211,
"stage_instance_id": 295,
"stage_key": "V0.1_triage",
"scope_kind": "workflow_stage",
"export_capability": {
"policy": "none",
"formats": [],
"body_selector": "content_only",
"reason_code": "NOT_ASSISTANT",
"content_revision": 1,
"content_sha256": "341b927d12b80c545ceb319a5ef0db38a0900c33cab0925ef3bd8aa32394881a"
},
"created_at": "2026-09-15T06:10:02.29734Z"
},
{
"id": 2320,
"session_id": "sess-p6275-u10",
"user_id": 0,
"role": "assistant",
"content": "《用户初始需求收集表》已生成。\n\n当前状态:**已完成**\n质量评分:100/100\n\n📄 受控文档:`doc_proj6275_intake`",
"intent": "TRIGGER_TASK",
"metadata": {
"artifact": {
"doc_id": "doc_proj6275_intake",
"quality_passed": true,
"quality_score": 100,
"sha256": "dc99aa130644353f258e951887c9d3babf35afc9d46e008327121bdedc641ddf",
"task_key": "intake",
"title": "用户初始需求收集表",
"workspace_id": "bizdev"
},
"client_turn_id": "intent-tab-a-061002",
"project_id": 6275,
"run_id": "run_1789452602295343365",
"run_manifest_sha256": "eec87a66d69f1e04df279689e66355c552dd7ac1756db4676430cfe8d92419d7",
"scope_kind": "workflow_stage",
"stage_instance_id": 295,
"stage_key": "V0.1_triage",
"storage_identity": "env:dev",
"task_key": "intake",
"task_request": {
"client_turn_id": "intent-tab-a-061002",
"customer_name": "",
"device_type": "超声软组织手术吸引设备",
"doc_id": "",
"intended_use": "intent dual-tab busy live",
"project_id": 6275,
"project_name": "INTENT双标签-061002",
"session_id": "sess-p6275-u10",
"target_market": "中国 NMPA",
"task_key": "intake",
"user_id": 10,
"user_message": "请开始编写用户初始需求表"
},
"task_status": "completed",
"workflow_instance_id": 211,
"workspace_id": "bizdev"
},
"day_key": "2026-09-15",
"category": "task",
"content_kind": "document_text",
"export_policy": "full",
"content_revision": 1,
"content_sha256": "e98386b7da0d395fed20132b53e21283fcc11efdb7bae134edb24bcb3930f535",
"workflow_instance_id": 211,
"stage_instance_id": 295,
"stage_key": "V0.1_triage",
"scope_kind": "workflow_stage",
"export_capability": {
"policy": "full",
"formats": [
"rich_clipboard",
"markdown",
"docx",
"pdf",
"private_document"
],
"body_selector": "content_only",
"content_revision": 1,
"content_sha256": "e98386b7da0d395fed20132b53e21283fcc11efdb7bae134edb24bcb3930f535"
},
"created_at": "2026-09-15T06:10:02.30037Z"
},
{
"id": 2321,
"session_id": "sess-p6275-u10",
"user_id": 10,
"role": "user",
"content": "请开始编写用户初始需求表",
"intent": "TRIGGER_TASK",
"metadata": {
"client_turn_id": "intent-tab-b-061002",
"project_id": 6275,
"scope_kind": "workflow_stage",
"stage_instance_id": 295,
"stage_key": "V0.1_triage",
"task_key": "intake",
"workflow_instance_id": 211
},
"day_key": "2026-09-15",
"category": "task",
"content_kind": "conversational_text",
"export_policy": "none",
"content_revision": 1,
"content_sha256": "341b927d12b80c545ceb319a5ef0db38a0900c33cab0925ef3bd8aa32394881a",
"workflow_instance_id": 211,
"stage_instance_id": 295,
"stage_key": "V0.1_triage",
"scope_kind": "workflow_stage",
"export_capability": {
"policy": "none",
"formats": [],
"body_selector": "content_only",
"reason_code": "NOT_ASSISTANT",
"content_revision": 1,
"content_sha256": "341b927d12b80c545ceb319a5ef0db38a0900c33cab0925ef3bd8aa32394881a"
},
"created_at": "2026-09-15T06:10:02.309173Z"
},
{
"id": 2322,
"session_id": "sess-p6275-u10",
"user_id": 0,
"role": "assistant",
"content": "文档【用户初始需求收集表】正在由 squad(run_1789452602295343365) 编辑中,当前请求未创建新任务",
"intent": "TRIGGER_TASK",
"metadata": {
"client_turn_id": "intent-tab-b-061002",
"error_info": {
"actions": [],
"class": "fatal",
"code": "DOC_BUSY",
"session_id": "sess-p6275-u10",
"user_message": "文档【用户初始需求收集表】正在由 squad(run_1789452602295343365) 编辑中,当前请求未创建新任务"
},
"project_id": 6275,
"scope_kind": "workflow_stage",
"stage_instance_id": 295,
"stage_key": "V0.1_triage",
"task_key": "intake",
"task_request": {
"client_turn_id": "intent-tab-b-061002",
"customer_name": "",
"device_type": "超声软组织手术吸引设备",
"doc_id": "",
"intended_use": "intent dual-tab busy live",
"project_id": 6275,
"project_name": "INTENT双标签-061002",
"session_id": "sess-p6275-u10",
"target_market": "中国 NMPA",
"task_key": "intake",
"user_id": 10,
"user_message": "请开始编写用户初始需求表"
},
"task_status": "failed",
"workflow_instance_id": 211
},
"day_key": "2026-09-15",
"category": "task",
"content_kind": "document_text",
"export_policy": "full",
"content_revision": 1,
"content_sha256": "5137c046ff6011eaa672af24e6fea41c83cc9e0e598287aad1a21b0ccea9915d",
"workflow_instance_id": 211,
"stage_instance_id": 295,
"stage_key": "V0.1_triage",
"scope_kind": "workflow_stage",
"export_capability": {
"policy": "full",
"formats": [
"rich_clipboard",
"markdown",
"docx",
"pdf",
"private_document"
],
"body_selector": "content_only",
"content_revision": 1,
"content_sha256": "5137c046ff6011eaa672af24e6fea41c83cc9e0e598287aad1a21b0ccea9915d"
},
"created_at": "2026-09-15T06:10:02.311768Z"
}
]
}
],
"has_more_before": false,
"oldest_loaded_day": "2026-09-15"
}
@@ -0,0 +1,209 @@
{
"session_id": "sess-p6275-u10",
"days": [
{
"day_key": "2026-09-15",
"count": 4,
"loaded_count": 4,
"truncated": false,
"first_message_id": 2319,
"last_message_id": 2322,
"messages": [
{
"id": 2319,
"session_id": "sess-p6275-u10",
"user_id": 10,
"role": "user",
"content": "请开始编写用户初始需求表",
"intent": "TRIGGER_TASK",
"metadata": {
"client_turn_id": "intent-tab-a-061002",
"project_id": 6275,
"scope_kind": "workflow_stage",
"stage_instance_id": 295,
"stage_key": "V0.1_triage",
"task_key": "intake",
"workflow_instance_id": 211
},
"day_key": "2026-09-15",
"category": "task",
"content_kind": "conversational_text",
"export_policy": "none",
"content_revision": 1,
"content_sha256": "341b927d12b80c545ceb319a5ef0db38a0900c33cab0925ef3bd8aa32394881a",
"workflow_instance_id": 211,
"stage_instance_id": 295,
"stage_key": "V0.1_triage",
"scope_kind": "workflow_stage",
"export_capability": {
"policy": "none",
"formats": [],
"body_selector": "content_only",
"reason_code": "NOT_ASSISTANT",
"content_revision": 1,
"content_sha256": "341b927d12b80c545ceb319a5ef0db38a0900c33cab0925ef3bd8aa32394881a"
},
"created_at": "2026-09-15T06:10:02.29734Z"
},
{
"id": 2320,
"session_id": "sess-p6275-u10",
"user_id": 0,
"role": "assistant",
"content": "任务执行中…",
"intent": "TRIGGER_TASK",
"metadata": {
"client_turn_id": "intent-tab-a-061002",
"project_id": 6275,
"run_id": "run_1789452602295343365",
"scope_kind": "workflow_stage",
"stage_instance_id": 295,
"stage_key": "V0.1_triage",
"task_key": "intake",
"task_request": {
"client_turn_id": "intent-tab-a-061002",
"customer_name": "",
"device_type": "超声软组织手术吸引设备",
"doc_id": "",
"intended_use": "intent dual-tab busy live",
"project_id": 6275,
"project_name": "INTENT双标签-061002",
"session_id": "sess-p6275-u10",
"target_market": "中国 NMPA",
"task_key": "intake",
"user_id": 10,
"user_message": "请开始编写用户初始需求表"
},
"task_status": "running",
"workflow_instance_id": 211
},
"day_key": "2026-09-15",
"category": "task",
"content_kind": "document_text",
"export_policy": "full",
"content_revision": 1,
"content_sha256": "e98386b7da0d395fed20132b53e21283fcc11efdb7bae134edb24bcb3930f535",
"workflow_instance_id": 211,
"stage_instance_id": 295,
"stage_key": "V0.1_triage",
"scope_kind": "workflow_stage",
"export_capability": {
"policy": "full",
"formats": [
"rich_clipboard",
"markdown",
"docx",
"pdf",
"private_document"
],
"body_selector": "content_only",
"content_revision": 1,
"content_sha256": "e98386b7da0d395fed20132b53e21283fcc11efdb7bae134edb24bcb3930f535"
},
"created_at": "2026-09-15T06:10:02.30037Z"
},
{
"id": 2321,
"session_id": "sess-p6275-u10",
"user_id": 10,
"role": "user",
"content": "请开始编写用户初始需求表",
"intent": "TRIGGER_TASK",
"metadata": {
"client_turn_id": "intent-tab-b-061002",
"project_id": 6275,
"scope_kind": "workflow_stage",
"stage_instance_id": 295,
"stage_key": "V0.1_triage",
"task_key": "intake",
"workflow_instance_id": 211
},
"day_key": "2026-09-15",
"category": "task",
"content_kind": "conversational_text",
"export_policy": "none",
"content_revision": 1,
"content_sha256": "341b927d12b80c545ceb319a5ef0db38a0900c33cab0925ef3bd8aa32394881a",
"workflow_instance_id": 211,
"stage_instance_id": 295,
"stage_key": "V0.1_triage",
"scope_kind": "workflow_stage",
"export_capability": {
"policy": "none",
"formats": [],
"body_selector": "content_only",
"reason_code": "NOT_ASSISTANT",
"content_revision": 1,
"content_sha256": "341b927d12b80c545ceb319a5ef0db38a0900c33cab0925ef3bd8aa32394881a"
},
"created_at": "2026-09-15T06:10:02.309173Z"
},
{
"id": 2322,
"session_id": "sess-p6275-u10",
"user_id": 0,
"role": "assistant",
"content": "文档【用户初始需求收集表】正在由 squad(run_1789452602295343365) 编辑中,当前请求未创建新任务",
"intent": "TRIGGER_TASK",
"metadata": {
"client_turn_id": "intent-tab-b-061002",
"error_info": {
"actions": [],
"class": "fatal",
"code": "DOC_BUSY",
"session_id": "sess-p6275-u10",
"user_message": "文档【用户初始需求收集表】正在由 squad(run_1789452602295343365) 编辑中,当前请求未创建新任务"
},
"project_id": 6275,
"scope_kind": "workflow_stage",
"stage_instance_id": 295,
"stage_key": "V0.1_triage",
"task_key": "intake",
"task_request": {
"client_turn_id": "intent-tab-b-061002",
"customer_name": "",
"device_type": "超声软组织手术吸引设备",
"doc_id": "",
"intended_use": "intent dual-tab busy live",
"project_id": 6275,
"project_name": "INTENT双标签-061002",
"session_id": "sess-p6275-u10",
"target_market": "中国 NMPA",
"task_key": "intake",
"user_id": 10,
"user_message": "请开始编写用户初始需求表"
},
"task_status": "failed",
"workflow_instance_id": 211
},
"day_key": "2026-09-15",
"category": "task",
"content_kind": "document_text",
"export_policy": "full",
"content_revision": 1,
"content_sha256": "5137c046ff6011eaa672af24e6fea41c83cc9e0e598287aad1a21b0ccea9915d",
"workflow_instance_id": 211,
"stage_instance_id": 295,
"stage_key": "V0.1_triage",
"scope_kind": "workflow_stage",
"export_capability": {
"policy": "full",
"formats": [
"rich_clipboard",
"markdown",
"docx",
"pdf",
"private_document"
],
"body_selector": "content_only",
"content_revision": 1,
"content_sha256": "5137c046ff6011eaa672af24e6fea41c83cc9e0e598287aad1a21b0ccea9915d"
},
"created_at": "2026-09-15T06:10:02.311768Z"
}
]
}
],
"has_more_before": false,
"oldest_loaded_day": "2026-09-15"
}
@@ -0,0 +1,99 @@
import { test, expect, type Page } from '@playwright/test';
import * as fs from 'fs';
import * as path from 'path';
import { fileURLToPath } from 'url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const runDualTab = process.env.INTENT_DUAL_TAB === '1';
const ASSETS = process.env.INTENT_ASSETS
? path.resolve(process.env.INTENT_ASSETS)
: path.resolve(__dirname, '../../.doc/test-reports/assets/2026-09-15-conversation-intent-dual-tab');
const SCREENSHOTS = path.join(ASSETS, 'screenshots');
fs.mkdirSync(SCREENSHOTS, { recursive: true });
const OWNER_USER = process.env.OWNER_USERNAME || process.env.ADMIN_USERNAME || '';
const OWNER_PASS = process.env.OWNER_PASSWORD || process.env.ADMIN_PASSWORD || '';
const PROJECT_NAME = (process.env.INTENT_PROJECT_NAME || '').trim();
test.skip(!runDualTab, 'set INTENT_DUAL_TAB=1 against rebuilt dev; do not use project 143');
async function login(page: Page, username: string, password: string) {
await page.goto('/');
await page.fill('input[placeholder*="系统用户名"]', username);
await page.fill('input[placeholder*="登录密码"]', password);
await page.click('button:has-text("进入医疗协同工作台")');
await expect(page.getByText('新建立项会话')).toBeVisible({ timeout: 30_000 });
}
async function openProject(page: Page, name: string) {
const entry = page.getByTitle(name, { exact: true });
await expect(entry.first()).toBeVisible({ timeout: 30_000 });
await entry.first().click();
await expect(page.getByRole('banner').getByText(name, { exact: true })).toBeVisible({ timeout: 30_000 });
}
test.describe.serial('conversation intent dual-tab busy', () => {
test('tab B shows busy without reload and keeps empty actions', async ({ browser }) => {
if (!OWNER_USER || !OWNER_PASS) {
throw new Error('OWNER_USERNAME and OWNER_PASSWORD are required');
}
if (!PROJECT_NAME) {
throw new Error('INTENT_PROJECT_NAME is required');
}
if (/143/.test(PROJECT_NAME) || process.env.INTENT_PROJECT_ID === '143') {
throw new Error('project 143 is read-only');
}
const context = await browser.newContext();
const tabA = await context.newPage();
const posts: { status: number; body: string }[] = [];
const attachRunProbe = (page: Page) => {
page.on('response', async (res) => {
if (res.url().includes('/api/v1/tasks/runs') && res.request().method() === 'POST') {
posts.push({ status: res.status(), body: await res.text().catch(() => '') });
}
});
};
attachRunProbe(tabA);
await login(tabA, OWNER_USER, OWNER_PASS);
await openProject(tabA, PROJECT_NAME);
const tabB = await context.newPage();
attachRunProbe(tabB);
await tabB.goto('/');
await openProject(tabB, PROJECT_NAME);
const busyBanner = tabB.getByText(/本文档正由【(项目助手|其他用户)】编辑/)
const busyBadge = tabB.getByText('编辑中', { exact: true })
await expect(busyBanner.or(busyBadge).first()).toBeVisible({ timeout: 20_000 })
await tabA.screenshot({ path: path.join(SCREENSHOTS, 'tab-a-running.png'), fullPage: true })
await tabB.screenshot({ path: path.join(SCREENSHOTS, 'tab-b-busy.png'), fullPage: true })
await expect(tabB.getByRole('button', { name: /发送/ })).toBeDisabled()
await expect(tabB.getByText('DOC_BUSY')).toBeVisible()
const box = tabB.getByPlaceholder(/向项目助手提问/)
await expect(box).toBeVisible({ timeout: 15_000 })
await box.fill('请开始编写用户初始需求表')
const send = tabB.getByRole('button', { name: /发送/ })
if (await send.isEnabled()) {
await send.click()
}
await tabB.waitForTimeout(2000)
await tabB.screenshot({ path: path.join(SCREENSHOTS, 'tab-b-after-write.png'), fullPage: true })
await expect(tabB.getByRole('button', { name: /重试|重新开始/ })).toHaveCount(0)
fs.writeFileSync(
path.join(ASSETS, 'playwright-network.json'),
JSON.stringify({ posts, project: PROJECT_NAME }, null, 2),
)
await tabB.reload()
await expect(tabB.getByText(/本文档正由【(项目助手|其他用户)】编辑/).or(tabB.getByText('DOC_BUSY')).first()).toBeVisible({ timeout: 20_000 })
await expect(tabB.getByRole('button', { name: /重试|重新开始/ })).toHaveCount(0)
await tabB.screenshot({ path: path.join(SCREENSHOTS, 'tab-b-after-reload.png'), fullPage: true })
await context.close();
});
});