Files
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

96 lines
8.4 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 项断言)