Files
Haocode/docs/agent-handoff/evidence/P1-01.md
T
sorrow404null bc0b92bcdc 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.
2026-09-17 16:40:06 +08:00

7.5 KiB
Raw Blame History

P1-01 双向消息渲染窗口 — 完成证据

状态: COMPLETE 完成时间: 2026-07-21(会话时间) 环境: Windows 11 x64, CPython 3.10.21 (.venv, uv), PyQt6 / Qt 6.10.0, Node

设计

数据流(引擎无关)

  • JS → Pythonbridge.onRequestWindowPage(sessionId, direction, boundaryId, generation) QtWebChannel slot 与 WebView2 postMessage 白名单同名,单一实现)。
  • Python → JSrun_js("rwPageResponse(<json>)") / rwInitWindow(<json>) / rwNoteLive(...) / rwBegin(...) / rwConfig(...)
  • JS 端只做"窗口游标 + DOM 搬移"Python 用既有逐消息 bridge 调用渲染 DOM, JS 状态机 ui/web/render_window.js 跟踪 order(消息 id 序列)+ indexById(链内下标), 不解析消息内容。

关键参数

  • 窗口 = render_window_size10/40/200,非法静默回落 40mode auto/manual 回落 auto)。
  • 页 = 半窗max(1, size//2))。若页 = 整窗,"首个可见消息"锚点必然被裁出窗口, 需求中的锚点恢复(≤2px)永远不可达——诊断实测验证了这一点后才改为半窗。
  • 活动流式消息受保护(trimHead/trimTail 跳过 activeStreamId),计入上限; streamFinished 解除保护。rwNoteLive 经 DOM .streaming 类自动接管流式保护, 并携带实时 chainLen 维持 hidden 计数新鲜。

滚动语义

  • 批次渲染期间 window.__rwPageRendering = true,抑制 softScroll()finishMessagescrollIntoView
  • 守卫在调度时刻捕获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_SIZEALLOWED_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/rwUpdateLoadButtonsclearChat 联动状态机清空;finishMessage 步骤 C 调 streamFinished、步骤 F 守卫捕获式;createMessage/createLongMessage/createUserMessageWithAttachmentssoftScroll 守卫;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.pytests/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 400offscreen 6/6 PASS:初始窗口=最新 40 条(链长 400);中部锚点保持(误差 ≤2px、无 newer 振荡);自顶部向上分页至头部(绝对顶部例外);头部状态+全链 400 条可达无重复;自顶部向下回翻 2 页;auto 模式顶部自动补页。锚点误差 0.00px18 页 @ 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.pyagent core 规范入口) 41/41 PASS

诊断过程记录(问题 → 根因 → 修复)

  1. rw_init_window 链下标偏移:初始窗口传入局部下标 0..39 而非链下标 → hiddenOlder 恒 0。 修复:offset = total - len(window_items),传 offset + i
  2. offscreen 零视口:未 resize+showinnerHeight=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)只作真机基准参考,非硬阈值(符合任务要求)。
  • 帧耗时真机人工基准报告留待人工验收环节。