Files
Haocode/docs/agent-handoff/evidence/P2-03.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

8.4 KiB
Raw Blame History

P2-03 证据:WebView2 原生窗口遮挡层(重命名遮罩)

任务:修复 RenameOverlay 被 WebView2 原生子窗口压住的确定性缺陷,审计同类遮罩,只修可确认的原生窗口遮挡。 结论RenameOverlay 已重写为独立顶层透明 Tool 窗(与 AttachmentPreviewOverlay 同一验证过的模式);审计确认它是唯一受影响的遮罩,其余全部本就是顶层窗口,未改动。自动化目标测试全绿。

根因(源码级)

  • core/webview2.py(L10-11 注释 + 子窗口发现实现):WebView2 的 Chrome_WidgetWin_*主窗口 HWND 的原生子 HWNDEnumChildWindows 轮询发现,SetBoundsAndZoomFactor 定位)。
  • Windows 上原生子 HWND 永远绘制在其父 HWND 内所有 Qt 渲染内容之上Qt 绘入主窗口 backing store,原生子窗在 z 序更高)。
  • RenameOverlaybg_widget子控件QWidget(parent=bg_widget) + setGeometry(parent.rect())):遮罩与卡片都是主窗口内的 Qt 绘制 → 在聊天区(WebView2 所在区域)内被原生 webview 窗盖住:遮罩不暗、卡片被压。
  • 唯一可盖住它的结构:独立顶层窗口(独立 HWND)+ 逐像素 alphaWA_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 → moveResize / WindowStateChange(最大化/还原;DPI 变化时 Qt 对主窗合成 move+resize,同路跟随)→ 客户区几何重同步 + 卡片重居中;Close/Hide → 关闭遮罩
入场动画 QGraphicsOpacityEffect(顶层窗不可靠) windowOpacity 属性动画 150ms(同附件预览层)
行为 点空白/✕/取消 关闭、输入全选、Enter 提交 全部保留,新增 Esc 关闭keyPressEvent);confirm() 空标题不 emit
释放 deleteLater close_overlay()removeEventFilter(main) + main.activateWindow()/raise_()(焦点回主窗)+ deleteLaterWA_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 | FramelessWindowHintL1649 不受影响
SessionModePopup L2245 Popup | FramelessWindowHintL2260 不受影响
PdfModePopup L2368 Popup | FramelessWindowHintL2392 不受影响
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.pyQtWebEngine 路径) 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.pyget_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 完全同构):新遮罩是独立顶层 HWNDWS_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() 不处理 DeferredDeletedeleteLater() 的控件必须显式 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 项断言)