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
+60
View File
@@ -0,0 +1,60 @@
# P1-03 证据:Windows/Linux 渲染器启动链
日期:2026-07-17 · 执行环境:Windows 11 10.0.26200 x64 / CPython 3.10.21 (.venv, uv) /
PyQt6 6.10 + PyQt6-WebEngine 6.10 · 每条命令均带显式超时
## 改动文件
| 文件 | 性质 | 说明 |
|---|---|---|
| `core/renderer_backend.py` | 新增(仅 stdlib,可在导入 PyQt6 前使用) | `resolve_backend(pref)`auto/webview2/qtwebengine 归一化;非法值/平台不支持 → 可见警告 + 平台默认,绝不阻断启动);`platform_default_backend()``webview2_module()`win32 门控的按需导入);`webengine_profile_name()/webengine_profile_dir()`(每实例独立 profile 目录:源码运行 `data/webengine/profile_<pid>_<ms>`,测试经 `HAOCODE_WEBENGINE_PROFILE_DIR` 重定向临时目录;只创建、从不清理);`is_root_or_container()`geteuid==0 / .dockerenv / .containerenv / .lxc / /proc/1/cgroup);`sanitize_chromium_flags()``--no-sandbox` 契约) |
| `main.py` | 修改 | sys.path 就绪后、导入 `MainWindow` 前调用 `sanitize_chromium_flags` 并打印最终 `QTWEBENGINE_CHROMIUM_FLAGS` |
| `ui/views/main_window.py` | 修改(手术式) | ① `core.webview2` 导入改为 `if sys.platform == "win32"` 门控(Linux 永不导入,不触达 pythonnet/Win32/WebView2 DLL/taskkill);② 浏览器创建处先 `resolve_backend(config["webview_backend"])` 并打印警告(原「读取配置」从 win32 分支内提前到分支外,Linux 非法值也有可见警告);③ QtWebEngine 路径创建本实例 `QWebEngineProfile``setPersistentStoragePath`/`setCachePath` 指向独立目录),`CustomWebPage(profile, browser)` |
| `ui/views/custom_web_page.py` | 修改 | 构造函数兼容 `CustomWebPage(profile, parent)` 与旧式 `CustomWebPage(parent)`(按 `isinstance(QWebEngineProfile)` 分派,防旧调用把 view 误当 profile |
| `requirements.txt` | 修改 | `pythonnet==3.1.0; sys_platform == "win32"``clr_loader==0.3.1; sys_platform == "win32"`;追加 Linux 源码运行最小步骤(apt 系统库清单、root/容器 `--no-sandbox` 说明、离屏测试命令) |
| `tests/_test_env.py` | 修改 | `isolate()` 增加 `HAOCODE_WEBENGINE_PROFILE_DIR` → 临时目录(setdefault,可覆盖) |
| `tests/smoke_offscreen.py` | 修改 | 同上(该文件不走 isolate) |
| `tests/test_renderer_matrix.py` | 新增 | R1/R3/R4/R5(见下)19 项 |
| `docs/agent-handoff/PLATFORM_PLAN.md` | 修改 | 「Linux 与 QtWebEngine 回落」追加 Linux 源码运行最小步骤 + 实现锚点 |
`vendor/webview2/` 与根 `WebView2Loader.dll` 未动;未新增产品功能/安装器/PyInstaller 改动。
## 关键设计决策与踩坑记录
1. **`QTimer.singleShot` 单位是毫秒**R4 首版写 `QTimer.singleShot(25, finish)` 期望 25s 兜底,实际 25ms 就触发 → 两个 worker 都报 `loaded=False`rc=1)。诊断时手动复现看到 `BACKSTOP t=0.4`(30ms 定时器在事件循环启动 ~0.35s 后即触发),定位后改为 `30000`
2. **QtWebEngineWidgets 必须先于 QApplication 导入**(否则 ImportError`QtWebEngineWidgets must be imported ... before a QCoreApplication instance is created`);**QWebEngineProfile 必须先于使用它的 page/view 创建**`setPersistentStoragePath/setCachePath` 必须在 profile 使用前调用。探针脚本先后踩中这两个顺序问题(前者 ImportError;后者在 QApplication 前建 profile 直接进程被杀 exit 127、stdout 缓冲丢失)。
3. **PyQt6-WebEngine 6.10 无 `QWebEnginePage.errorOccurred`**Qt 6.5+ API 未在此绑定暴露)→ 诊断改用 `loadFinished(ok)` + 手动 processEvents 循环对比三种 profile 配置(默认 profile / 命名 profile 无自定义路径 / 命名 profile + 自定义路径),三者 file:// 加载全部成功,证明「命名 profile + setPersistentStoragePath」组合可用。
4. **旧式位置调用兼容**`CustomWebPage(browser)` 的 view 会被新签名当作 `profile` 传入 → `TypeError: argument 1 has unexpected type 'QWebEngineView'`(smoke 前用探针暴露)。按类型分派(`isinstance(QWebEngineProfile)`)同时支持新旧调用。
5. **R3 Linux 模拟导入**:子进程 `sys.platform='linux'``import ui.views.main_window` 成功——因 `global_hotkey.py``from ctypes import wintypes` 只是类型定义(3.10 下跨平台可导入),`ctypes.windll` 访问全在 `if _is_windows` 内;`core.webview2` 模块级仅 stdlib 导入,但被 main_window 的 win32 门控挡住,`clr`/`clr_loader` 未进 `sys.modules`
## 验证结果(全部显式超时)
| 检查 | 结果 | 命令(超时) |
|---|---|---|
| `tests/test_renderer_matrix.py` | **19/19 PASS** | `PYTHONIOENCODING=utf-8 python tests/test_renderer_matrix.py`timeout 300 |
| `tests/test_wv2_guard.py`P0 遗留守卫) | **10/10 PASS** | `timeout 120` |
| `tests/test_debug_window.py` | **22 PASS / 0 FAIL** | `timeout 180` |
| `tests/smoke_offscreen.py` | **8/8 ALL PASS**,日志含 `[Renderer] QtWebEngine 独立 profile: <temp>/profile_<pid>_*` | `QT_QPA_PLATFORM=offscreen HAOCODE_RENDER=software QTWEBENGINE_CHROMIUM_FLAGS=--disable-gpu timeout 240` |
| `node tests/test_math_extract.js` | **39 passed, 0 failed** | `timeout 60` |
| `main.py` 真实启动链(offscreen + 临时配置,无 providers | 启动成功、无 traceback`[渲染] 最终 QTWEBENGINE_CHROMIUM_FLAGS = '--disable-gpu'``[Renderer] QtWebEngine 独立 profile: D:\...\data\webengine\profile_56256_*``[System] 浏览器内核: QtWebEngine``data/config.json` mtime 前后一致(未触碰) | `HAOCODE_CONFIG_FILE=<temp> QT_QPA_PLATFORM=offscreen ... timeout 45 python main.py`124=到点 kill,预期) |
| `main.py` 真实启动链:普通桌面 + `--no-sandbox` | 标志被剥离 + 告警:`[Renderer] ⚠️ ... 已剥离该标志并保留 Chromium 沙箱`,最终 flags 无 `--no-sandbox` | 同上(timeout 30 |
| 回归:`tests/run_tests.py` | **41 passed** | `timeout 300` |
| 回归:`test_bash_stream` / `test_tool_params` / `test_main_window_event_filter` / `test_config_isolation` | 全部 **ALL PASS / EXIT=0** | 各 `timeout 300` |
| 回归:`node tests/test_render_window.js` | **424 passed, 0 failed** | `timeout 120` |
| 回归:`tests/smoke_mode.py` | **ALL PASS** | `timeout 240` |
| P0-03 一致性抽查(文件存在、requirements marker、wv2 门控、单提示词占位符) | **10/10** | 纯 Python 读文件 |
## 测试点明细(test_renderer_matrix.py
- **R1.1R1.11** resolve_backend 矩阵(mock 平台):Win × auto/webview2/qtwebengine/非法字符串/非字符串/大小写容差;Linux × auto/webview2(警告+回落)/qtwebengine/非法;`webview2_module()` 非 Windows 返回 None。
- **R3.1** 子进程 `sys.platform='linux'` 导入 `ui.views.main_window``_wv2mod is None``core.webview2`/`clr`/`clr_loader` 均不在 `sys.modules`
- **R4.1R4.3** 两个 offscreen 子进程**并行**各建 `QWebEngineProfile``HAOCODE_WEBENGINE_PROFILE_DIR` 同基目录、各自 `parallel_<pid>` 子目录)+ `CustomWebPage(profile, view)` 载入本地 HTML:均 `WORKER_OK loaded=True`;两 profile 目录不同;进程退出后目录仍在(无互相清理)。
- **R5.1R5.4** `sanitize_chromium_flags` 契约(mock root/容器探测 + 捕获 stdout):普通桌面无标志原样;普通桌面剥离 `--no-sandbox`+告警;root/容器+显式保留+「高可见警告」;root/容器未设→原样+提示。
## 遗留 / 平台验证待办
- **Linux 真机验证(待 Ubuntu 主机)**:`pip install -r requirements.txt`(确认 pythonnet/clr_loader 被 marker 跳过)→ `python3.10 main.py`X11/Wayland+ 离屏套件 + `tests/test_renderer_matrix.py` 的 R3/R4(真实 Linux 平台而非模拟)。
- **root/容器真机**`--no-sandbox` 接受路径的运行时验证(本环境为普通 Windows 桌面,只能验证剥离路径;接受路径为纯逻辑 + mock 验证)。
- **Windows 真机 WebView2 首选路径:已验证(2026-09-17 真桌面)**。本机 D: 卷 .NET 把工程卷误判为“网络位置”导致 `clr.AddReference`LoadFrom)报 0x80131515;在 `core/webview2.py`**byte[] 回落**(快路径仍 `AddReference`,失败才 `Assembly.Load(byte[])`,正常机器行为不变;外层异常改 `BaseException` 以兼容 pythonnet 非 Exception 异常)。修复后真桌面启动日志:`[WV2] AddReference 路径加载失败 → 回落 Assembly.Load(byte[])``Runtime ready: 153.0.4234.32``controller ready``NavigationCompleted src=file:///.../ui/web/index.html`stderr 无 error/disposed/0x8007。`test_wv2_guard` 10/10 无回归。
- **Windows 真机 QtWebEngine 回落:已验证(2026-09-17 真桌面,`HAOCODE_FORCE_QTWEBENGINE=1`**。日志:`[WV2] ... 跳过 WebView2,回落 QtWebEngine`(强制标志生效,**未执行 taskkill**)→ `[Renderer] QtWebEngine 独立 profile: data/webengine/profile_<pid>_<ms>`per-instance 隔离 profile 真机生效)→ `[System] 浏览器内核: QtWebEngine`。聊天区截图像素统计:83% 亮背景 + 159 种颜色桶(含文本深色像素)= 真实 DOM 渲染,非空白。两模式主窗口截图已存 evidence:`win_real_wv2_mainwindow.png``win_real_qtwebengine_mainwindow.png`(半尺寸 PNG)。
- 交叉验证(T0 守卫真机):WV2 主程序运行期间另跑 diag_panel_scrollbar.pydiag 实例因 instance-lock 被占自动回落 QtWebEngine,未误杀主程序 WebView2 进程。