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.
7.5 KiB
7.5 KiB
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,非法静默回落 40;modeauto/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 规模诊断(真实 viewport:resize+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 ms(min/avg/max);DOM 节点 1057–1080;页面高度 7127–7213 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 |
诊断过程记录(问题 → 根因 → 修复)
rw_init_window链下标偏移:初始窗口传入局部下标 0..39 而非链下标 →hiddenOlder恒 0。 修复:offset = total - len(window_items),传offset + i。- offscreen 零视口:未
resize+show前innerHeight=0,锚点几何全废。 修复(诊断侧):window.resize(1400,950)+show()+ 等待innerHeight>0+ 显式load_messages_to_web重载。 runJavaScript不能返回 DOM 元素:回调转换失败 → 用cond ? 1 : 0/ 原语返回值。- 分支兄弟偷叶:链尾补兄弟消息使
add_message自动改叶 → 链被截断。 修复(夹具):兄弟消息在循环内i==298处插入。 - 页 = 整窗导致锚点必被裁(设计缺陷):半窗页修复(见"关键参数")。
finishMessage守卫延迟求值 → 滚底 → 'newer' 振荡(见"滚动语义"第 2 条)。- 中部换页落点贴底:15% 视口位置向上换页后锚点落 65%(不贴底);底部半窗换页本身会 落向底部属半窗几何固有——真实入口(顶部"加载更早消息"按钮 / auto 顶部观察器)不触发该位置, 且落底后自动 'newer' 恢复符合"向下自动恢复"需求。
已知观察项(不在本任务范围)
diag_render_scale的每页耗时(~180ms)只作真机基准参考,非硬阈值(符合任务要求)。- 帧耗时真机人工基准报告留待人工验收环节。