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.
9.4 KiB
9.4 KiB
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 改动。
关键设计决策与踩坑记录
QTimer.singleShot单位是毫秒:R4 首版写QTimer.singleShot(25, finish)期望 25s 兜底,实际 25ms 就触发 → 两个 worker 都报loaded=False(rc=1)。诊断时手动复现看到BACKSTOP t=0.4(30ms 定时器在事件循环启动 ~0.35s 后即触发),定位后改为30000。- 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 缓冲丢失)。 - PyQt6-WebEngine 6.10 无
QWebEnginePage.errorOccurred(Qt 6.5+ API 未在此绑定暴露)→ 诊断改用loadFinished(ok)+ 手动 processEvents 循环对比三种 profile 配置(默认 profile / 命名 profile 无自定义路径 / 命名 profile + 自定义路径),三者 file:// 加载全部成功,证明「命名 profile + setPersistentStoragePath」组合可用。 - 旧式位置调用兼容:
CustomWebPage(browser)的 view 会被新签名当作profile传入 →TypeError: argument 1 has unexpected type 'QWebEngineView'(smoke 前用探针暴露)。按类型分派(isinstance(QWebEngineProfile))同时支持新旧调用。 - 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.1–R1.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.1–R4.3 两个 offscreen 子进程并行各建
QWebEngineProfile(HAOCODE_WEBENGINE_PROFILE_DIR同基目录、各自parallel_<pid>子目录)+CustomWebPage(profile, view)载入本地 HTML:均WORKER_OK loaded=True;两 profile 目录不同;进程退出后目录仍在(无互相清理)。 - R5.1–R5.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_guard10/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.py,diag 实例因 instance-lock 被占自动回落 QtWebEngine,未误杀主程序 WebView2 进程。