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
+95
View File
@@ -0,0 +1,95 @@
# P2-03 证据:WebView2 原生窗口遮挡层(重命名遮罩)
**任务**:修复 RenameOverlay 被 WebView2 原生子窗口压住的确定性缺陷,审计同类遮罩,只修可确认的原生窗口遮挡。
**结论**`RenameOverlay` 已重写为独立顶层透明 Tool 窗(与 `AttachmentPreviewOverlay` 同一验证过的模式);审计确认它是**唯一**受影响的遮罩,其余全部本就是顶层窗口,未改动。自动化目标测试全绿。
## 根因(源码级)
- `core/webview2.py`(L10-11 注释 + 子窗口发现实现):WebView2 的 `Chrome_WidgetWin_*` 是**主窗口 HWND 的原生子 HWND**EnumChildWindows 轮询发现,SetBoundsAndZoomFactor 定位)。
- Windows 上原生子 HWND 永远绘制在其父 HWND 内所有 Qt 渲染内容**之上**Qt 绘入主窗口 backing store,原生子窗在 z 序更高)。
-`RenameOverlay``bg_widget` 的**子控件**`QWidget(parent=bg_widget)` + `setGeometry(parent.rect())`):遮罩与卡片都是主窗口内的 Qt 绘制 → 在聊天区(WebView2 所在区域)内被原生 webview 窗盖住:遮罩不暗、卡片被压。
- 唯一可盖住它的结构:独立顶层窗口(独立 HWND)+ 逐像素 alpha`WA_TranslucentBackground`)。`AttachmentPreviewOverlay` 早已按此模式修复(其 docstring 明文记录同一缺陷),`RenameOverlay` 是遗留的旧模式。
## 修改(ui/views/main_window.py,仅 RenameOverlay 类,L1363 起整类替换)
| 项 | 旧 | 新 |
|---|---|---|
| 窗口类型 | `bg_widget` 子控件 | 顶层 `FramelessWindowHint \| Tool`(独立 HWND,拥有者=主窗口,Windows 上默认浮于拥有者之上,不入任务栏) |
| 透明 | 无(fillRect 半透明灰) | `WA_TranslucentBackground` + paintEvent 半透明灰(逐像素 alpha |
| 覆盖范围 | `parent.rect()`(客户区内嵌) | **主窗口客户区**`mapToGlobal(main.rect().topLeft())` + `main.rect().size()` —— 不含系统标题栏/边框,标题栏与窗口控制保持可操作 |
| 跟随 | `resizeEvent`(子控件自动跟随) | eventFilter 挂主窗口:`Move` → move`Resize` / `WindowStateChange`(最大化/还原;DPI 变化时 Qt 对主窗合成 move+resize,同路跟随)→ 客户区几何重同步 + 卡片重居中;`Close`/`Hide` → 关闭遮罩 |
| 入场动画 | QGraphicsOpacityEffect(顶层窗不可靠) | windowOpacity 属性动画 150ms(同附件预览层) |
| 行为 | 点空白/✕/取消 关闭、输入全选、Enter 提交 | 全部保留,**新增 Esc 关闭**keyPressEvent);`confirm()` 空标题不 emit |
| 释放 | `deleteLater` | `close_overlay()``removeEventFilter(main)` + `main.activateWindow()/raise_()`(焦点回主窗)+ `deleteLater``WA_DeleteOnClose``_closed` 幂等防重入 |
| 卡片拖拽 | 有(限父窗内) | 保留(限客户区内) |
`_rename_session` 调用点未改(仍 `RenameOverlay(current_title, self.bg_widget)``parent.window()` 取主窗口)。无新增辅助函数(几何同步逻辑与附件预览层各 10 行,不构成"真实重复",未抽公共函数)。
## 同类遮罩审计(只修可复现者)
| 遮罩/弹窗 | 位置 | 窗口类型 | 结论 |
|---|---|---|---|
| `AttachmentPreviewOverlay` | L53 | 顶层 Tool + `WA_TranslucentBackground` + eventFilter 跟随(frameGeometry | **不受影响**(已是正确模式,本次修复的参照) |
| `SettingsWindow` | L424 | 顶层 `FramelessWindowHint`(独立窗,内部虚化遮罩是其子控件) | **不受影响**(独立 HWND,天然在 webview 之上) |
| `SessionContextPopup` | L1267 | `Popup \| FramelessWindowHint` | **不受影响**Popup 为独立顶层原生窗) |
| `ModelSelectPopup` | L1624 | `Popup \| FramelessWindowHint`L1649 | **不受影响** |
| `SessionModePopup` | L2245 | `Popup \| FramelessWindowHint`L2260 | **不受影响** |
| `PdfModePopup` | L2368 | `Popup \| FramelessWindowHint`L2392 | **不受影响** |
| `RenameOverlay` | L1363 | ~~bg_widget 子控件~~ → 顶层 Tool + 透明 | **受影响,已修复**(唯一) |
未把普通 popup/dialog 重写成统一框架(硬约束)。
## 自动化验证(本机 Windows 11 10.0.26200 x64`.venv` CPython 3.10.21
新建 `tests/diag_rename_overlay.py`(隔离临时 DBoffscreen34 项断言,EXIT=0):
```
RESULT: 34/34 -> ALL PASS
R0 遮罩创建且为顶层窗口
R1 结构:isWindow / window() is self(非内嵌子控件)/ Tool / Frameless /
WA_TranslucentBackground / WA_DeleteOnClose / 主窗口未设透明
R2 几何:覆盖客户区左上角与尺寸(±2px)/ 卡片居中 / 输入框初始全选 / 预填旧标题
R3 跟随:主窗 move(+150,+80)→精确跟随 / resize(1200x700)→尺寸同步+卡片重居中 /
WindowStateChange 分支(最大化/还原同路)不崩溃且几何仍正确
R4 行为:Enter 提交→DB+侧栏标题更新 / 空标题 confirm 不改标题 /
Esc 关闭 / 点空白关闭 / ✕ 关闭 / 取消关闭 / 非确认关闭不改标题
R5 释放:顶层窗口消失、无残留 rename_form、主窗口存活可用
R6 焦点回主窗(offscreen 软检查,INFO 记录)
```
目标测试 + 回归(均 EXIT=0):
| 套件 | 结果 |
|---|---|
| `tests/diag_rename_overlay.py` | 34/34 ALL PASS |
| `tests/smoke_offscreen.py`QtWebEngine 路径) | 8/8 ALL PASS |
| `tests/smoke_copy_session.py`(侧栏/Popup 链路) | ALL PASS |
| `tests/smoke_bash_panel.py` | 140 项 ALL PASS |
| `tests/run_tests.py` | 41 passed, 0 failed |
## 真机 WebView2 验收(用户走查步骤)
真机 WebView2 自动化被有意放弃:`core/webview2.py``get_environment()``taskkill /F /IM msedgewebview2.exe`(会杀掉用户其他 WebView2 应用进程)且 SDK 怪癖要求默认共享 profile(无法重定向到临时目录)——无人值守自动化运行该路径风险不可接受。
**【2026-09-17 更新】本机 WebView2 加载阻塞已解除**`core/webview2.py` 加 byte[] 回落,见 evidence/P1-03.md):主程序现已能在本机以真实 WebView2 启动(Runtime 153.0.4234.32、controller ready、index.html NavigationCompleted、stderr 无错)。下表人工清单现可在本机真实 WebView2 模式下执行。
机制保证(与生产已验证的 AttachmentPreviewOverlay 完全同构):新遮罩是**独立顶层 HWND**WS_EX_TOOLWINDOW + 拥有者=主窗口);Windows z 序规则下,拥有者窗口之上的顶层窗永远绘制在拥有者的原生子 HWND(WebView2 `Chrome_WidgetWin_*`)之上,与 offscreen 平台无关。
人工验收清单(Windows 桌面,真实 WebView2 模式运行主程序后):
1. 会话右键 → 重命名:遮罩盖住**整个客户区**(含聊天区 webview,webview 变暗),标题栏(含最小化/最大化/关闭)仍可点。
2. 拖主窗口 / 拉边框缩放 / 最大化 / 还原:遮罩与卡片全程贴合客户区、卡片保持居中。
3. 多显示器间拖主窗 / 改缩放比后重开遮罩:几何正确(mapToGlobal 路径)。
4. Esc / 点空白 / ✕ / 取消 → 遮罩消失、焦点回主窗、侧栏可继续操作;Enter 或确定 → 标题更新。
5. 连续开/关 5 次:无残留遮罩、无卡顿、任务栏无新增图标。
## 教训(跨压缩持久)
- 【P2-03 发现】**offscreen/无真实事件循环时 `processEvents()` 不处理 `DeferredDelete`**`deleteLater()` 的控件必须显式 `QCoreApplication.sendPostedEvents(None, QEvent.Type.DeferredDelete)` 才会真正删除(探针证实:仅 processEvents 循环后对象仍 alive)。生产事件循环常驻不受影响,但所有 offscreen 测试的"已删除"断言前必须冲刷 DeferredDelete。
- 【P2-03 教训】PyQt6 `setGeometry``(QPoint, QSize)` 重载 → 构造 `QRect(tl, size)``QTest.mouseClick(widget, button, modifier, pos)` 第 3 参是 **modifier**(易误当 pos)。
- 【P2-03 发现】`core/webview2.py` `get_environment()``taskkill /F /IM msedgewebview2.exe` + 共享默认 profile 不可重定向 → 无人值守自动化不得走真实 WebView2 启动路径;真机验收走人工清单。
- 【P2-03 结构判据】"顶层窗可带 owner parent"`setWindowFlags(Tool|Frameless)``parentWidget()` 仍可非 None(owner 关系),判据是 `isWindow()` / `window() is self`,不是 `parentWidget() is None`
## 修改文件
- `ui/views/main_window.py`(仅 `RenameOverlay` 类整类替换,~170 行;调用点未改)
- `tests/diag_rename_overlay.py`(新,34 项断言)