Files
Haocode/docs/agent-handoff/evidence/P1-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

61 lines
9.4 KiB
Markdown
Raw 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.
# 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 进程。