docs: add agent handoff docs, verification guide, and repair evidence

Add docs/agent-handoff (backlog, execution state, verification, platform plan, per-task evidence), repo AGENTS.md, and architecture notes updated for the dual-renderer design.
This commit is contained in:
2026-09-17 16:40:06 +08:00
parent 75b2ec4123
commit bc0b92bcdc
25 changed files with 2347 additions and 0 deletions
+90
View File
@@ -0,0 +1,90 @@
# P1-01 双向消息渲染窗口 — 完成证据
**状态**: COMPLETE
**完成时间**: 2026-07-21(会话时间)
**环境**: Windows 11 x64, CPython 3.10.21 (.venv, uv), PyQt6 / Qt 6.10.0, Node
## 设计
**数据流(引擎无关)**
- JS → Python`bridge.onRequestWindowPage(sessionId, direction, boundaryId, generation)`
QtWebChannel slot 与 WebView2 postMessage 白名单同名,单一实现)。
- Python → JS`run_js("rwPageResponse(<json>)")` / `rwInitWindow(<json>)` / `rwNoteLive(...)` / `rwBegin(...)` / `rwConfig(...)`
- JS 端只做"窗口游标 + DOM 搬移"Python 用既有逐消息 bridge 调用渲染 DOM
JS 状态机 `ui/web/render_window.js` 跟踪 `order`(消息 id 序列)+ `indexById`(链内下标),
不解析消息内容。
**关键参数**
- 窗口 = `render_window_size`10/40/200,非法静默回落 40mode `auto`/`manual` 回落 auto)。
- **页 = 半窗**`max(1, size//2)`)。若页 = 整窗,"首个可见消息"锚点必然被裁出窗口,
需求中的锚点恢复(≤2px)永远不可达——诊断实测验证了这一点后才改为半窗。
- 活动流式消息受保护(`trimHead`/`trimTail` 跳过 `activeStreamId`),计入上限;
`streamFinished` 解除保护。`rwNoteLive` 经 DOM `.streaming` 类自动接管流式保护,
并携带实时 `chainLen` 维持 hidden 计数新鲜。
**滚动语义**
- 批次渲染期间 `window.__rwPageRendering = true`,抑制 `softScroll()`
`finishMessage``scrollIntoView`
- **守卫在调度时刻捕获**`var rwBatchSuppressed = !!window.__rwPageRendering` 后立即
rAF+50ms 延迟回调):延迟回调触发时批次已结束、标志已被 Python 复位,届时再读会漏放
`scrollIntoView(smooth)` → 平滑滚底 → `rwAutoCheck` 误判贴底 → 触发 'newer' 反向换页振荡。
这是诊断中 `scrollY 0→4367` 振荡的根因,已修复。
- 向上换页锚点恢复:`scrollTop = oldScroll + (anchor.newTop - anchor.docTop)`
绝对顶部(`oldScroll <= 1`)例外:停在 0 露出新页;auto 模式沿顶部 60ms 链式补页。
- 向下换页两模式均自动恢复(`isNearBottom() && canRequest('newer')`)。
- 过期响应(会话/代次不匹配、pending 已被新请求替换、重复投递)整批丢弃,已渲染 DOM 回滚。
**生成代次(generation**
- Python `MainWindow._rw_generation` 为唯一权威源:每次 `load_messages_to_web` / 新建会话 +1。
- JS `clear()` 本地防御性 +1,Python 下次推送重新同步。
## 文件变更
| 文件 | 变更 |
|---|---|
| `core/config_paths.py` | 新增 `render_window_settings()`16 类配置归一化)、`DEFAULT_RENDER_WINDOW_SIZE``ALLOWED_RENDER_WINDOW_SIZES` |
| `ui/web/render_window.js` | 新增:DOM 无关状态机(id+chainIndex 模型,双遍 recompute |
| `ui/web/app.js` | `rwState`/`rwInitWindow`/`rwPageResponse`/`rwApplyOlder`/`rwApplyNewer`/`rwNoteLive`/`rwRequestPage`/`rwAutoCheck`/`rwCaptureAnchor`/`rwRemoveMessageDom`/`rwEnsureLoadButtons`/`rwUpdateLoadButtons``clearChat` 联动状态机清空;`finishMessage` 步骤 C 调 `streamFinished`、步骤 F 守卫捕获式;`createMessage`/`createLongMessage`/`createUserMessageWithAttachments``softScroll` 守卫;scroll 监听挂 `rwAutoCheck` |
| `ui/web/index.html` | 引入 `render_window.js`(先于 app.js);WebView2 shim 增加 `onRequestWindowPage` |
| `ui/web/style.css` | `.load-window-btn` 样式(含 `[hidden]` 规则) |
| `ui/views/chat_bridge.py` | 信号 `window_page_requested`、slot `onRequestWindowPage`、推送方法 `rw_config`/`rw_begin`/`rw_init_window`/`rw_note_live`/`rw_page_response` |
| `ui/views/wv2_view.py` | `_BRIDGE_METHODS` 白名单加 `onRequestWindowPage` |
| `ui/views/main_window.py` | `init_browser` 窗口状态初始化 + 信号连接;`_on_js_ready_checked` 一次性 `rwConfig` 推送;`load_messages_to_web` 窗口化重写(代次+1、`rwBegin`、最新 size 条窗口渲染、`rwInitWindow`、流式恢复保留);新增 `_rw_visible_chain`/`_rw_note_live`/`_render_history_one`/`_on_window_page_request`(边界缺失安全降级空页);`on_new_chat_clicked` 代次+1;发送/重答/完成 5 处 `_rw_note_live` 挂点 |
| `tests/test_render_window.js` | 新增:状态机 Node 测试 |
| `tests/diag_render_scale.py` | 新增:400 条链 offscreen 规模诊断(真实 viewportresize+show+等待 innerHeight>0+显式重载) |
| `tests/smoke_timeline.py``tests/smoke_midswitch.py` | 转换到 `tests/_test_env.isolate()`(临时 DB+临时配置,P0-01 铁律) |
| `tests/_probe_rw.py` | 调试探针(保留,供后续排障) |
## 验证(全部 EXIT=0,全部带显式超时执行)
| 套件 | 结果 |
|---|---|
| `node tests/test_render_window.js` | **424/424 PASS**(配置归一化 16 例、auto/manual×10/40/200 初始窗口、双向连续换页 39 页/向、短链、过期响应 4 类、活动流保护、clear 语义、锚点几何、原子性、noteLive) |
| `python tests/diag_render_scale.py 400`offscreen | **6/6 PASS**:初始窗口=最新 40 条(链长 400);中部锚点保持(误差 ≤2px、无 newer 振荡);自顶部向上分页至头部(绝对顶部例外);头部状态+全链 400 条可达无重复;自顶部向下回翻 2 页;auto 模式顶部自动补页。**锚点误差 0.00px**18 页 @ 169/183/204 msmin/avg/max);DOM 节点 10571080;页面高度 71277213 px |
| `python tests/smoke_offscreen.py` | 8/8 ALL PASS |
| `python tests/smoke_timeline.py` | 11/11 ALL PASS(流式/历史路径,含 streaming 类收尾) |
| `python tests/smoke_midswitch.py` | 7/7 ALL PASS(切走切回时间线完整) |
| `python tests/test_file_attach.py` | 9 tests OK |
| `node tests/test_math_extract.js` | 39/39 PASS |
| 回归:`smoke_bash_panel` / `test_config_isolation` / `test_main_window_event_filter` / `smoke_mode` / `test_error_persist` | 全部 ALL PASS |
| `python tests/run_tests.py`agent core 规范入口) | **41/41 PASS** |
## 诊断过程记录(问题 → 根因 → 修复)
1. **`rw_init_window` 链下标偏移**:初始窗口传入局部下标 0..39 而非链下标 → `hiddenOlder` 恒 0。
修复:`offset = total - len(window_items)`,传 `offset + i`
2. **offscreen 零视口**:未 `resize`+`show``innerHeight=0`,锚点几何全废。
修复(诊断侧):`window.resize(1400,950)` + `show()` + 等待 `innerHeight>0` + 显式 `load_messages_to_web` 重载。
3. **`runJavaScript` 不能返回 DOM 元素**:回调转换失败 → 用 `cond ? 1 : 0` / 原语返回值。
4. **分支兄弟偷叶**:链尾补兄弟消息使 `add_message` 自动改叶 → 链被截断。
修复(夹具):兄弟消息在循环内 `i==298` 处插入。
5. **页 = 整窗导致锚点必被裁**(设计缺陷):半窗页修复(见"关键参数")。
6. **`finishMessage` 守卫延迟求值 → 滚底 → 'newer' 振荡**(见"滚动语义"第 2 条)。
7. **中部换页落点贴底**:15% 视口位置向上换页后锚点落 65%(不贴底);底部半窗换页本身会
落向底部属半窗几何固有——真实入口(顶部"加载更早消息"按钮 / auto 顶部观察器)不触发该位置,
且落底后自动 'newer' 恢复符合"向下自动恢复"需求。
## 已知观察项(不在本任务范围)
- `diag_render_scale` 的每页耗时(~180ms)只作真机基准参考,非硬阈值(符合任务要求)。
- 帧耗时真机人工基准报告留待人工验收环节。