From bc0b92bcdcb055f737b95351b58a3211751d3f83 Mon Sep 17 00:00:00 2001 From: sorrow404null Date: Thu, 17 Sep 2026 16:40:06 +0800 Subject: [PATCH] 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. --- AGENTS.md | 66 +++ ARCHITECTURE.md | 3 + docs/agent-handoff/CURRENT_STATE.md | 110 ++++ docs/agent-handoff/EXECUTION_STATE.md | 65 +++ docs/agent-handoff/KICKOFF_PROMPT.md | 362 ++++++++++++ docs/agent-handoff/PLATFORM_PLAN.md | 153 +++++ docs/agent-handoff/README.md | 55 ++ docs/agent-handoff/REPAIR_BACKLOG.md | 548 ++++++++++++++++++ docs/agent-handoff/VERIFICATION.md | 262 +++++++++ docs/agent-handoff/evidence/P0-01.md | 45 ++ docs/agent-handoff/evidence/P0-02.md | 56 ++ .../evidence/P0-PHASE_REGRESSION.md | 42 ++ docs/agent-handoff/evidence/P1-01.md | 90 +++ docs/agent-handoff/evidence/P1-02.md | 74 +++ docs/agent-handoff/evidence/P1-03.md | 60 ++ docs/agent-handoff/evidence/P1-04.md | 59 ++ docs/agent-handoff/evidence/P2-01.md | 50 ++ docs/agent-handoff/evidence/P2-02.md | 48 ++ docs/agent-handoff/evidence/P2-03.md | 95 +++ docs/agent-handoff/evidence/P2-04.md | 104 ++++ .../evidence/p2-02-codebox-corner-4x.png | Bin 0 -> 1875 bytes .../evidence/p2-02-outbox-render.png | Bin 0 -> 2420 bytes docs/agent-handoff/evidence/p2-02-panel.png | Bin 0 -> 61139 bytes .../win_real_qtwebengine_mainwindow.png | Bin 0 -> 24609 bytes .../evidence/win_real_wv2_mainwindow.png | Bin 0 -> 29657 bytes 25 files changed, 2347 insertions(+) create mode 100644 AGENTS.md create mode 100644 docs/agent-handoff/CURRENT_STATE.md create mode 100644 docs/agent-handoff/EXECUTION_STATE.md create mode 100644 docs/agent-handoff/KICKOFF_PROMPT.md create mode 100644 docs/agent-handoff/PLATFORM_PLAN.md create mode 100644 docs/agent-handoff/README.md create mode 100644 docs/agent-handoff/REPAIR_BACKLOG.md create mode 100644 docs/agent-handoff/VERIFICATION.md create mode 100644 docs/agent-handoff/evidence/P0-01.md create mode 100644 docs/agent-handoff/evidence/P0-02.md create mode 100644 docs/agent-handoff/evidence/P0-PHASE_REGRESSION.md create mode 100644 docs/agent-handoff/evidence/P1-01.md create mode 100644 docs/agent-handoff/evidence/P1-02.md create mode 100644 docs/agent-handoff/evidence/P1-03.md create mode 100644 docs/agent-handoff/evidence/P1-04.md create mode 100644 docs/agent-handoff/evidence/P2-01.md create mode 100644 docs/agent-handoff/evidence/P2-02.md create mode 100644 docs/agent-handoff/evidence/P2-03.md create mode 100644 docs/agent-handoff/evidence/P2-04.md create mode 100644 docs/agent-handoff/evidence/p2-02-codebox-corner-4x.png create mode 100644 docs/agent-handoff/evidence/p2-02-outbox-render.png create mode 100644 docs/agent-handoff/evidence/p2-02-panel.png create mode 100644 docs/agent-handoff/evidence/win_real_qtwebengine_mainwindow.png create mode 100644 docs/agent-handoff/evidence/win_real_wv2_mainwindow.png diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..276a5ce --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,66 @@ +# 仓库协作规则 + +haocode 是面向 Windows 和 Linux 的 PyQt6 桌面 AI Agent 客户端。Windows 首选 WebView2,失败时回退 QtWebEngine;Linux 只使用 QtWebEngine。`core/agent/` 保持无 GUI,并继续与 pi 对齐。 + +## 权威交接入口 + +处理修复、跨平台、测试、项目结构或交接任务前,必须先读 `docs/agent-handoff/README.md`,再按其中的触发条件读取对应文档。`readme.md`、`Frame.md` 和 `ARCHITECTURE.md` 只作历史背景,不能未经核验就当作事实源。 + +`data/config.json` 是不透明的本机密钥文件。Agent 不得打开、读取、搜索、打印、复制、编辑它,也不得让宽范围内容搜索包含它。测试必须使用临时配置。 + +## 项目结构 + +- `main.py`:入口、UTF-8 标准输出保护、Chromium 参数和 `MainWindow` 创建。 +- `core/`:无 GUI 后端,包括 SQLite 会话树、Qt 工作线程、WebView2 后端和日志。 +- `core/agent/`:与 pi 对齐的循环、流式处理、重试/压缩恢复,以及 `read`/`bash`/`write`/`edit` 工具;可直接进行纯 Python 测试。 +- `ui/views/`:PyQt6 窗口与桥接。`main_window.py`(约 6000 行)是当前中心,还包括 `bash_panel.py`、`chat_bridge.py`、`wv2_view.py`、`custom_web_page.py`、`debug_window.py` 和 `system_tools/`。 +- `ui/web/`:离线网页层,包括 `index.html`、`app.js`、`style.css` 及 vendored marked/DOMPurify/KaTeX/highlight.js。 +- `data/`:源码运行时数据,包括不透明的 `config.json`、自动创建的 `chat_history.db` 和 `attachments/`。 +- `tools/builtin_tools/pdf_reader.py`、`svg/`(图标)、`SYSTEM_PROMPT.md`(每次请求重新读取,修改后无需重启)。 +- `vendor/webview2/` 和根目录 `WebView2Loader.dll`:Windows 运行依赖,**不得删除**。 +- `tests/`:测试、smoke、诊断和人工验证脚本。 + +## 常用命令 + +- `pip install -r requirements.txt`:安装当前依赖;Linux 依赖规范化见交接任务清单。 +- `python main.py`:从源码运行。Windows 可从 WebView2 回退 QtWebEngine;Linux 使用 QtWebEngine。 +- `python tests/run_tests.py`:只运行当前 agent-core 聚合,不会发现整个测试目录。 +- `node tests/test_math_extract.js`:运行前端公式提取测试。 +- `python tests/check_db_migration.py `:只读检查数据库迁移完整性。 + +## 编码规范 + +- 以 Python 3.10 语法为基线,使用 4 空格缩进、`snake_case` 函数/文件名和 `PascalCase` 类名;目标运行矩阵为 Python 3.10–3.12。仓库未配置 formatter/linter,修改时匹配周边风格。 +- 注释和文档使用中文;路径、命令和 API 标识符保持原文。 +- `requirements.txt` 只列源码确实导入的包;依赖变更必须有意为之。 +- `core/agent/` 必须保持无 GUI,确保可离屏测试。 + +## 测试规则 + +仓库不使用 pytest;测试是可直接运行的 Python/Node 脚本。`tests/run_tests.py` 当前只加载 agent-core 套件。`test_*.py` 表示聚焦自动化测试,`smoke_*.py` 表示离屏集成测试,`diag_*`/`verify_*`/`tune_*` 默认表示诊断或人工脚本,除非文件自身另有说明。 + +```bat +set PYTHONIOENCODING=utf-8 +set QT_QPA_PLATFORM=offscreen :: only for smoke_* GUI tests +python tests/test_tool_params.py +``` + +跨平台聚合入口(不改变任何独立命令):`python tests/run_all.py --group logic|offscreen|all`。每个条目独立子进程 + 临时目录 + 显式超时;依赖/平台不适用项打印明确 SKIP 理由;任一失败则退出码非零。默认不跑 `diag_*`/`verify_*`/`tune_*`/真实 API/真实桌面脚本。 + +导入 `MainWindow` 前,必须把 `core.db_manager._DEFAULT_DB` 指向临时数据库,并把 `HAOCODE_CONFIG_FILE` 指向临时配置。当前仍有代码绕过该变量;对应任务完成前,测试还必须 patch 相关模块缓存的路径,并证明真实运行时文件未被访问或修改。 + +## 调试铁律 + +- **所有调试命令、测试与诊断脚本执行都必须设置显式的最长耗时预算(timeout)**。 +- 若中途因超时跳出,先定位卡点,再允许延长预算重跑;**严禁不设超时让它无限卡死**。 + +## 变更粒度 + +当前快照没有 Git 历史。不得初始化 Git 或伪造提交;每批修改仍须保持可独立提交。未来提交信息使用简短祈使句并注明区域,例如 `core/agent: fix compaction cut-point`。 + +## Agent 注意事项 + +- 运行时可能在根目录写出 `compaction_diag.log`、`stream_diag.log`、`diag_shot_*.png` 等诊断产物。除非当前任务明确负责清理,否则不得删除。 +- shell 行为尚未规范化。目标契约是 Windows `cmd.exe`、Linux `/bin/bash -lc`;修改前先读 `docs/agent-handoff/PLATFORM_PLAN.md`。 +- 本阶段源码运行继续把数据放在项目 `data/` 下;打包路径和 AppData/XDG 迁移延后。 +- 不得删除 `vendor/webview2/` 或根目录 `WebView2Loader.dll`。 diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index bd00d3b..cf39878 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -1,5 +1,8 @@ # 项目文件架构 +> [!WARNING] +> **历史资料,不是当前事实源。** 本文仅保留旧架构与故障记录。处理修复、跨平台、测试、项目结构或交接任务时,先读 [`docs/agent-handoff/README.md`](docs/agent-handoff/README.md)。 + > 仅描述目录与文件的基础组织,不涉及具体实现细节。 ``` diff --git a/docs/agent-handoff/CURRENT_STATE.md b/docs/agent-handoff/CURRENT_STATE.md new file mode 100644 index 0000000..7ce636a --- /dev/null +++ b/docs/agent-handoff/CURRENT_STATE.md @@ -0,0 +1,110 @@ +# 当前代码状态与结构审计 + +> 用途:处理结构、命名、模块边界或技术债任务前读取。本文只记录已经由源码确认的现状;精确实现仍以当前源码为准。修复顺序和验收要求见 [REPAIR_BACKLOG.md](REPAIR_BACKLOG.md) 与 [VERIFICATION.md](VERIFICATION.md)。 + +## 结论 + +项目的目录分层和文件命名总体符合 Python、JavaScript 与 Qt 项目的常见习惯,不需要为了“看起来规范”而批量改名或搬目录。当前主要风险不在命名,而在以下位置: + +- `ui/views/main_window.py` 承担窗口组装、浏览器选择、会话、附件、截图、弹层和大量事件处理,已经形成高耦合中心。 +- `ui/web/app.js` 同时负责渲染、流式更新、历史、分支和交互状态,Python 与 JavaScript 之间没有显式协议定义。 +- Windows 专用的 WebView2、全局热键、进程终止和路径假设尚未被完整隔离,Linux 不能仅凭“Python 跨平台”获得支持。 +- 测试入口聚合不完整(P2-04 待实施);配置重定向与运行时数据隔离已由 P0-01 统一(见 [REPAIR_BACKLOG.md](REPAIR_BACKLOG.md) P0-01 与 evidence/P0-01.md)。 +- 根目录旧文档混有过时描述;它们不能继续作为实现依据。 + +当前阶段允许修复错误、添加测试和添加小型平台适配器,但不移动或拆分现有模块。超大文件是已记录技术债,不是本轮重构授权。 + +## 目录职责 + +| 路径 | 当前职责 | 审计判断 | +| --- | --- | --- | +| `main.py` | 进程入口、标准输出保护、QtWebEngine 启动参数、`QApplication` 和主窗口创建 | 入口职责基本合理;渲染参数需要区分平台 | +| `core/` | 数据库、LLM 工作线程、日志和 WebView2 后端 | 名称规范;仍含 Windows 专用实现和配置路径旁路 | +| `core/agent/` | 与 pi 对齐的 agent 循环、上下文、压缩、恢复和内置工具 | 应继续保持 GUI-free、可离线测试 | +| `ui/views/` | PyQt6 窗口、桥接层和浏览器包装 | 边界最薄弱;`main_window.py` 是主要风险点 | +| `ui/views/system_tools/` | 文件读取、全局热键、屏幕截图 | 适合作为平台适配落点;当前实现以 Windows 为中心 | +| `ui/web/` | 离线 HTML/CSS/JavaScript 聊天界面和 vendored 前端库 | 无需构建步骤;历史窗口状态机尚未独立 | +| `tools/builtin_tools/` | 面向 agent 的独立实用工具 | 当前仅有 PDF 读取器;与其他两类“工具”需用全路径区分 | +| `tests/` | 纯逻辑测试、offscreen smoke、诊断与人工验证脚本 | 命名前缀有约定,但聚合入口没有覆盖全部自动化测试 | +| `data/` | 源码运行时的配置、数据库和附件 | 本阶段保留项目内路径;凭据处理遵循 [README.md](README.md) 的硬性边界 | +| `vendor/webview2/`、`WebView2Loader.dll` | Windows WebView2 运行依赖 | 只允许 Windows 路径加载,不能成为 Linux 启动前置条件 | +| `svg/` | UI 图标 | 目录职责清晰 | + +## 命名规范审计 + +| 范围 | 现状 | 结论 | +| --- | --- | --- | +| Python 文件、函数、变量 | 基本使用 `snake_case` | 符合 PEP 8 常规写法 | +| Python 类 | 基本使用 `PascalCase` | 符合惯例 | +| Qt 覆盖方法 | 使用 `eventFilter`、`closeEvent` 等 Qt 固定名称 | 正确例外,不应改成 `snake_case` | +| QWebChannel/JS 可调用接口 | 存在 camelCase 名称 | 跨语言协议名称可保留,但必须集中记录 | +| JavaScript | 主要使用 `camelCase` | 符合惯例 | +| CSS | 选择器和属性使用 Web 常规形式 | 符合惯例 | +| 测试文件 | `test_*`、`smoke_*`、`diag_*`、`verify_*`、`tune_*` | 前缀表达运行性质,约定合理 | + +以下名称有可读性或发布规范问题,但不应在当前修复阶段批量改名: + +- `Frame.md` 语义过宽,且与 `readme.md`、`ARCHITECTURE.md` 的职责重叠。旧文档只作历史资料。 +- `readme.md` 的大小写不影响源码运行;是否改成 `README.md` 留到仓库整理阶段。 +- `db_manager.py`、`llm_engine.py` 等名称合规,但 “manager/engine” 隐藏了较宽职责;先通过边界文档约束新增代码。 +- `core/agent` 的版本字符串不是标准 PEP 440 形式;打包阶段再统一。 +- “工具”同时指 agent 的 `read/bash/write/edit`、`tools/builtin_tools/` 实用工具和 `ui/views/system_tools/` 桌面集成。任务与文档必须使用完整路径或明确类别,不能只写“tool”。 + +## 已确认的结构与行为缺陷 + +### 配置和测试隔离 + +- 已解决(P0-01):所有运行时配置读取统一经 `core/config_paths`(`HAOCODE_CONFIG_FILE` 优先);`tests/test_config_isolation.py` 用打开路径拦截器证明真实配置从未被打开。 +- 新测试必须在导入 `MainWindow` 及相关模块前同时重定向数据库和配置(统一用 `tests/_test_env.py` 的 `isolate()`),并检查被测模块是否缓存了路径常量。 +- 少数 GUI 测试(`smoke_offscreen.py`、`smoke_mode.py`、`smoke_copy_session.py`)自身只重定向数据库、未设置 `HAOCODE_CONFIG_FILE`;独立运行时由环境注入临时配置,P2-04 聚合入口将按子进程强制注入。 +- `tests/run_tests.py` 当前不是全套测试聚合器,不能把一次成功运行等同于整个 `tests/` 目录通过。 + +### UI 和事件处理 + +- 已解决(P0-02):`MainWindow` 重复定义的 `eventFilter` 已合并为一份;发送规则收进 `send_message(from_enter=...)` 单一实现(见 evidence/P0-02.md)。 +- UI 层存在直接 SQL 和跨模块私有成员访问,导致数据库、窗口和渲染状态相互渗透。当前只修复会造成实际错误的调用,不展开分层重构。 +- 原生窗口型 WebView2 与 Qt 弹层的遮挡关系需要逐个验证;重命名弹层是已确认案例,不能假定所有 QWidget 弹层都会自然显示在 WebView2 之上。 + +### Python 与 JavaScript 边界 + +- 当前通过 `ui/views/chat_bridge.py`、直接 JavaScript 执行以及硬编码函数名传递状态,没有协议版本、载荷 schema 或统一错误回传。 +- 当前没有“按边界消息 ID + 方向”请求历史页的 JS→Python 协议。现有消息缓冲也不足以重建附件、时间线、工具输出和分支关系完整的历史项。 +- 建立历史滑动窗口时,必须传递完整消息描述符,并用会话/分支 generation 丢弃过期响应;不能让前端自行拼接不完整历史。 +- 跨语言公开名称一旦落地即视为协议。实现 Agent 应把请求、响应、错误和重置事件集中列在同一处,并用契约测试锁定。 + +### 平台边界 + +- `core/webview2.py`、`ui/views/wv2_view.py` 和相关进程处理是 Windows 专用路径;Linux 只能选择 QtWebEngine。 +- `ui/views/system_tools/global_hotkey.py` 使用 Windows 原生 API,Linux 导入和运行路径尚未隔离。 +- agent 的 bash 工具使用隐式 shell 选择:Windows 通常落到 `cmd.exe`,Linux 通常落到 `/bin/sh`,不满足既定的 `/bin/bash -lc` 契约。 +- Windows 使用进程树终止手段;POSIX 侧尚无对完整进程组的等价取消/超时保证。 +- 项目中的 Windows 路径、动态库和 WebView2 探测不能在 Linux 启动阶段被无条件访问。 + +### 文档和仓库状态 + +- `ARCHITECTURE.md` 仍含旧产品名和失效目录;误拼接的外部修复文档已经移除,文件顶部已明确标记为历史资料。 +- 第三方修复要求提到的 `haocode.spec`、`pyi_rth_trace.py`、`tests/diag_render_scale.py`、`tests/diag_panel_scrollbar.py` 和 `tests/diag_rename_overlay.py` 当前不存在。上述三个 `tests/diag_*` 脚本可按任务新建;两个打包文件属于后续阶段。 +- 工作区没有可用 Git 历史,不能引用不存在的基线提交,也不应擅自初始化仓库。任务清单仍按可独立提交的粒度书写,供未来接入版本控制。 +- 源码树存在 `__pycache__`、数据库、日志和锁等运行产物。当前不做清理工程;测试必须使用临时位置,避免继续污染生产数据。 + +## 应保持的边界 + +1. `core/agent/` 保持 GUI-free;纯 agent 行为可不创建 `QApplication` 直接测试。 +2. 浏览器差异留在 WebView2/QtWebEngine 适配层,不把后端判断散落到业务逻辑。 +3. 系统热键、截图和 shell 通过最小平台适配接口选择实现;Windows 模块与 Linux 模块只在对应平台延迟导入。 +4. 数据库存取继续由 `core/db_manager.py` 承担;新的 UI 功能不要增加直接 SQL。 +5. Python↔JavaScript 载荷使用完整、可测试的描述符;DOM 只保存当前窗口,不承担持久化或完整会话真相。 +6. 运行时秘密边界、任务范围和验证要求分别以本目录的 `README.md`、`REPAIR_BACKLOG.md` 和 `VERIFICATION.md` 为准。 + +## 本阶段明确延后 + +- 拆分 `main_window.py`、`app.js` 或迁移现有模块。 +- 批量重命名文件、类或公开跨语言接口。 +- PyInstaller/其他打包配置、安装器、AppData/XDG 数据目录迁移。 +- WebKitGTK 或其 Qt 封装。 +- 通用键盘钩子框架;Linux 只实现现有截图快捷键需要的能力。 +- agent shell 沙盒、命令审批、路径权限边界。 +- 新工具注册系统、参考其他 harness 增加功能或改变 pi 对齐目标。 +- Ubuntu 以外 Linux 发行版的支持承诺。 + +结构或命名任务只有在“发现的问题已逐项归类为确定缺陷、已接受技术债或明确延后,并且没有借机移动/拆分模块”时才算审计完成。 diff --git a/docs/agent-handoff/EXECUTION_STATE.md b/docs/agent-handoff/EXECUTION_STATE.md new file mode 100644 index 0000000..391dbba --- /dev/null +++ b/docs/agent-handoff/EXECUTION_STATE.md @@ -0,0 +1,65 @@ +# 全量修复执行状态 + +状态:ACTIVE +总目标:完成 REPAIR_BACKLOG.md 中所有待实施任务 + +## 调试铁律(跨压缩持久,每次恢复后必须遵守) + +- **所有调试命令、测试与诊断脚本执行都必须设置显式的最长耗时预算(timeout)**。 +- 若中途因超时跳出,先定位卡点,再允许延长预算重跑;**严禁不设超时让它无限卡死**。 + +当前任务:全部完成(P0-01 → P2-04) +当前阶段:COMPLETE(自动化部分;Windows 真机双渲染路径已验证;Linux 真机平台验证项见各任务 evidence) +最后完成动作:Windows 真机验证 —— ① `core/webview2.py` 加 byte[] 加载回落(本机 D: 卷 .NET “网络位置”怪癖),真桌面 WebView2 启动成功(Runtime 153.0.4234.32、controller ready、NavigationCompleted、截图像素证实聊天区真实 DOM 渲染);② `HAOCODE_FORCE_QTWEBENGINE=1` 真桌面回落验证(独立 per-instance profile、无 taskkill、渲染正常);③ P2-02 真机截图(diag_panel_scrollbar 真桌面 ALL PASS,三图已更新);④ T0 守卫真机交叉验证(diag 实例被锁自动回落,未误杀主程序 WebView2);证据更新 evidence/P1-03.md、evidence/P2-02.md、evidence/P2-03.md +下一步唯一动作:P2-03 人工清单 1–5 可在本机真实 WebView2 模式下走查(app 现可正常以 WV2 启动,当前前台实例为强制 QtWebEngine 模式,重开即默认 WV2);其余为 Linux 真机验证(P1-03/P1-04),需 Ubuntu 桌面主机 +当前修改文件:core/webview2.py(byte[] 加载回落 + BaseException 防护)、docs/agent-handoff/evidence/{P1-03,P2-02,P2-03}.md、evidence/win_real_*_mainwindow.png(新) +最近测试结果:test_wv2_guard 10/10(修复后无回归);diag_panel_scrollbar 真桌面 ALL PASS;run_all 25/25(前次终态) +尚未验证的平台:Linux X11、Linux Wayland(P1-03 启动链、P1-04 X11 XGrabKey 真实注册/命中、Wayland portal 三态均需真机);Windows 真机双渲染路径(WebView2 + QtWebEngine 回落)已于 2026-09-17 验证,仅剩 P2-03 人工遮罩走查(1–5,可本机执行) +阻塞项:无 +观察项: +- 【P1-01 教训·offscreen QtWebEngine 诊断】未 `window.resize()`+`show()` 前 `innerHeight=0`,锚点/滚动几何全废;诊断必须先 resize+show、等待 `innerHeight>0`、再显式 `load_messages_to_web` 重载 +- 【P1-01 教训】`runJavaScript` 回调不能返回 DOM 元素(转换失败);`wait_until` 条件一律返回原语(`cond ? 1 : 0`) +- 【P1-01 教训】批次渲染守卫(`__rwPageRendering`)必须在**调度时刻**捕获,延迟回调(rAF+setTimeout)触发时批次已结束、标志已复位,届时再读会漏放 `scrollIntoView` 触发 'newer' 振荡 +- 【P1-01 设计】页 = 半窗(`size//2`),非整窗:整窗页会使向上换页锚点必被裁出窗口,≤2px 锚点恢复不可达 +- 【P1-02 教训】Windows 下 argv 列表形态 Popen 会被 `list2cmdline` 转义内部引号(`\"`),cmd.exe 不认 → 显式 cmd 契约必须用**字符串命令行** `cmd.exe /d /s /c ""` +- 【P1-02 发现】旧 `shell=True` 对带引号程序名实为直接 CreateProcessW(不经 cmd);显式 cmd 后行为统一且可预测 +- 【P1-03 教训】`QTimer.singleShot` 单位是**毫秒**(25 = 25ms),兜底预算要写 30000;事件循环启动前不触发 +- 【P1-03 教训】QtWebEngine 顺序硬约束:QtWebEngineWidgets 必须先于 QApplication 导入;QWebEngineProfile 必须先于使用它的 page/view 创建,且 setPersistentStoragePath/setCachePath 要在 profile 使用前调 +- 【P1-03 发现】PyQt6-WebEngine 6.10 未暴露 `QWebEnginePage.errorOccurred`(Qt 6.5+ API),加载失败诊断用 loadFinished(ok) + processEvents 循环 +- 【P1-03 教训】改构造函数签名必须保旧式位置调用:`CustomWebPage(browser)` 的 view 会被新首形参误当 profile,按 `isinstance(QWebEngineProfile)` 分派 +- 【P1-04 教训】PyQt6:`QThread.run()` 内任何未处理异常 = **abort 整个进程**(退出码 127、无 traceback、stdout 缓冲丢失,极难诊断)→ QThread.run() 必须顶层 try/except 全捕获 + 日志 +- 【P1-04 教训】`ctypes.byref(x)` 返回 CArgObject:真实 CDLL 调用正常,传给测试替身(普通可调用对象)会 TypeError —— 替身端 `getattr(arg, "_obj", arg)` 还原原对象 +- 【P1-04 教训】Windows 上 `os.pipe()` 的 fd 不能可靠用于 select()(10093/10038)——跨平台可 select 假 fd 用 `socket.socketpair()`;且 `a.send()` 的数据在 b 的接收缓冲(方向别写反) +- 【P1-04 发现】`file://` URI 解析用剥前缀+unquote 而非 urlparse().path:`file://D%3A%5Cx`(无第三斜杠)会被当 netloc;gdbus portal 的 FilePicked/Finished 信号都发在 request 对象上 +- 【P2-01 教训】Qt:对刚展开、布局尚未 flush 的 QPlainTextEdit 立即设水平滚动值,随后的挂起 resize flush 会把滚动清零——真实用户无法在未渲染框上滚动,属测试时序伪影;测试设滚动前必须先 settle(N1×N2 探针矩阵证实 N2≥1 即稳定) +- 【P2-01 教训】offscreen harness 收尾用 `os._exit`:`sys.exit` 后 QtWebEngine 渲染/GPU 子进程可能不回收 → 解释器挂起至 timeout;管道执行时只看输出会误判通过(必须验 EXIT 码) +- 【P2-02 教训】offscreen 下 QPlainTextEdit 的 `grab()` 文档区不填充(黑图)但 `panel.grab()` 正常;像素断言一律走 `widget.render(painter)`(渲染到透明 QPixmap),且先做健全性检查(文本色/handle 色像素计数>0)防黑图假通过 +- 【P2-02 教训】PyQt6:`Qt.Vertical`→`Qt.Orientation.Vertical`;`QStyleOptionSlider()` 无参构造(不接受 widget);`subControlRect(cc, opt, sc, widget)` 参数序 + 滚动条箭头子控件枚举名是 `SC_ScrollBarSubLine/AddLine`;`CC_ScrollBar` 属于 `QStyle.ComplexControl` +- 【P2-02 发现】裸控件(无样式表祖先)`style()` 是基础风格,空 option 下 subControlRect 返回 0 矩形——「原生参照」只能靠 sizeHint/实际几何对照(原生横条 14px vs 面板 8px) +- 【P2-03 发现】offscreen/无真实事件循环时 `processEvents()` **不处理 DeferredDelete**:`deleteLater()` 后必须显式 `QCoreApplication.sendPostedEvents(None, QEvent.Type.DeferredDelete)` 才会真正删除(offscreen 测试断言"已删除"前必须冲刷;生产事件循环常驻不受影响) +- 【P2-03 教训】PyQt6 `setGeometry` 无 `(QPoint, QSize)` 重载→构造 `QRect(tl,size)`;`QTest.mouseClick(widget, button, modifier, pos)` 第 3 参是 **modifier**;`QTest` 在独立模块 `PyQt6.QtTest` +- 【P2-03 发现】`core/webview2.py` `get_environment()` 含 `taskkill /F /IM msedgewebview2.exe` + 共享默认 profile 不可重定向 → 无人值守自动化不得走真实 WebView2 启动路径;真机验收走人工清单(拥有者顶层 Tool 窗 z 序上必盖 WebView2 原生子 HWND,机制与生产已验证的 AttachmentPreviewOverlay 同构) +- 【P2-03 结构判据】顶层窗可带 owner parent:`setWindowFlags(Tool|Frameless)` 后 `parentWidget()` 仍可非 None;判据是 `isWindow()` / `window() is self`,不是 `parentWidget() is None` +- 本机无系统级 Python 3.10;已用 uv 安装 CPython 3.10.21(用户目录托管)并重建项目内 `.venv`(Python 3.10.21),与文档基线对齐 +- WSL 存在 Ubuntu-22.04(当前 Stopped,python3.12.3,未装 PyQt6);未检测到 X11/Wayland 桌面会话 +- `tests/diag_live_agent.py:19`、`tests/tune_model_popup.py:155` 直接引用真实配置路径;属 live/tune 脚本,默认聚合排除,不属 P0-01 允许范围 +- `readme.md` 提及的 `haocode.spec`/`pyi_rth_trace.py` 不在本快照内(打包属后续阶段,不影响源码运行) + +环境探测(只读): +- OS:Windows 11 10.0.26200 x64 +- Python:默认 3.13 / 3.12.10,无 3.10;`.venv` = **3.10.21**(uv 重建,P0-01)+ PyQt6 6.10.2 + PyQt6-WebEngine 6.10.0 + openai 2.26.0 + pythonnet 3.1.0 + PyMuPDF 1.28.0 +- WebView2 Runtime:153.0.4234.32 存在 +- WSL:Ubuntu-22.04(Stopped)、docker-desktop(Running) + +任务状态表: +- P0-01: COMPLETE +- P0-02: COMPLETE +- P0-03: COMPLETE(2026-09-16 复核通过,文档一致性修订已固化) +- P1-01: COMPLETE +- P1-02: COMPLETE(Windows 侧自动化全绿;Linux 进程组用例 C3 待 Linux 环境运行生效) +- P1-03: COMPLETE(Windows 侧自动化 + offscreen 真实启动链全绿;Linux 真机与 root/容器 `--no-sandbox` 接受路径待对应环境验证) +- P1-04: COMPLETE(Windows 侧自动化全绿:路由矩阵/X11 替身/portal 替身/offscreen 真实启动链;Linux X11 XGrabKey 真实注册与 Wayland portal 授权/取消/无 portal 三态待真实 Linux 宿主手动验证,见 evidence/P1-04.md) +- P2-01: COMPLETE(两栏启动倒序 + 状态保持,offscreen 全量覆盖,无平台相关项;见 evidence/P2-01.md) +- P2-02: COMPLETE(offscreen 全量覆盖 + 无泄漏断言;Windows/Linux 真机截图待对应环境运行 diag_panel_scrollbar.py;见 evidence/P2-02.md) +- P2-03: COMPLETE(offscreen 结构+行为全量覆盖 34/34;审计确认唯一受影响遮罩;Windows 真机 WebView2 人工验收清单见 evidence/P2-03.md) +- P2-04: COMPLETE(聚合入口 tests/run_all.py:Windows 25/25 PASS;WSL/3.12 FAIL 0 + 21 有理由 SKIP;夹具演示非零退出码;独立/聚合输出一致;暴露并修复 T9 陈旧断言与 compaction.py 3.11+ dataclass 缺陷;见 evidence/P2-04.md) diff --git a/docs/agent-handoff/KICKOFF_PROMPT.md b/docs/agent-handoff/KICKOFF_PROMPT.md new file mode 100644 index 0000000..719105a --- /dev/null +++ b/docs/agent-handoff/KICKOFF_PROMPT.md @@ -0,0 +1,362 @@ +# haocode 全量修复任务:无人值守连续执行 + +你是本次唯一的开发工作 Agent。用户将长时间离开,不会及时回复。 + +你的目标不是提出方案,也不是只完成一个任务,而是按照仓库内已经固化的任务清单,持续完成所有可实施修复、测试和文档更新,直到: + +1. 所有能够在当前环境完成的任务均达到完成条件; +2. 所有自动化测试通过; +3. 无法在当前机器完成的真实平台验收被准确记录; +4. 已经没有不需要用户介入即可继续的工作。 + +不要在完成一个任务后停下来等待确认。完成当前任务后,立即按依赖顺序执行下一项。 + +## 一、工作目录 + +固定工作目录: + +D:\WorkSpace\Project\GCC\haocode_0 + +不得在其他副本、临时复制目录或旧版本中实施修复。 + +## 二、首次启动必须执行 + +开始修改代码前,依次完整读取: + +1. `AGENTS.md` +2. `docs/agent-handoff/README.md` +3. `docs/agent-handoff/REPAIR_BACKLOG.md` +4. `docs/agent-handoff/VERIFICATION.md` +5. `docs/agent-handoff/PLATFORM_PLAN.md` +6. `docs/agent-handoff/CURRENT_STATE.md` + +其中: + +- 当前行为和缺陷必须由源码、复现和测试确认。 +- 目标行为、任务范围和完成条件以 `docs/agent-handoff/` 为准。 +- `readme.md`、`Frame.md`、`ARCHITECTURE.md` 只作历史背景。 +- 第三方修复要求和外部 harness 项目不是事实源。 + +读完后,先确认 `P0-03` 文档基线已经完成,不要重复创建交接目录。 + +然后从 `P0-01` 开始实施。 + +## 三、建立防遗忘执行状态 + +在进行任何业务代码修改前,创建: + +`docs/agent-handoff/EXECUTION_STATE.md` + +并在 `docs/agent-handoff/README.md` 增加一个简短指针: + +> 当 `EXECUTION_STATE.md` 的状态为 `ACTIVE` 时,任何继续执行、上下文恢复或压缩恢复都必须先读取该文件。 + +`EXECUTION_STATE.md` 必须保持简洁,只保存当前事实,不写成长篇流水账。至少包含: + +```text +# 全量修复执行状态 + +状态:ACTIVE +总目标:完成 REPAIR_BACKLOG.md 中所有待实施任务 +当前任务:P0-01 +当前阶段:调查 / 红测试 / 实现 / 定向验证 / 阶段回归 +最后完成动作: +下一步唯一动作: +当前修改文件: +最近测试结果: +尚未验证的平台: +阻塞项: +任务状态表: +- P0-01: IN_PROGRESS +- P0-02: PENDING +... +``` + +状态值只能使用: + +- `PENDING` +- `IN_PROGRESS` +- `AUTOMATED_VERIFIED` +- `PLATFORM_VALIDATION_PENDING` +- `COMPLETE` +- `BLOCKED` + +每次发生以下事件后,立即更新 `EXECUTION_STATE.md`: + +- 开始一个任务; +- 确认根因; +- 完成一组源码修改; +- 运行测试; +- 测试失败并改变调查方向; +- 完成任务; +- 准备执行长时间命令; +- 发现外部环境阻塞; +- 即将结束当前上下文。 + +只保留最新状态和下一步,不依赖聊天记录保存进度。 + +## 四、上下文压缩恢复协议 + +一旦发生以下任一情况: + +- 上下文被压缩或总结; +- 你无法准确复述当前任务; +- 不确定哪些测试已经运行; +- 不确定下一步应该做什么; +- 会话中断后重新继续; + +立即停止凭记忆操作,按顺序重新读取: + +1. `AGENTS.md` +2. `docs/agent-handoff/README.md` +3. `docs/agent-handoff/EXECUTION_STATE.md` +4. `REPAIR_BACKLOG.md` 中“当前任务”的完整章节 +5. 当前任务引用的 `PLATFORM_PLAN.md` 或 `VERIFICATION.md` 章节 +6. `EXECUTION_STATE.md` 列出的当前修改文件 + +然后核对工作区实际状态和最近测试结果,再从“下一步唯一动作”继续。 + +聊天摘要不能代替 `EXECUTION_STATE.md`。不得因为上下文压缩重新设计范围、跳过测试或把未完成任务误判为完成。 + +## 五、永久硬约束 + +### 凭据铁律 + +`data/config.json` 是不透明的本机密钥文件。 + +绝对不得: + +- 打开; +- 读取; +- 搜索其内容; +- 打印; +- 复制; +- 修改; +- 计算散列; +- 制作快照; +- 让递归内容搜索包含它; +- 让其内容进入工作上下文或测试输出。 + +所有自动化测试必须使用临时配置和临时数据库。 + +验证真实配置未被访问时,使用打开路径拦截器、替身或访问记录;不得通过读取真实文件验证。 + +在 `P0-01` 完成前,不得运行可能绕过临时配置而读取真实配置的 GUI/LLM 测试。先审查测试入口并完成隔离。 + +### 范围约束 + +- 不初始化 Git,不创建提交,不伪造历史。 +- 不删除或清理用户现有数据库、附件、日志、锁文件或运行产物。 +- 不移动或拆分现有模块。 +- 不借修复重写 `main_window.py`、`app.js` 或弹窗系统。 +- 不增加新产品功能。 +- 不增加工具注册系统。 +- 不参考或移植 Claude Code、Codex、Grok Build、DeepSeek Harness。 +- 不改变 `core/agent/` 作为 pi Python 移植的定位。 +- 不增加 Agent shell sandbox、命令审批或路径权限边界。 +- Chromium sandbox 保持默认开启。 +- 不引入 WebKitGTK 或相关 Qt 封装。 +- 不做 PyInstaller、安装器、AppData/XDG 迁移或发行包。 +- 本阶段只保证源码运行。 +- `vendor/webview2/` 和根目录 `WebView2Loader.dll` 不得删除。 +- 产品内部 Agent 继续按原方式调用 LLM,不修改其产品行为。 +- 不把开发工作委派给其他 Agent;由你连续完成。 + +只修改当前任务“允许修改”中列出的文件。发现无关问题时记录到 `EXECUTION_STATE.md` 的“观察项”,不要顺手扩大范围。 + +## 六、任务执行顺序 + +严格按以下顺序连续执行: + +1. `P0-01` 配置路径与测试隔离 +2. `P0-02` 合并重复的 `MainWindow.eventFilter` +3. 复核已经完成的 `P0-03` +4. `P1-01` 双向消息渲染窗口 +5. `P1-02` Windows/Linux shell 与进程树终止 +6. `P1-03` Windows/Linux 渲染器启动链 +7. `P1-04` Linux 截图热键与截图实现 +8. `P2-01` Bash 任务按启动时间倒序 +9. `P2-02` 右侧 Bash 面板滚动条 +10. `P2-03` WebView2 原生窗口遮挡层 +11. `P2-04` 跨平台聚合测试入口 + +不得跳过依赖。某项存在真实平台验收阻塞时,先完成其实现和自动化验证,将状态设为 `PLATFORM_VALIDATION_PENDING`,然后继续所有不受该阻塞影响的后续任务。 + +## 七、每个任务的固定执行循环 + +对每个任务严格执行以下循环: + +### 1. 领取 + +- 在 `EXECUTION_STATE.md` 中把任务设为 `IN_PROGRESS`。 +- 完整读取该任务的“先读文件、允许修改、硬约束、目标测试、完成证据”。 +- 只加载当前任务需要的源码。 + +完成标准:能够准确列出当前任务允许修改的文件、禁止范围和验收条件。 + +### 2. 复现和根因 + +- 从实际源码出发确认附件描述是否正确。 +- 建立最小复现或失败测试。 +- 对确定性缺陷记录实际调用链、状态变化或平台差异。 +- 描述不准确时以代码证据纠正,不机械照抄文档中的猜测。 + +完成标准:测试或可重复证据在修复前能够暴露缺陷。 + +### 3. 实现 + +- 采用与现有代码风格一致的最小修改。 +- 优先复用现有接口和模块边界。 +- 只在确实能隔离平台差异或测试状态时增加窄辅助模块。 +- 不进行无关格式化、批量重命名或结构重构。 + +完成标准:失败复现转绿,且没有扩大任务行为面。 + +### 4. 定向验证 + +运行任务章节列出的全部目标测试。 + +每条测试记录: + +- 精确命令; +- 平台和环境; +- 退出码; +- PASS/FAIL/SKIP; +- 关键断言; +- 失败原因; +- 是否使用临时配置和数据库。 + +测试失败时进入诊断循环并继续修复,不能通过删除断言、放宽正确性要求或把失败改成 SKIP 获得通过。 + +完成标准:全部适用定向测试通过;不适用项有真实平台理由。 + +### 5. 自检 + +逐条核对当前任务的所有“硬约束”和“完成证据”。 + +检查: + +- 是否改了允许范围之外的文件; +- 是否引入了新功能; +- 是否碰到真实配置或数据库; +- 是否只验证了 happy path; +- 是否保留 Windows 现有行为; +- 是否误把 offscreen 当作真实桌面证据; +- 是否有未记录的测试失败。 + +完成标准:每条完成证据都有源码、测试输出或真机证据对应。 + +### 6. 固化进度 + +- 更新 `REPAIR_BACKLOG.md` 的任务状态。 +- 更新 `EXECUTION_STATE.md`。 +- 将简洁证据写入 `docs/agent-handoff/evidence/.md`。 +- 不粘贴大量完整日志,只记录命令、结果和证据文件路径。 +- 立即开始下一任务,不等待用户确认。 + +只有全部完成条件满足时才能标记 `COMPLETE`。 + +## 八、阶段回归 + +完成 P0、P1、P2 每个阶段后,运行 `VERIFICATION.md` 对应的阶段完整回归。 + +要求: + +- 定向测试不能代替阶段回归。 +- `tests/run_tests.py` 在修复聚合入口前不能被称为全套测试。 +- 平台不适用项必须明确显示 `SKIP` 和理由。 +- 共同逻辑测试失败不能以平台差异豁免。 +- 测试不得访问网络、真实 API 或真实凭据,除非任务明确要求且用户已经提供授权;当前没有该授权。 +- 不运行 `diag_live_*` 或 `smoke_live_*` 的真实 API 路径。 + +P2-04 完成后,使用新的聚合入口运行最终自动化集合,并保留每个独立测试的运行能力。 + +## 九、平台验收处理 + +先只读检测当前机器实际具备的环境: + +- Windows 版本; +- WebView2 是否可用; +- QtWebEngine 是否可用; +- 是否存在可用的 WSL/Ubuntu、X11 或 Wayland 环境。 + +不得为了补齐平台矩阵擅自安装操作系统、创建虚拟机或修改宿主机关键配置。 + +当前环境具备的平台必须完成真实验证。 + +当前环境不具备的平台: + +1. 完成平台适配代码; +2. 完成平台路由和替身自动化测试; +3. 记录缺少的真实环境; +4. 将任务标为 `PLATFORM_VALIDATION_PENDING`,不能标记 `COMPLETE`; +5. 继续执行其他任务。 + +不得伪造 Windows WebView2、Linux X11、Linux Wayland、portal、DPI、全局热键或截图的真机证据。 + +## 十、阻塞处理 + +用户正在休息。不要因为普通实现选择、测试失败或代码复杂而询问用户。 + +优先采用: + +1. 现有源码行为; +2. `docs/agent-handoff/` 中已经冻结的决策; +3. 最小、兼容、可测试的实现; +4. 将判断依据写入执行状态和证据文档。 + +只有遇到以下情况才允许停止: + +- 必须获取用户凭据; +- 必须执行不可逆或破坏性操作; +- 必须使用当前不存在的外部机器; +- 两个权威要求存在无法同时满足的真实矛盾; +- 连续诊断后确认没有任何不需要用户介入的工作可继续。 + +即使一个任务阻塞,也要继续所有不依赖该阻塞的任务。 + +## 十一、最终收尾 + +所有可执行工作完成后: + +1. 运行最终自动化聚合测试。 +2. 复核所有单文件测试入口仍可运行。 +3. 复核 `data/config.json` 未被测试访问,但不要读取它。 +4. 检查交接文档与最终代码是否一致。 +5. 更新 `CURRENT_STATE.md`,移除已经修复的“当前缺陷”表述。 +6. 更新 `REPAIR_BACKLOG.md` 中每项真实状态。 +7. 把 `EXECUTION_STATE.md` 状态改为: + - `COMPLETE`:所有自动化和真实平台条件均满足; + - `PLATFORM_VALIDATION_PENDING`:仅剩当前机器无法提供的真机验证; + - `BLOCKED`:仍有必须由用户决定或提供资源的问题。 +8. 创建 `docs/agent-handoff/FINAL_REPORT.md`。 + +`FINAL_REPORT.md` 必须包含: + +- 每个任务的最终状态; +- 根因与实际修复摘要; +- 修改文件清单; +- 自动化测试命令和结果; +- Windows/Linux 真机验证结果; +- 明确的未验证项; +- 对真实配置和数据库的隔离证明方式; +- 仍需用户处理的最少事项; +- 下一位 Agent 的恢复入口。 + +## 十二、最终回复格式 + +只有在没有可继续执行的工作时才回复用户。 + +最终回复必须先说明整体状态,然后依次给出: + +1. 已完成任务; +2. 修改范围; +3. 测试结果; +4. 真机平台证据; +5. 未完成或受阻事项; +6. `FINAL_REPORT.md` 和 `EXECUTION_STATE.md` 路径; +7. 用户醒来后需要执行的最少动作。 + +不要只回复“完成了”。不要隐藏失败、跳过项或未验证平台。 + +现在开始执行首次启动步骤,创建持久化执行状态,然后从 P0-01 连续工作,直到达到上述终止条件。 diff --git a/docs/agent-handoff/PLATFORM_PLAN.md b/docs/agent-handoff/PLATFORM_PLAN.md new file mode 100644 index 0000000..014760d --- /dev/null +++ b/docs/agent-handoff/PLATFORM_PLAN.md @@ -0,0 +1,153 @@ +# Windows / Linux 源码运行契约 + +> 触发条件:修改启动、浏览器、shell、进程取消、系统热键、截图、路径、依赖或平台测试前读取。本文定义目标行为,不代表这些能力已经实现。只有完成本文验收矩阵后,才能把对应环境标为“已验证”。 + +## 支持矩阵 + +| 环境 | Python | 浏览器后端 | 系统集成 | 状态定义 | +| --- | --- | --- | --- | --- | +| Windows x64 | 现有 Python 3.10 基线 | WebView2 首选;失败或 profile 被占用时回落 QtWebEngine | 现有 Windows 原生全局热键和截图 | 必须保持现有产品行为 | +| Ubuntu 22.04/24.04 x64 + X11 | Python 3.10–3.12 | 仅 QtWebEngine | X11 原生全局热键;现有截图交互的 X11 实现 | 本阶段 Linux 主支持目标 | +| Ubuntu 22.04/24.04 x64 + Wayland | Python 3.10–3.12 | 仅 QtWebEngine | `xdg-desktop-portal` 全局快捷键/截图能力 | portal 缺失或桌面不支持时明确报错 | +| `QT_QPA_PLATFORM=offscreen` | 同对应系统 | QtWebEngine 测试路径;Windows 跳过 WebView2 | 不验证真实全局热键和桌面截图 | 只用于自动化测试,不算桌面支持证据 | + +其他 Linux 发行版可以尝试源码运行,但必须标记“未验证”。Linux 不引入 WebKitGTK、WebKitGTK 的 Qt 包装层或第二套网页 UI。 + +## 平台选择原则 + +平台能力通过小型适配器隔离,不建立通用插件框架,也不移动现有模块。适配器至少覆盖以下三类能力: + +- 浏览器选择:Windows 尝试 WebView2 后回落 QtWebEngine;非 Windows 直接进入 QtWebEngine。 +- shell 生命周期:生成固定 shell argv、设置进程组选项、取消和超时时终止完整命令树。 +- 桌面集成:注册/注销 `Alt+S`,启动截图并以现有 Qt 信号返回结果或错误。 + +Windows 专用模块和 Linux 专用模块只在平台选择完成后延迟导入。Linux 启动链不得先访问 `ctypes.WinDLL`、Windows HWND、`taskkill` 或 WebView2 DLL;Windows 启动链不得依赖 DBus、portal 或 X11 包。 + +## 浏览器后端 + +### Windows + +1. 保持 WebView2 为首选,初始化失败时显示可诊断日志并回落 QtWebEngine,主窗口仍可使用。 +2. 保持现有实例锁语义。同一 WebView2 profile 已由另一个实例使用时,新实例回落 QtWebEngine,不与其争用 profile,也不结束对方进程。 +3. WebView2 原生子窗口继续使用现有包装层;修复弹层时只调整已确认的遮挡案例。 +4. 强制 QtWebEngine 回落必须有测试入口,以便在 Windows 上验证两条渲染路径。 + +### Linux 与 QtWebEngine 回落 + +1. 非 Windows 平台不探测、不导入、不加载 WebView2。 +2. 每个 QtWebEngine 应用实例使用独立 profile/storage 目录,避免多个 Chromium 实例争用同一 profile。源码阶段目录仍位于项目 `data/` 范围;自动化测试使用临时目录。 +3. 保持离线加载 `ui/web/` 资源;路径拼接必须兼容大小写敏感文件系统。 +4. QtWebEngine 必须在 `QApplication` 创建前完成项目所需的导入和 Chromium 环境设置。 +5. 保留现有软件渲染/GPU 配置入口。无效可选配置应输出明确警告并回落可启动默认值,不能让源码运行因可选渲染配置直接失败。 + +**Linux 源码运行(最小步骤,Ubuntu 22.04/24.04 x64):** + +```bash +# 1) 依赖(pythonnet/clr_loader 带 sys_platform == "win32" marker,Linux 自动跳过) +python3.10 -m venv .venv && . .venv/bin/activate +pip install -r requirements.txt +sudo apt install -y libnss3 libxkbcommon0 libfontconfig1 libdbus-1-3 libgl1 libegl1 libasound2t64 +# (22.04 无 t64 后缀,用 libasound2) + +# 2) 运行(普通用户;只走 QtWebEngine;每实例独立 profile:data/webengine/profile__*) +python3.10 main.py + +# 3) 仅当 root/容器启动且页面空白时,才显式禁用沙箱(启动会打印高可见风险警告): +QTWEBENGINE_CHROMIUM_FLAGS="--no-sandbox" python3.10 main.py + +# 4) 离屏自动化测试: +QT_QPA_PLATFORM=offscreen HAOCODE_RENDER=software QTWEBENGINE_CHROMIUM_FLAGS=--disable-gpu \ + python3.10 tests/smoke_offscreen.py +``` + +实现锚点:`core/renderer_backend.py`(后端解析 / profile 目录 / sandbox 标志处理);`main.py` 在导入 PyQt6 前调用 `sanitize_chromium_flags`;`ui/views/main_window.py` 以 `sys.platform == "win32"` 门控 `core.webview2` 导入(非 Windows 永不导入,不触达 pythonnet/Win32/WebView2 DLL/taskkill);`CustomWebPage(profile, parent)` 接收本实例独立 `QWebEngineProfile`。 + +### Chromium sandbox + +Chromium sandbox 与 agent 命令权限是两件事。默认保留 Chromium sandbox,项目代码不得普遍追加 `--no-sandbox`。仅当运行者明确为 root/容器场景配置现有 `QTWEBENGINE_CHROMIUM_FLAGS`,且启动代码确认处于该场景时才接受该标志,同时输出高可见警告。普通 Windows/Linux 桌面运行不得关闭 sandbox。 + +## Shell 和进程生命周期 + +“bash 工具”保留产品名称,但执行契约按平台固定: + +| 平台 | argv | `Popen` 要求 | 取消/超时 | +| --- | --- | --- | --- | +| Windows | `cmd.exe /d /s /c ` | `shell=False`;保持现有流式 stdout/stderr | 终止该命令的完整 Windows 进程树 | +| Linux | `/bin/bash -lc ` | `shell=False`、`start_new_session=True`;保持流式 stdout/stderr | 向进程组发 `SIGTERM`,短暂宽限后仍存活则发 `SIGKILL` | + +实现必须满足: + +- 取消、超时、窗口退出三条路径复用同一个幂等终止函数。 +- Linux 以 `os.getpgid(proc.pid)` 定位本次命令进程组,不按进程名杀进程,也不遗留孙进程。 +- 正常退出不进入强杀路径;进程已经结束时重复取消不抛出用户可见异常。 +- `cwd`、环境变量、编码和分块输出保持现有工具语义。命令字符串不在 Python 中按 `;`、`&&` 或管道自行拆分。 +- 平台 shell 名称进入诊断日志,避免把 Linux `/bin/sh` 误报成 bash。 + +## 系统提示词 + +只维护根目录一份 `SYSTEM_PROMPT.md`。每次请求继续重新读取公共正文,再由运行时生成一段短的平台信息并插入请求,不创建 Windows/Linux 两份完整提示词。 + +运行时段至少声明: + +- 当前操作系统和 shell:Windows `cmd.exe` 或 Linux `/bin/bash -lc`。 +- 路径格式和路径分隔符;Linux 文件名大小写敏感。 +- 对应 shell 的环境变量、命令连接和引号规则。 +- agent 没有额外 shell 沙盒或命令审批层,不要虚构这些能力。 + +平台段是请求构造的一部分,不写入会话历史,不参与压缩持久化。契约测试应验证公共提示词只有一份,且不同平台只改变运行时段。 + +## 配置和路径 + +1. 所有配置读取入口统一尊重 `HAOCODE_CONFIG_FILE`;测试在导入 UI/LLM 模块前把它指向临时文件。真实凭据文件按 [README.md](README.md) 的边界处理。 +2. 所有自动化测试同时把默认数据库改到临时目录,不能依赖开发机已有数据库或附件。 +3. 源码运行阶段继续使用项目内 `data/`。AppData、XDG Base Directory、安装器写入权限和配置迁移全部留到打包阶段。 +4. 使用 `pathlib` 或 `os.path` 组合路径,不拼接平台分隔符;资源存在性检查覆盖 Linux 大小写差异。 +5. WebView2 DLL、Windows 锁文件和 Windows 进程命令不成为 Linux 配置校验项。Linux portal/X11 依赖缺失也不能阻止不相关的聊天和 shell 功能启动。 + +## Linux 桌面集成 + +### X11 + +- 为现有 `Alt+S` 截图快捷键实现 X11 原生注册/注销,保持 `GlobalHotkeyThread` 对上层的信号语义。 +- 注册失败时返回具体原因并保留窗口内截图按钮或应用内快捷键;不得后台忙等键盘状态。 +- 屏幕像素获取走 X11 可用的原生抓屏路径;区域选择 UI 可以继续使用 Qt。 +- 截图继续提供现有 `QImage` 返回语义,覆盖多显示器和负坐标桌面布局。 + +### Wayland + +- 使用 `xdg-desktop-portal` 提供的桌面协议请求全局快捷键和截图,不使用 X11 API 假装支持 Wayland。 +- portal 请求必须异步处理授权、拒绝、取消和桌面不支持四种结果;UI 只禁用受影响能力,主程序继续运行。 +- 无法提供全局快捷键时给出明确、可操作的错误,不能静默降级成“看似注册成功”。窗口内按钮/快捷键仍按桌面允许范围工作。 +- portal 返回的截图 URI/数据先完成有效性检查,再转换为现有 `QImage`/附件流程;临时文件生命周期由适配器负责。 + +会话类型优先综合 Qt 平台名和 `XDG_SESSION_TYPE` 判断,并记录最终选择。环境变量与实际 Qt 后端矛盾时,选择可证明可用的后端并输出警告。 + +> **实现状态(P1-04,2026-07-17,证据 `evidence/P1-04.md`)**:`ui/views/system_tools/desktop_session.py` 按上文判定顺序实现 `session_kind()`(offscreen→unknown;QT_QPA_PLATFORM/WAYLAND_DISPLAY/XDG_SESSION_TYPE 综合);X11 热键 = `x11_hotkey.py`(ctypes→libX11 XGrabKey,同 `triggered` 信号语义,失败明确日志);Wayland 截图 = `portal_capture.py`(xdg-desktop-portal Screenshot,系统 gdbus CLI,异步 worker,授权/拒绝/不支持/超时四态明确)。**Wayland 全局快捷键**依赖 compositor 桌面协议(ext-global-shortcut 等),本版本无免依赖实现 → 按上文“无法提供全局快捷键时给出明确、可操作的错误”处理(日志明确说明 + 保留应用内 Alt+S/按钮),不静默降级。真机验证待 Ubuntu 22.04/24.04 X11/Wayland 会话。 + +## 依赖范围 + +- `requirements.txt` 只加入源码确实导入的 Python 包,并使用平台 marker 隔离 Windows/Linux 专用依赖。 +- Ubuntu 所需系统包另写安装说明,不把 apt 包名伪装成 pip 依赖。 +- 不增加 WebKitGTK、打包器、安装器或与现有功能无关的桌面框架。 +- 可选平台能力缺失时要局部报错;聊天、会话和 shell 的基本源码运行仍应可达。 + +## 验收矩阵 + +每行都要留下命令输出、日志或截图证据;人工桌面项不能用 offscreen 结果替代。具体测试分层见 [VERIFICATION.md](VERIFICATION.md)。 + +| 环境 | 必验操作 | 通过标准 | 证据 | +| --- | --- | --- | --- | +| Windows / WebView2 | 启动、流式聊天、弹层、截图、退出 | 选择 WebView2;弹层无已确认遮挡;退出不影响其他实例 | 后端日志 + 桌面截图 | +| Windows / 强制 QtWebEngine | 同一基本流程 | 不加载 WebView2;UI 和消息协议行为一致 | 后端日志 + 定向测试 | +| Windows / 第二实例 | 首实例占用 WebView2 profile 后启动第二实例 | 第二实例回落 QtWebEngine;首实例继续工作 | 双实例日志 | +| Ubuntu 22.04 X11 | 启动、聊天、`Alt+S`、区域截图、shell | 只使用 QtWebEngine;热键与截图成功;shell 为 bash | 日志 + 截图 + shell 测试 | +| Ubuntu 24.04 X11 | 同上 | 与 22.04 相同 | 日志 + 截图 + shell 测试 | +| Ubuntu 22.04/24.04 Wayland | portal 授权、拒绝、取消;截图和快捷键 | 授权路径可用;其余路径明确报错且主程序不退出 | portal 日志 + 桌面截图 | +| Linux 进程生命周期 | 命令创建子进程后取消和超时 | 父、子、孙进程全部退出;无同名进程误杀 | PID/进程组测试记录 | +| Windows 进程生命周期 | 命令创建子进程后取消和超时 | 本次命令树全部退出;其他进程不受影响 | PID 测试记录 | +| Windows/Linux 提示词 | 捕获请求构造结果 | 公共正文一致;shell/路径段与平台匹配;平台段不入历史 | 契约测试 | +| Windows/Linux 临时配置 | 使用临时配置和数据库运行 GUI smoke | 不访问项目真实配置/数据库;运行结束无生产数据变更 | 隔离断言 | +| Windows/Linux offscreen | 运行自动化 smoke | 可重复通过;明确不声称验证系统热键/截图 | 测试汇总 | +| root/容器 | 分别在未显式配置和显式配置下启动 | 默认保持 sandbox;显式 `--no-sandbox` 时打印警告 | 启动日志 | + +平台任务完成条件:矩阵中该阶段承诺的每一行都有证据;自动化定向测试和阶段全量测试均通过;Linux 启动链没有 Windows 专用导入;实现没有引入 WebKitGTK、打包工作、通用钩子框架或新的 agent 权限模型。 diff --git a/docs/agent-handoff/README.md b/docs/agent-handoff/README.md new file mode 100644 index 0000000..0af8a4e --- /dev/null +++ b/docs/agent-handoff/README.md @@ -0,0 +1,55 @@ +# haocode 维修交接入口 + +> **执行状态**:当 `EXECUTION_STATE.md` 的状态为 `ACTIVE` 时,任何继续执行、上下文恢复或压缩恢复都必须先读取该文件。 + +本目录是 2026-09-16 之后的维修与跨平台工作的唯一交接入口。当前交付固化事实、决策、任务和验收方式;业务代码修复已按 `REPAIR_BACKLOG.md` 状态表推进(任务完成状态以该表和 `EXECUTION_STATE.md` 为准)。 + +精确实现仍以源码和可重复测试为准。若本文档与源码冲突,先记录证据,再修正文档;不要用旧文档覆盖已经核实的代码事实。 + +## 强制读取顺序 + +后续 Agent 必须从仓库根目录开始,按以下顺序读取: + +1. `AGENTS.md` +2. `docs/agent-handoff/README.md`(本文件) +3. 根据任务类型,只读取下表对应的文档 + +| 触发条件 | 接着读取 | 用途 | +|---|---|---| +| 准备领取或实施修复任务 | `REPAIR_BACKLOG.md` | 任务顺序、允许范围、禁止事项、完成条件 | +| 涉及 Windows/Linux、渲染器、shell、热键或截图 | `PLATFORM_PLAN.md` | 已冻结的跨平台契约 | +| 涉及测试、诊断脚本或验收 | `VERIFICATION.md` | 分层测试矩阵与证据要求 | +| 需要了解目录、命名、模块边界或技术债 | `CURRENT_STATE.md` | 已核实的当前状态 | + +不要从 `readme.md`、`Frame.md` 或 `ARCHITECTURE.md` 开始。这三份文件只能作为历史背景;其中的路径、打包说明和完成状态可能已经失效。第三方提供的《haocode 修复需求》也不是事实源,其中只有经源码核验并写入 `REPAIR_BACKLOG.md` 的内容有效。 + +## 永久操作约束 + +- 将 `data/config.json` 视为不透明的本机密钥文件。不得打开、读取、搜索、打印、复制、修改或让它进入 Agent 上下文;任何递归内容搜索都必须排除它。 +- 测试必须使用临时配置和临时数据库,并在导入 `MainWindow` 前完成重定向(统一用 `tests/_test_env.py` 的 `isolate()`)。自 P0-01 起所有运行时配置读取统一走 `core/config_paths`(`HAOCODE_CONFIG_FILE` 优先);少数 GUI 测试脚本(如 `smoke_offscreen.py`)自身不设置该环境变量,独立运行时由环境注入临时配置,P2-04 聚合入口将按子进程强制注入。 +- 不初始化 Git,不伪造提交历史。任务按可独立提交的粒度编写,等仓库以后具备 Git 历史再逐项提交。 +- 不引入 WebKitGTK。Windows 使用 WebView2(首选)或 QtWebEngine(回退);Linux 只使用 QtWebEngine。 +- 不新增 Agent shell 沙箱、审批或路径边界;Chromium 渲染进程的 sandbox 保持默认开启。 +- 不拆分或移动现有模块,不借修 bug 增加新产品功能。允许修改确认错误的源码,并新增聚焦测试、平台适配器、文档和配置样例。 +- 本阶段以 Windows/Linux 源码运行正确为目标;PyInstaller、安装器、AppData/XDG 目录迁移和发行包是下一阶段。 +- 不比较或移植 Claude Code、Codex、Grok Build、DeepSeek Harness。当前 `core/agent/` 继续保持 pi 的 Python 移植定位。 + +## 文档职责 + +- `CURRENT_STATE.md` 只记录已经从仓库核实的事实与结构/命名结论。 +- `PLATFORM_PLAN.md` 是平台行为的唯一决策源。 +- `REPAIR_BACKLOG.md` 是工作拆分和改动边界的唯一决策源。 +- `VERIFICATION.md` 是测试命令、平台矩阵和验收证据的唯一决策源。 +- `EXECUTION_STATE.md` 是无人值守执行与上下文压缩后的唯一恢复入口;任务权威状态仍以 `REPAIR_BACKLOG.md` 为准,执行证据放 `evidence/`。 + +同一规则不要在多份文件中复制扩写。需要变更决策时,先修改其唯一归属文档,再更新这里的路由;不要在实现过程中悄悄改变范围。 + +## 领取任务 + +1. 从 `REPAIR_BACKLOG.md` 选择一个未完成任务 ID。 +2. 只读取该任务列出的源码和它引用的规范文档。 +3. 先建立最小复现或失败测试,再修改允许范围内的文件。 +4. 运行任务的聚焦测试;阶段结束时再运行 `VERIFICATION.md` 指定的完整自动化集合。 +5. 记录实际命令、结果和平台证据。没有运行的测试必须明确写“未运行”,不能按通过处理。 + +任务完成的含义是:行为、回归测试、平台适用性和文档中的完成条件全部满足。只提交代码或只写说明都不算完成。 diff --git a/docs/agent-handoff/REPAIR_BACKLOG.md b/docs/agent-handoff/REPAIR_BACKLOG.md new file mode 100644 index 0000000..9557d17 --- /dev/null +++ b/docs/agent-handoff/REPAIR_BACKLOG.md @@ -0,0 +1,548 @@ +# 修复任务清单 + +本文是修复工作的任务源。每次只领取一个任务单元;任务完成后再进入下一个单元。精确实现以当前源码为准,本文约束修复范围和验收结果,不授权功能扩展。 + +验证方法与平台取证要求统一见 [VERIFICATION.md](VERIFICATION.md)。跨平台运行契约见 [PLATFORM_PLAN.md](PLATFORM_PLAN.md)。 + +## 执行边界 + +- 当前阶段只保证源码运行。安装包、冻结构建、AppData/XDG 目录迁移均不在本清单内。 +- 保持现有模块位置,不拆分或移动 `ui/views/main_window.py`、`ui/web/app.js` 等大文件。可以新增测试、窄的平台适配器和配置路径辅助模块。 +- 保持现有 Agent 能力和 `core/agent/` 与 pi 的行为,不增加工具注册、权限确认、命令沙盒或路径边界。 +- 保留 Chromium sandbox。只有明确检测到 root/container 且用户显式选择时,才允许添加 `--no-sandbox`。 +- `data/config.json` 是不透明的运行时秘密。工作 Agent 不得打开、读取、搜索、打印、复制或修改该文件;所有自动化测试使用临时配置。 +- 第三方修复文档只是线索。只有源码可复现的缺陷和本清单明确写出的行为才是修复依据。 +- 当前快照没有 Git 历史。每个任务仍须保持可独立审查;以后接入 Git 时,一个任务对应一个提交。 + +## 顺序与依赖 + +| 顺序 | 任务 | 状态 | 依赖 | +|---|---|---|---| +| P0-01 | 配置路径与测试隔离 | 已完成(P0 阶段回归通过,2026-09-16;见 evidence/P0-01.md) | 无,其他 GUI/配置测试的前置任务 | +| P0-02 | 合并重复的 `MainWindow.eventFilter` | 已完成(P0 阶段回归通过,2026-09-16;见 evidence/P0-02.md) | P0-01 | +| P0-03 | 修复交接文档基线 | 已完成(2026-09-16) | 无;后续只做一致性复核 | +| P1-01 | 双向消息渲染窗口 | 已完成 | P0-01 | +| P1-02 | Windows/Linux shell 与进程树终止 | 已完成(Windows 侧自动化全绿,2026-07-09;见 evidence/P1-02.md) | P0-01 | +| P1-03 | Windows/Linux 渲染器启动链 | 已完成(Windows 侧自动化 + offscreen 真实启动链全绿,2026-07-17;见 evidence/P1-03.md) | P0-01、P1-02 | +| P1-04 | Linux 截图热键与截图实现 | 已完成(Windows 侧自动化全绿,2026-07-17;X11/Wayland 真机待手动;见 evidence/P1-04.md) | P1-03 | +| P2-01 | Bash 任务按启动时间倒序 | 已完成(offscreen 全量覆盖,2026-07-21;见 evidence/P2-01.md) | P0-01 | +| P2-02 | 右侧 Bash 面板滚动条 | 已完成(offscreen 全量覆盖,2026-07-21;真机截图待对应环境跑 diag;见 evidence/P2-02.md) | P2-01 | +| P2-03 | WebView2 原生窗口遮挡层 | 已完成(offscreen 34 项全绿,2026-07-21;真机 WebView2 人工验收清单见 evidence/P2-03.md) | P0-02、P1-03 | +| P2-04 | 跨平台聚合测试入口 | 已完成(2026-07-17) | 新增 `tests/run_all.py`:Windows 25/25 PASS,WSL/3.12 FAIL 0 + 21 有理由 SKIP;暴露并修复 T9 陈旧断言与 `core/agent/compaction.py` 的 3.11+ dataclass 缺陷;证据 `evidence/P2-04.md` | + +## P0-01 配置路径与测试隔离 + +**状态:已实施(定向测试通过)。** 新增 `core/config_paths.py`(统一入口,环境变量优先、容错加载、可见警告);`core/llm_engine.py`、`ui/views/main_window.py`(webview_backend 与 init_model_popup 两处)、`ui/views/bash_panel.py` 全部改走统一入口;新增 `tests/_test_env.py` 统一临时环境与 `tests/test_config_isolation.py` 隔离回归。证据见 [evidence/P0-01.md](evidence/P0-01.md)。 + +**目标** + +让所有配置读取方统一尊重 `HAOCODE_CONFIG_FILE`,并保证自动化测试在导入 GUI 前完成数据库和配置重定向。默认源码运行仍使用项目内 `data/`。 + +**先读文件** + +- `core/llm_engine.py` +- `ui/views/bash_panel.py` +- `ui/views/main_window.py` 中所有配置路径和配置读取函数 +- `core/db_manager.py` 中 `_DEFAULT_DB` 的定义和初始化时机 +- `tests/smoke_bash_panel.py` +- `tests/test_error_persist.py` +- `tests/test_agent_core.py` 中依赖 provider 配置的用例 + +只读源码中的路径引用,不读取 `data/config.json` 的内容。 + +**允许修改** + +- 上述源码和测试。 +- 可以新增一个只负责路径解析和容错加载的 `core/` 辅助模块,以及一个 `tests/` 临时环境辅助模块。 +- 可以新增 `tests/test_config_isolation.py`。 + +**硬约束** + +- 环境变量优先级统一;没有环境变量时才回到项目内现有路径。 +- 路径解析本身不得在 import 时输出、复制或迁移配置内容。 +- 缺失或格式错误的配置必须产生可见警告,并使用现有安全默认值继续启动;不得吞掉错误,也不得因此阻断不需要该配置的源码路径。 +- 测试必须先创建临时配置、设置 `HAOCODE_CONFIG_FILE`、重定向 `core.db_manager._DEFAULT_DB`,然后才能 import `MainWindow`。 +- 不修改真实配置,不引入配置迁移或新配置格式。 + +**目标测试** + +```text +python tests/test_config_isolation.py +python tests/test_error_persist.py +python tests/smoke_bash_panel.py +python tests/test_agent_core.py +``` + +**完成证据** + +- 测试用拦截器记录到的配置打开路径全部位于临时目录,且禁止路径从未被打开;此断言不得通过读取或散列真实配置完成。 +- 临时配置的读写用例通过,临时数据库之外没有数据库写入。 +- 缺失配置和损坏配置各有一个回归用例,日志包含明确警告,进程正常退出。 +- 源码中不存在绕过统一路径解析的运行时配置读取。 + +## P0-02 合并重复的 `MainWindow.eventFilter` + +**状态:已实施(定向测试通过)。** 两处定义合并为一份(位于 `init_chat_events` 前的「事件拦截」节);发送规则(按钮禁用 / 流式生成时 Enter 不发送)收进 `send_message(from_enter=...)` 单一实现,按钮点击的中断语义不变。证据见 [evidence/P0-02.md](evidence/P0-02.md)。 + +**目标** + +修复同一类中后定义方法覆盖前定义方法的确定性缺陷,使 Enter、Shift+Enter、发送按钮状态和流式生成状态使用一套事件策略。 + +**先读文件** + +- `ui/views/main_window.py` 中 `MainWindow.init_chat_events`、两处 `MainWindow.eventFilter`、`send_message` 和 `_update_send_button_state` +- 与输入框发送行为相关的现有 smoke 测试 + +**允许修改** + +- `ui/views/main_window.py` 的事件过滤逻辑。 +- 可以新增 `tests/test_main_window_event_filter.py` 或在现有离屏 smoke 中增加断言。 + +**硬约束** + +- `MainWindow` 最终只能有一个 `eventFilter` 定义。 +- Enter 只发送一次;Shift+Enter 放行换行;发送按钮禁用或当前会话正在流式生成时不得发送。 +- 其他对象和事件必须继续交给父类,不顺带重构主窗口事件系统。 +- 测试遵守 P0-01 的临时配置和临时数据库规则。 + +**目标测试** + +```text +python tests/test_main_window_event_filter.py +python tests/smoke_offscreen.py +python tests/smoke_mode.py +``` + +**完成证据** + +- 四种键盘状态均有断言:Enter 可发送、Enter 被禁用、流式时 Enter 被拦截、Shift+Enter 换行。 +- 静态断言或 AST 检查证明 `MainWindow` 只有一个 `eventFilter`。 +- 测试中一次按键对应最多一次 `send_message` 调用。 + +## P0-03 修复交接文档基线 + +**状态:已完成。** 2026-09-16 无人值守轮次按本节标准复核:三项 `rg` 回归通过,交接目录与 `AGENTS.md` 相对链接全部有效;并随 P0-01/P0-02 结果更新了 README/CURRENT_STATE/VERIFICATION 中相应过时表述。保留本节作为后续文档变更的回归标准,不要重复搬运或重建交接目录。 + +**目标** + +让后续 Agent 只从一条清晰入口读取当前事实,并把损坏或过时材料降级为历史资料。 + +**先读文件** + +- `AGENTS.md` +- `ARCHITECTURE.md` +- `docs/agent-handoff/README.md` +- `docs/agent-handoff/CURRENT_STATE.md` +- `docs/agent-handoff/PLATFORM_PLAN.md` +- 本文和 `VERIFICATION.md` + +**允许修改** + +- 仅上述文档。 + +**硬约束** + +- `AGENTS.md` 的入口指针必须覆盖修复、跨平台、测试、结构审查和工作交接五类触发场景。 +- `ARCHITECTURE.md` 必须移除被拼接进来的第三方任务书正文,并在旧架构内容前明确标记“历史资料”;不得把旧目录树继续描述为当前事实。 +- 当前支持矩阵、任务要求和验证规则分别只有一个权威位置,通过链接引用,不复制成多份。 +- 不创建虚构的 Git 历史,不声称缺失的脚本或打包配置已经存在。 + +**目标测试** + +```text +rg -n "agent-handoff/README.md" AGENTS.md +rg -n "历史资料|当前事实" ARCHITECTURE.md +rg -n "haocode 修复需求(团队任务书)" ARCHITECTURE.md +``` + +最后一条应无匹配;再逐一检查交接文档中的相对链接和文件路径存在性。 + +**完成证据** + +- 新 Agent 按 `AGENTS.md` 指针能在一次跳转内到达 `docs/agent-handoff/README.md`。 +- `ARCHITECTURE.md` 没有拼接残片,且任何保留旧内容均带历史标记。 +- 交接目录中没有互相冲突的支持矩阵、默认值或验收口径。 + +## P1-01 双向消息渲染窗口 + +**目标** + +把前端 DOM 限制为严格的双向滑动窗口,同时保留完整当前会话链、附件、时间线、工具输出、分支切换和流式体验。 + +**先读文件** + +- `ui/views/main_window.py` 中 `load_messages_to_web`、分支切换、删除/重答和 `_active_streams` +- `ui/views/chat_bridge.py` +- `ui/web/app.js` 中消息创建、`messageBuffer`、滚动、`clearChat`、流式完成和时间线回放 +- `ui/web/index.html`、`ui/web/style.css` +- `core/db_manager.py` 中 `get_message_chain` 和分支查询 +- `tests/test_math_extract.js`、`tests/smoke_timeline.py`、`tests/smoke_midswitch.py` + +**允许修改** + +- 上述 Python/JS/CSS 文件和相关测试。 +- 可以新增一个 DOM 无关的 JS 窗口状态模块、`tests/test_render_window.js` 和 `tests/diag_render_scale.py`。 +- 可以在统一配置加载器中增加 `render_window_mode`、`render_window_size` 的解析。 + +**硬约束** + +- Python 保留完整的当前会话可见消息链作为窗口数据源;本任务不改 SQLite 查询模型或数据库 schema。 +- 初始窗口为最新 `size` 条。`size` 只接受非布尔整数 `10..200`,缺失、布尔、字符串、零、负数和越界值都静默回落 `40`。模式只接受 `auto`/`manual`,否则回落 `auto`。 +- `.message-wrapper` 数量始终不超过 `size`。流式消息计入上限;默认值下有一条流式消息时,最多保留另外 39 条。 +- JS 以“方向 + 边界消息 ID”向 Python 请求页;Python 以单个批次返回完整消息描述。描述必须足以独立还原正文、reasoning、附件、时间线和工具结果、分支信息及必要的发送者信息。 +- 请求与响应携带会话/代次标识;切会话、清屏或切分支后到达的旧响应必须丢弃。 +- `messageBuffer` 只负责活动流,不得作为历史分页数据源。 +- `manual` 模式向上只能点击“加载更早消息”;滚到顶部不得自动请求。`auto` 模式由顶部观察器自动请求,同时保留同一按钮。两个模式向下都自动恢复较新消息。 +- 每个新流式 token 延续当前行为:立即回到底部并跟随活动消息。 +- 向上换页使用“首个可见消息 ID + 像素偏移”恢复锚点,误差不超过 2 px。不得只按总高度差估算。 +- 切换分支后重建链,并尽量让目标消息保持在同一视口位置;目标已不存在时回到底部。 +- `clearChat()` 清除当前窗口的游标、缓存、DOM、未决请求和代次;已注入的配置模式和大小保持不变。 +- 一次换页始终执行“加入一端、裁掉另一端”,并维护 `hiddenOlder`、`hiddenNewer`。上方没有更多记录时隐藏加载入口。 +- 不引入 JSDOM、npm 工程或新的前端依赖。 + +**目标测试** + +```text +node tests/test_render_window.js +python tests/diag_render_scale.py 400 +python tests/smoke_offscreen.py +python tests/smoke_timeline.py +python tests/smoke_midswitch.py +python tests/test_file_attach.py +node tests/test_math_extract.js +``` + +`test_render_window.js` 必须直接测试 DOM 无关状态机;DOM/QWebChannel 集成由 Qt smoke 覆盖。 + +**完成证据** + +- 参数化测试覆盖 `auto`/`manual`、10/40/200、全部非法配置类型、双向连续换页、首尾边界和过期响应。 +- 固定 400 条夹具中 `.message-wrapper <= size`,并记录总节点数与页面高度;节点和高度阈值只对该固定夹具验收,见 `VERIFICATION.md`。 +- 附件消息、带工具时间线消息和多分支消息在被裁剪后再次加载,内容与控件完整。 +- 有/无活动流两种情况下均满足严格上限;流式期间没有删除活动消息。 +- 自动与手动模式分别有向上行为断言;两个模式都有无需点击的向下恢复断言。 +- 锚点误差记录不超过 2 px;分支目标缺失的回底行为有断言。 +- 帧耗时只形成真机人工基准报告,不作为自动化硬阈值。 + +## P1-02 Windows/Linux shell 与进程树终止 + +**状态:已完成。** 2026-07-09 无人值守轮次:新增 `core/platform_shell.py` 窄适配(Windows 字符串命令行 `cmd.exe /d /s /c ""`、Linux `/bin/bash -lc` argv + 独立进程组;超时/中止整树终止);`SYSTEM_PROMPT.md` 单一通用正文 + `{{SHELL_PLATFORM_SECTION}}` 运行时短平台段。目标测试 20/20、30/30、35/35、41/41 全绿,smoke_mode 完整 agent 回合 ALL PASS。Linux 进程组用例(C3)在 Linux 环境运行时生效。证据见 `evidence/P1-02.md`。 + +**目标** + +明确工具 shell 契约,并保证超时、取消和异常清理能结束整棵子进程树。 + +**先读文件** + +- `core/agent/tools.py` 中 bash 工具、`Popen`、超时和终止逻辑 +- `core/llm_engine.py` 中 `load_system_prompt` +- `SYSTEM_PROMPT.md` 的 shell/path 说明 +- `tests/test_bash_stream.py`、`tests/test_tool_params.py` + +**允许修改** + +- 上述文件和测试。 +- 可以新增一个窄的平台进程适配模块及 `tests/test_cross_platform_shell.py`。 + +**硬约束** + +- Windows 明确通过 `cmd.exe` 执行;Linux 明确通过 `/bin/bash -lc` 执行,不依赖 `shell=True` 的平台默认值。 +- Windows 使用现有等价机制终止进程树;Linux 创建独立 POSIX 进程组,超时和主动中止均向整组发送终止信号,并在宽限期后强制结束。 +- 输出流、超时提示、截断上限和工具结果结构保持现有接口。 +- 保留一份通用 `SYSTEM_PROMPT.md`;运行时只插入短的平台 shell/path 段。不得维护两份完整提示词。 +- 提示词仍在每次请求时重读。Windows 段不得出现在 Linux 请求中,Linux 段不得出现在 Windows 请求中。 +- 不增加命令审批、Agent shell sandbox 或路径限制。 + +**目标测试** + +```text +python tests/test_cross_platform_shell.py +python tests/test_bash_stream.py +python tests/test_tool_params.py +python tests/test_agent_core.py +``` + +**完成证据** + +- 平台参数测试捕获到 Windows 的 `cmd.exe` argv 和 Linux 的 `/bin/bash -lc` argv。 +- Linux 用例启动父进程和孙进程,分别在超时与主动中止后证明两者都不存在;Windows 有等价进程树用例。 +- 提示词测试模拟两个平台,证明只有对应平台段被插入且通用正文完全相同。 +- 现有 bash 流式输出、返回码、超时和截断回归全部通过。 + +## P1-03 Windows/Linux 渲染器启动链 + +**状态:已完成。** 2026-07-17 无人值守轮次:新增 `core/renderer_backend.py` 窄适配(后端解析:非法/跨平台 `webview2` 可见警告 + 回落平台默认;每实例独立 QtWebEngine profile 目录:源码 `data/webengine/profile__`、测试经 `HAOCODE_WEBENGINE_PROFILE_DIR` 重定向;`--no-sandbox` 仅显式 + root/容器时接受并打印高可见警告,普通桌面剥离);`main_window.py` 以 `sys.platform == "win32"` 门控 `core.webview2` 导入(Linux 永不触达 pythonnet/Win32/DLL/taskkill);`CustomWebPage(profile, parent)` 兼容旧式调用;`main.py` 导入 PyQt6 前 sanitize flags;requirements 平台 marker + Linux 运行说明。目标测试 19/19、10/10、22/0、8/8、39 passed 全绿 + `main.py` offscreen 真实启动链验证;Linux 真机与 root/容器接受路径待对应环境。证据见 `evidence/P1-03.md`。 + +**目标** + +建立明确的渲染器矩阵:Windows 首选 WebView2、失败回落 QtWebEngine;Linux 只使用 QtWebEngine。 + +**先读文件** + +- `main.py` +- `ui/views/main_window.py` 中浏览器创建、JS 就绪和配置读取 +- `core/webview2.py` +- `ui/views/wv2_view.py` +- `ui/views/custom_web_page.py` +- `requirements.txt` +- `tests/test_wv2_guard.py`、`tests/smoke_offscreen.py` + +**允许修改** + +- 上述启动链、依赖说明和测试。 +- 可以新增小型平台检测/QtWebEngine profile 适配器和 Linux 源码运行说明。 + +**硬约束** + +- Linux 路径不得导入或调用 pythonnet、Win32 API、WebView2 DLL 和 `taskkill`。 +- 不引入 WebKitGTK 或它的 Qt 封装。 +- QtWebEngine 使用隔离 profile,两个并行源码实例不得争用同一个 Chromium profile。保持现有页面功能,不伪造 Linux 单实例锁。 +- 非法 `webview_backend` 配置产生可见警告并回落平台默认值,不能阻断源码启动。 +- 正常桌面运行保留 Chromium sandbox;root/container 的无 sandbox 路径必须显式选择并打印风险提示。 +- `vendor/webview2/` 和根目录 `WebView2Loader.dll` 保持 Windows 运行时用途,不删除。 +- 不添加 PyInstaller/spec 文件或打包承诺。 + +**目标测试** + +```text +python tests/test_wv2_guard.py +python tests/smoke_offscreen.py +python tests/test_debug_window.py +node tests/test_math_extract.js +``` + +平台真实启动按 `VERIFICATION.md` 执行。 + +**完成证据** + +- 平台模拟测试证明 Windows 的首选/回落路径和 Linux 的 QtWebEngine-only 路径。 +- Linux import 测试不触达任何 Windows-only 符号。 +- 两个 QtWebEngine 实例同时运行、载入本地页面和关闭,profile 无锁冲突或互相清理。 +- Windows 真实 WebView2 和强制 QtWebEngine 回落各有一次启动记录;Linux X11/Wayland 各有一次 QtWebEngine 启动记录。 +- 普通用户运行参数中不存在 `--no-sandbox`;显式 root/container 路径有单独证据。 + +## P1-04 Linux 截图热键与截图实现 + +**状态:已完成。** 2026-07-17 无人值守轮次:新增三个窄适配器——`desktop_session.py`(会话探测 win32/x11/wayland/unknown + 能力路由/明确不可用日志)、`x11_hotkey.py`(ctypes→libX11 XGrabKey 原生全局热键,零 pip 依赖,注册失败/无显示明确日志,stop 释放)、`portal_capture.py`(Wayland 经 xdg-desktop-portal Screenshot,系统 gdbus CLI,用户授权不绕过 compositor,FilePicked→现有附件流程,拒绝/不支持/超时有明确结果);`main_window.py` 热键与截图按平台路由(Windows 行为保持);`screen_capture.py` 空画面守卫。目标测试 23/23、17/17、9 OK、8/8 全绿 + 回归 41/41 等全绿 + `main.py` offscreen 真实启动链;X11 真实注册/命中与 Wayland portal 三态待真实 Linux 宿主手动。证据见 `evidence/P1-04.md`。 + +**目标** + +把现有“截图全局热键”能力适配到 Linux,而不是扩展成通用键盘钩子系统。 + +**先读文件** + +- `ui/views/system_tools/global_hotkey.py` +- `ui/views/system_tools/screen_capture.py` +- `ui/views/main_window.py` 中热键注册、截图启动和结果处理 +- `main.py` 的平台启动逻辑 +- 相关附件/图片测试 + +**允许修改** + +- 上述文件和测试。 +- 可以新增 Windows、Linux X11、Linux Wayland 的窄适配器;原入口保持稳定。 + +**硬约束** + +- Windows 保持现有行为。 +- Linux X11 使用原生全局快捷键和可行的原生屏幕捕获路径。 +- Linux Wayland 使用 `xdg-desktop-portal` 或桌面协议完成快捷键和截图;遵守用户授权流程,不绕过 compositor。 +- portal、桌面环境或协议版本不支持时,界面/日志必须明确说明当前能力不可用,主程序仍可聊天和使用其他功能。 +- 只处理现有截图快捷键,不增加任意键监听、记录或重映射。 +- 不做打包和发行版泛化;Ubuntu 22.04/24.04 x64 以外标记“未验证”。 + +**目标测试** + +```text +python tests/test_global_hotkey_platforms.py +python tests/test_screen_capture_platforms.py +python tests/test_file_attach.py +python tests/smoke_offscreen.py +``` + +适配器单元测试使用替身;X11/Wayland 真实行为按 `VERIFICATION.md` 手工取证。 + +**完成证据** + +- 平台路由、授权拒绝、portal 缺失和注册失败均有确定的回归测试。 +- X11 真实桌面中,应用失焦时热键仍触发截图,图片回到现有附件流程。 +- Wayland 真实桌面中,通过 portal/桌面协议完成授权、触发和截图;若目标桌面确实不支持,留下明确错误和环境信息,而不是伪造成功。 +- Windows 现有热键和截图回归通过。 + +## P2-01 Bash 任务按启动时间倒序 + +**目标** + +运行中和已完成两栏都让最新启动的任务位于第一项,任务从运行中移动到已完成时仍使用原启动顺序。 + +**先读文件** + +- `ui/views/bash_panel.py` 中 `set_session`、`_refresh`、`on_started`、`on_finished`、`BashLayer` 和两个 section 类 +- `ui/views/main_window.py` 中 `_on_tool_started`、输出/计时/完成转发 +- `tests/smoke_bash_panel.py` + +**允许修改** + +- `ui/views/bash_panel.py`、必要的事件元数据传递和测试。 + +**硬约束** + +- 排序键是启动时间/启动序号,降序显示;不得用完成时间重排。 +- 已落库时间线没有显式时间时,使用消息链顺序和时间线内顺序构造稳定启动序号,不改数据库 schema。 +- 任务从运行中进入已完成后,相对顺序由原启动键决定,不因结束先后跳位。 +- 重排复用现有 `BashLayer` 实例;展开/折叠状态、实时输出、代码框水平/垂直滚动值、运行中栏和已完成栏的 section 滚动位置都保持。 +- 保留现有限量显示和上下文标记语义。 + +**目标测试** + +```text +python tests/smoke_bash_panel.py +python tests/test_bash_stream.py +``` + +**完成证据** + +- 至少三项任务以不同启动/完成顺序运行,两个 section 均断言启动时间降序。 +- 完成中间任务前后,对象 identity、展开状态、输出文本和四类滚动值保持。 +- 切换会话后从 DB/活动流重建的顺序与实时期间一致。 + +**状态:已完成。** 2026-07-21 无人值守轮次:`bash_panel.py` 的 `_refresh()` 两栏显示改为启动序号降序(排序键 = `_order` 稳定启动序号,构造方式 = 消息链顺序 + 时间线内顺序,无时间戳、无 schema 变更;完成时间从不参与排序,`on_finished` 不移动 `_order` 位置);重排仍走 `set_layers` 复用同一批 `BashLayer` 实例,展开/折叠、实时输出、代码框滚动、栏滚动位置全部保持;限量窗口成员与提示语不变。`smoke_bash_panel.py` 新增第 11 节 23 项断言(交错完成顺序、对象 identity、四类滚动值保持、DB 重建一致性)140 项 ALL PASS,`test_bash_stream.py` 30/30,回归 smoke_offscreen 8/8、run_tests 41/41 全绿。证据见 `evidence/P2-01.md`。 + +## P2-02 右侧 Bash 面板滚动条 + +**目标** + +只修复右侧 Bash 面板的原生滚动条和横纵滚动条交汇角,不污染其他 Qt 控件。 + +**先读文件** + +- `ui/views/main_window.py` 中全局 QSS 的 `#right_sidebar`、`#bl_code` 段 +- `ui/views/bash_panel.py` 中 `BashLayer._code_box` 和 section 滚动区 +- Qt 当前版本的 `QAbstractScrollArea::corner`/`QPlainTextEdit::corner` 样式行为 +- `tests/smoke_bash_panel.py` + +**允许修改** + +- 右侧面板的局部 QSS、相关测试和新建 `tests/diag_panel_scrollbar.py`。 + +**硬约束** + +- 保留现有 `#bl_code` 背景、边框、圆角和文本基础样式,只补滚动条与正确的 corner 规则。 +- 所有选择器必须限定在右侧 Bash 面板或 `#bl_code`;不得添加无作用域的 `QScrollBar`/`QAbstractScrollArea` 全局规则。 +- 横纵滚动条目标厚度为 8 px,隐藏箭头,handle 可见且 hover 正常;corner 与代码框背景一致。 +- 使用 Qt 支持的 `QAbstractScrollArea`/`QPlainTextEdit` corner 子控件语法,不写 `QScrollBar::corner`。 +- 不借机调整其他弹窗、会话列表或全局字体。 + +**目标测试** + +```text +python tests/smoke_bash_panel.py +python tests/diag_panel_scrollbar.py +``` + +**完成证据** + +- 离屏测试断言横纵滚动条 sizeHint/实际厚度和箭头 extent,诊断脚本生成局部截图并打印测量值。 +- Windows 与 Linux QtWebEngine 真机截图均显示无箭头、无亮色 corner 方块、handle 可辨识。 +- 模型弹窗、会话列表、附件预览和调试窗口的滚动条与修复前一致。 + +**状态:已完成。** 2026-07-21 无人值守轮次:`main_window.py` 全局 QSS 中 `#bl_code` 规则后插入一段完全限定作用域的滚动条 + corner 规则(`QPlainTextEdit#bl_code QScrollBar:*`、`QPlainTextEdit#bl_code::corner { background-color: #fbfcfe; }`、`QScrollArea#bl_scroll QScrollBar:*`)——零无作用域规则;`#bl_code` 原有背景/边框/圆角/文本样式未动。新建 `tests/diag_panel_scrollbar.py`(18 项断言:厚度/sizeHint/箭头 subControlRect=0/corner 像素 #fbfcfe/无亮白/未命名框仍原生 14px/附件区仍 6px)18 项 ALL PASS,`smoke_bash_panel.py` 140 项、回归 smoke_offscreen 8/8、run_tests 41/41、timeline 11/11、midswitch 7/7 全绿。截图落盘 `evidence/p2-02-*.png`。Windows/Linux 真机截图项:在对应环境直接运行 `diag_panel_scrollbar.py` 即可出图验证。证据见 `evidence/P2-02.md`。 + +## P2-03 WebView2 原生窗口遮挡层 + +**目标** + +修复改名遮罩被 WebView2 原生子窗口压住的确定性缺陷,并审计同类遮罩,只修复能确认的原生窗口遮挡。 + +**先读文件** + +- `ui/views/main_window.py` 中 `AttachmentPreviewOverlay`、`RenameOverlay`、`SessionContextPopup`、`SettingsWindow`、所有以 `bg_widget` 或主窗口为 parent 的全窗口候选,以及 `_rename_session` +- `ui/views/wv2_view.py` +- `core/webview2.py` 中原生子窗口层级说明 +- `tests/smoke_offscreen.py` + +**允许修改** + +- 上述遮罩实现和测试。 +- 可以新增 `tests/diag_rename_overlay.py`;只有出现真实重复时才可加一个窄的几何同步辅助函数。 + +**硬约束** + +- `RenameOverlay` 使用可覆盖原生 WebView2 的独立顶层透明窗口,覆盖主窗口客户区并保持卡片居中;系统标题栏和窗口控制仍可操作。 +- 主窗口移动、缩放、最大化、还原和多显示器/DPI 变化时几何同步正确。 +- 保留点击空白关闭、Esc、关闭按钮、输入框全选、Enter 提交和关闭后焦点恢复。 +- 顶层窗口关闭后释放,不残留遮罩、事件过滤器或焦点捕获。 +- 审计每个遮罩后只修复可复现的同类问题;普通 popup/dialog 不重写成统一框架。 +- QtWebEngine 路径行为不得退化;真正的 WebView2 覆盖只能在 Windows 真机验收。 + +**目标测试** + +```text +python tests/diag_rename_overlay.py +python tests/smoke_offscreen.py +python tests/smoke_copy_session.py +``` + +**完成证据** + +- 结构测试证明遮罩是顶层 Tool 窗口、启用透明背景、覆盖主窗口客户区并可释放。 +- Windows WebView2 真机截图显示聊天区与 Qt 区域均匀变暗,卡片在最上层且可交互。 +- 拖动、缩放、最大化、还原、多 DPI 显示器至少各验证一次;记录覆盖误差。 +- 遮罩审计表列出每个候选、是否受原生窗口影响、复现结果和处理决定。 + +**状态:已完成。** 2026-07-21 无人值守轮次:`RenameOverlay` 整类重写为独立顶层 `FramelessWindowHint|Tool` 透明窗(`WA_TranslucentBackground` + `WA_DeleteOnClose`)——只覆盖主窗口**客户区**(`mapToGlobal(main.rect().topLeft())`+客户区尺寸,系统标题栏/窗口控制保持可操作),eventFilter 跟随 `Move`/`Resize`/`WindowStateChange`(最大化/还原/DPI 变化同路),保留点空白/✕/取消/Enter/输入全选,新增 Esc 关闭;关闭时 `removeEventFilter`+焦点回主窗+`deleteLater`,`_closed` 幂等。调用点 `_rename_session` 未改。审计:`AttachmentPreviewOverlay`/`SettingsWindow`/`SessionContextPopup`/`ModelSelectPopup`/`SessionModePopup`/`PdfModePopup` 均为独立顶层窗(不受影响),`RenameOverlay` 是唯一 bg_widget 子控件模式者(已修)。未抽公共辅助函数(不构成真实重复);未重写普通 popup/dialog。新建 `tests/diag_rename_overlay.py`(34 项:结构/客户区覆盖/跟随/全部关闭与提交行为/释放无残留)ALL PASS;目标测试 smoke_offscreen 8/8、smoke_copy_session ALL PASS;回归 smoke_bash_panel 140 项、run_tests 41/41 全绿(均 EXIT=0)。真机 WebView2 验收:`core/webview2.py` `get_environment()` 含 `taskkill /F /IM msedgewebview2.exe` 且共享 profile 不可重定向,无人值守自动化有意不走该路径;机制与生产已验证的附件预览层同构(拥有者顶层 Tool 窗 z 序必盖 WebView2 原生子 HWND),人工验收清单见 `evidence/P2-03.md`。证据见 `evidence/P2-03.md`。 + +## P2-04 跨平台聚合测试入口 + +**目标** + +提供一个可在 Windows/Linux 调用的自动化聚合入口,同时保留每个现有测试的独立运行方式。 + +**先读文件** + +- `tests/run_tests.py` +- `tests/test_*.py`、`tests/smoke_*.py` 的入口和环境假设 +- 本文各任务新增的测试 +- `VERIFICATION.md` + +**允许修改** + +- `tests/run_tests.py`,或新增 `tests/run_all.py`。 +- 可以新增测试清单/分组元数据和必要的测试环境辅助模块。 +- 可以更新交接测试文档。 + +**硬约束** + +- 保留所有独立命令;聚合入口不得要求 pytest、npm 或网络。 +- 每个测试使用独立临时目录;GUI 测试在 import `MainWindow` 前完成临时数据库与临时配置设置。 +- 默认聚合不运行 `diag_*`、`verify_*`、`tune_*`、真实 API、真实桌面或需要凭据的脚本。 +- 平台不适用项必须以明确 `SKIP` 和理由呈现,不能伪装通过;平台共同项失败时返回非零。 +- 一个子测试崩溃或超时不能阻止结果汇总,最终退出码仍反映失败。 +- 不读取真实配置,不访问网络,不删除用户运行数据。 + +**目标测试** + +```text +python tests/run_all.py --group logic +python tests/run_all.py --group offscreen +python tests/run_all.py --group all +``` + +若选择扩展现有 `run_tests.py`,保持等价分组参数,并同步本文命令。 + +**完成证据** + +- Windows 和 Linux 各有一份汇总,列出 PASS/FAIL/SKIP、耗时和失败命令。✅ Windows 25/25 PASS(logic 12 + offscreen 13,均 EXIT=0);WSL/CPython 3.12.3 全量 PASS 4 / FAIL 0 / SKIP 21(依赖缺失/平台不适用,理由逐条打印,EXIT=0)。 +- 用一个故意失败的临时夹具证明聚合入口返回非零且仍汇总后续测试;夹具不提交。✅ 崩溃夹具→FAIL、超时夹具→TIMEOUT,后续健康测试照跑,退出码 1,完整汇总 + 失败命令清单;夹具用后即删。 +- 默认运行记录证明未启动真实 API 测试、人工诊断脚本或真实桌面脚本。✅ 默认清单仅 25 条自动化条目;diag_*/verify_*/tune_*/real-DB/凭据类均在排除清单带理由(见 run_all.py 注释)。 +- 各单文件命令仍可直接运行,输出与聚合子进程一致。✅ `test_copy_session.py` 独立 vs 聚合子日志尾逐字一致(54/54 PASS)。 + +## 整体验收终点 + +只有同时满足以下条件,本清单才完成: + +1. 每个任务的目标测试通过,并留下该任务要求的证据。 +2. `VERIFICATION.md` 的完整自动化回归在 Windows 和 Linux 通过;合理的平台专属项明确跳过。 +3. Windows WebView2、Windows QtWebEngine 回落、Linux X11 QtWebEngine、Linux Wayland QtWebEngine 均完成真实桌面检查。(Windows 两项:✅ 2026-09-17 已验证,见 evidence/P1-03.md;Linux 两项:待 Ubuntu 桌面主机) +4. X11 与 Wayland 的截图热键按各自协议验证;不支持的 Wayland 环境留下明确错误证据。 +5. 没有业务功能扩展、模块搬迁、打包变更、Agent sandbox 或真实配置访问混入修复。 diff --git a/docs/agent-handoff/VERIFICATION.md b/docs/agent-handoff/VERIFICATION.md new file mode 100644 index 0000000..1108935 --- /dev/null +++ b/docs/agent-handoff/VERIFICATION.md @@ -0,0 +1,262 @@ +# 验证与取证规范 + +本文定义修复任务的统一验证口径。任务范围和完成条件见 [REPAIR_BACKLOG.md](REPAIR_BACKLOG.md)。平台行为契约见 [PLATFORM_PLAN.md](PLATFORM_PLAN.md)。 + +## 测试前置条件 + +### 凭据与运行数据隔离 + +`data/config.json` 是不透明秘密。执行测试的 Agent 不得打开、读取、搜索、打印、复制或修改它,也不得用散列或快照方式“验证未变化”。采用以下正向隔离: + +1. 为每个测试进程创建独立临时目录和最小临时配置。 +2. 在 import 任意可能间接加载 `MainWindow` 的模块前设置 `HAOCODE_CONFIG_FILE`。 +3. 在 import `MainWindow` 前把 `core.db_manager._DEFAULT_DB` 指向临时数据库。 +4. 写入、迁移、附件和截图产物只落到临时目录。 +5. 用文件打开拦截器或替身断言禁止路径从未被访问,不读取禁止路径本身。 + +需要真实 API key 的 `diag_live_*`、`smoke_live_*` 由用户在受控环境手工决定是否运行;默认测试和工作 Agent 均不运行它们。 + +### GUI 环境 + +离屏测试只验证结构和一般 Qt 行为,不算真机视觉证据。 + +Windows PowerShell: + +```powershell +$env:PYTHONIOENCODING = 'utf-8' +$env:QT_QPA_PLATFORM = 'offscreen' +python tests/smoke_offscreen.py +``` + +Linux: + +```bash +PYTHONIOENCODING=utf-8 QT_QPA_PLATFORM=offscreen python tests/smoke_offscreen.py +``` + +真实桌面检查前移除 `QT_QPA_PLATFORM=offscreen`。不得用离屏截图替代 WebView2、X11、Wayland、portal、DPI 或滚动条真机检查。 + +### 结果记录 + +每个任务至少记录: + +- 操作系统、桌面会话类型、Python、PyQt6/Qt 版本; +- 精确命令、退出码和测试摘要; +- 失败或跳过项及理由; +- 真机项的截图/录屏路径和复现步骤; +- 是否使用临时配置、临时数据库和离屏模式。 + +不要写固定“应通过 N 项”的文案;用退出码和当前测试自己报告的数量为准,避免测试增删后文档失真。 + +## 定向测试矩阵 + +每个任务先跑自己的定向测试。定向通过后才跑所在阶段的完整回归。 + +| 任务 | 必跑命令 | 额外人工证据 | +|---|---|---| +| P0-01 配置隔离 | `python tests/test_config_isolation.py`;`python tests/test_error_persist.py`;`python tests/smoke_bash_panel.py`;test_agent_core(经 `python tests/run_tests.py` 运行) | 打开路径拦截记录只含临时目录 | +| P0-02 `eventFilter` | `python tests/test_main_window_event_filter.py`;`python tests/smoke_offscreen.py`;`python tests/smoke_mode.py` | Enter/Shift+Enter 行为记录 | +| P0-03 文档 | 文档链接检查与 `rg` 检查 | 从 `AGENTS.md` 演练一次读取路径 | +| P1-01 渲染窗口 | `node tests/test_render_window.js`;`python tests/diag_render_scale.py 400`;相关 Qt smoke | 400 条固定夹具、锚点误差、帧时间报告 | +| P1-02 shell | `python tests/test_cross_platform_shell.py`;`python tests/test_bash_stream.py`;`python tests/test_tool_params.py` | 两平台进程树消失证明 | +| P1-03 渲染器 | `python tests/test_wv2_guard.py`;`python tests/smoke_offscreen.py`;`node tests/test_math_extract.js` | Windows 两后端、Linux X11/Wayland 启动 | +| P1-04 热键/截图 | 平台适配单元测试、`python tests/test_file_attach.py` | Windows、X11、Wayland 各自真机行为 | +| P2-01 Bash 排序 | `python tests/smoke_bash_panel.py`;`python tests/test_bash_stream.py` | 重排前后状态与滚动位置 | +| P2-02 滚动条 | `python tests/diag_panel_scrollbar.py`;`python tests/smoke_bash_panel.py` | Windows/Linux 局部截图和尺寸 | +| P2-03 遮罩 | `python tests/diag_rename_overlay.py`;`python tests/smoke_offscreen.py` | Windows WebView2 遮罩截图/移动缩放录屏 | +| P2-04 聚合入口 | `python tests/run_all.py --group logic`;`--group offscreen`;`--group all` | Windows/Linux 汇总各一份 | + +表中尚不存在的脚本属于对应任务的交付物,不得在创建前声称已经通过。 + +## P1-01 渲染窗口专项 + +### DOM 无关状态机 + +`tests/test_render_window.js` 直接加载纯状态机,不依赖浏览器、JSDOM 或 npm。至少覆盖: + +- `auto`、`manual` 两种模式; +- `size` 为 10、40、200; +- 缺失、`true`/`false`、字符串、0、负数、9、201 等值都回落 40; +- 初始最新页、连续向上、连续向下、首尾边界; +- 加一端时裁另一端,窗口始终不超过上限; +- 活动流计入上限,且不会被裁; +- 新 token 到来回到底部; +- `clearChat` 清游标/缓存/未决请求但保留配置; +- 会话/代次变化后丢弃旧响应; +- 分支目标保留和目标缺失回底。 + +### Qt/DOM 集成 + +Qt smoke 使用临时会话生成完整描述,至少包含普通消息、附件、reasoning、工具时间线、工具结果和多分支消息。用 QWebChannel 完成多轮双向换页后,逐项比较重建结果。 + +通用硬指标:任意时刻 `.message-wrapper <= render_window_size`。这条对所有夹具、所有窗口大小和流式状态都成立。 + +固定 400 条夹具的附加指标: + +| 指标 | 门槛 | +|---|---| +| `.message-wrapper` | `<= render_window_size`,默认 `<= 40` | +| DOM 总节点 | `<= 4000` | +| 页面总高度 | `<= 30000 px` | +| 向上换页锚点误差 | `<= 2 px` | + +总节点数和页面高度只对版本化的固定 400 条夹具验收。修改夹具必须在结果中说明,不得把这些数字套到任意内容长度。 + +### 帧时间 + +帧时间属于人工基准报告,不是自动化通过门槛。相同机器、相同窗口尺寸、相同 400 条夹具分别记录修复前/后: + +- 流式追加一段固定文本期间的采样次数; +- frame duration 的 median、p95 和最大值; +- 是否发生肉眼可见停顿; +- 浏览器后端和 Qt 版本。 + +报告原始数值和测量方法,不把环境波动包装成确定性断言。 + +## 当前独立回归命令 + +在 P2-04 聚合入口完成前,按影响范围选择下列现有命令。每项任务不要求机械运行所有命令;一个阶段结束时运行完整集合。 + +```text +python tests/run_tests.py +python tests/test_config_isolation.py +python tests/test_main_window_event_filter.py +python tests/test_tool_params.py +python tests/test_compaction_persist.py +python tests/test_copy_session.py +python tests/test_bash_stream.py +python tests/test_error_persist.py +python tests/test_wv2_guard.py +python tests/test_debug_window.py +python tests/test_think_code_neutral.py +python tests/test_file_attach.py +python tests/test_pdf_reader.py +python tests/smoke_offscreen.py +python tests/smoke_mode.py +python tests/smoke_copy_session.py +python tests/smoke_bash_panel.py +node tests/test_math_extract.js +``` + +平台专属测试在不适用的平台明确 `SKIP`。任何共同逻辑测试失败都不能以平台差异豁免。 + +`tests/run_tests.py` 当前只覆盖其显式加载内容;在 P2-04 完成前,不能把它单独称为“全套测试”。 + +## 分阶段完整回归 + +### P0 完成后 + +运行全部纯逻辑测试和所有受影响的离屏 GUI 测试。重点证明测试不会访问真实配置/数据库,并且 `MainWindow` 输入行为没有回归。 + +### P1 完成后 + +运行当前独立回归命令全集,加上: + +```text +node tests/test_render_window.js +python tests/test_cross_platform_shell.py +python tests/test_global_hotkey_platforms.py +python tests/test_screen_capture_platforms.py +``` + +随后完成 Windows/Linux 真实桌面矩阵。平台适配任务没有真实桌面证据时不能标记完成。 + +### P2 完成后 + +优先运行新聚合入口: + +```text +python tests/run_all.py --group all +``` + +再单独运行三个诊断脚本;它们属于人工/半自动取证,不应混入默认聚合: + +```text +python tests/diag_render_scale.py 400 +python tests/diag_panel_scrollbar.py +python tests/diag_rename_overlay.py +``` + +## 真实桌面矩阵 + +### Windows 10/11 x64 + +至少覆盖以下路径: + +1. WebView2 首选路径:启动、发一轮对话、流式输出、附件、分支、切会话。 +2. QtWebEngine 强制回落:使用临时配置启动同一套基本流程。 +3. 同时启动两个 QtWebEngine 实例,证明 profile 不争用。 +4. WebView2 下打开改名遮罩,验证整个客户区覆盖;拖动、缩放、最大化和还原。 +5. Bash 面板三项以上任务,验证最新任务在顶部、状态保持和滚动条视觉。 +6. 截图全局热键在应用失焦时仍能捕获并进入附件流程。 + +证据必须标注实际后端。QtWebEngine 截图不能替代 WebView2 原生遮挡验收。 + +### Ubuntu 22.04/24.04 x64 + X11 + +至少使用 Python 3.10--3.12 范围内一个受支持版本完成: + +1. `python main.py` 启动 QtWebEngine,渲染 Markdown、KaTeX、代码块和工具时间线。 +2. 发一轮对话并执行 shell 工具,实际命令由 `/bin/bash -lc` 执行。 +3. 超时和主动中止后检查父/孙进程均不存在。 +4. 双开应用,两个 QtWebEngine profile 不冲突。 +5. X11 原生全局截图热键在应用失焦时触发,截图进入附件流程。 +6. 消息窗口、Bash 排序与滚动条完成一次真机检查。 + +### Ubuntu 22.04/24.04 x64 + Wayland + +至少完成: + +1. QtWebEngine 正常启动并完成基本对话与渲染。 +2. portal/桌面协议请求有清晰的用户授权流程。 +3. 全局快捷键和截图通过 portal/桌面协议完成;若当前桌面协议不支持,界面明确报错,应用其余功能继续可用。 +4. 拒绝授权、portal 服务缺失和协议版本不足各记录一次结果。 +5. 普通用户启动保持 Chromium sandbox。 + +Wayland 下不得用 X11 私有 API 或静默降级成“仅窗口内快捷键”并声称全局热键通过。 + +### 其他发行版 + +可以记录探索结果,但统一标记“未验证”,不能扩大官方支持矩阵。 + +## 人工 UI 检查 + +### Bash 排序与状态 + +按 A、B、C 顺序启动,按 B、A、C 或其他不同顺序结束。运行中栏和已完成栏始终按 C、B、A 的启动顺序显示。操作前先: + +- 展开其中一个 layer; +- 在输出框分别设置水平和垂直滚动位置; +- 滚动运行中栏和已完成栏。 + +任务状态迁移后,上述展开状态、输出、代码框滚动和两个 section 滚动均保持。 + +### Bash 滚动条 + +同时制造横向与纵向溢出,记录: + +- 横纵滚动条实际厚度; +- 箭头区域是否消失; +- handle 是否可拖动并有 hover; +- 横纵交汇处是否与代码框背景一致; +- 模型弹窗、会话列表、附件预览和调试窗口是否未受影响。 + +### 改名遮罩 + +Windows WebView2 下记录打开、移动、缩放、最大化、还原、提交和取消。截图必须包含整个主窗口,能比较聊天区与 Qt 控件区的遮罩亮度。结构性离屏断言只是补充。 + +## 聚合入口契约 + +P2-04 完成后的聚合入口必须: + +- 使用 Python 子进程执行现有独立测试,不把所有测试 import 到同一进程; +- 为每个子进程建立独立临时环境; +- 支持至少 `logic`、`offscreen`、`all` 三组; +- 汇总命令、退出码、耗时和 PASS/FAIL/SKIP; +- 默认排除 live、diag、verify、tune、网络和凭据测试; +- 一个子进程失败后继续收集其余结果,最终返回非零; +- 在 Windows 和 Linux 使用同一 Python 接口,不嵌入 `.bat` 或 Bash 专属命令串。 + +## 最终判定 + +一项修复只有在以下内容齐全时才算完成:定向测试通过、阶段完整回归通过、该平台需要的真实桌面证据齐全、所有跳过项有理由、真实配置和数据库从未被测试访问。任何一项缺失都应标记为“未完成”而不是“基本完成”。 diff --git a/docs/agent-handoff/evidence/P0-01.md b/docs/agent-handoff/evidence/P0-01.md new file mode 100644 index 0000000..df45beb --- /dev/null +++ b/docs/agent-handoff/evidence/P0-01.md @@ -0,0 +1,45 @@ +# P0-01 配置路径与测试隔离 — 执行证据 + +日期:2026-09-16(无人值守轮次) +平台:Windows 11 10.0.26200 x64 · Python 3.10.21(`.venv`)· PyQt6/Qt 6.10.0/6.10.2 · 离屏 `QT_QPA_PLATFORM=offscreen` + +## 根因(源码确认) + +1. `core/llm_engine.py` 模块常量 `CONFIG_PATH` + `_load_config()` 不读 `HAOCODE_CONFIG_FILE` → 三个 Qt worker(Agent/Chat/Title)直接读项目内真实配置。 +2. `ui/views/main_window.py:3133`(webview_backend 分支)与 `:5244`(`init_model_popup`)直接 `open(data/config.json)`,绕过环境变量。 +3. `ui/views/bash_panel.py` 有私有的环境变量解析(双入口,非统一)。 +4. `core/db_manager._DEFAULT_DB` 为模块级全局,测试在 import 前重定向的既有模式成立,沿用。 + +## 修复摘要 + +- 新增 `core/config_paths.py`:`config_path()`(`HAOCODE_CONFIG_FILE` 优先、调用时解析)+ `load_config()`(缺失/损坏/非对象 → 可见警告 + 安全空 dict,不抛异常)。 +- `core/llm_engine.py`:删除 `CONFIG_PATH` 常量;`_load_config()` 委托 `load_config()`(保留函数名兼容既有调用方)。 +- `ui/views/main_window.py`:两处直接 open 改走 `core.config_paths.load_config`。 +- `ui/views/bash_panel.py`:`_cfg_path()` 委托统一 `config_path()`,删除私有 `_CFG_PATH` 常量。 +- 新增 `tests/_test_env.py`:`isolate(tag, config)` 统一创建临时配置 + 临时数据库并在 import MainWindow 前完成重定向。 +- 改造 3 个在范围测试使用统一临时环境:`tests/smoke_bash_panel.py`、`tests/test_error_persist.py`、`tests/test_agent_core.py`(provider 用例改读临时配置中的 `testprov`,不再依赖真实配置)。 +- 新增 `tests/test_config_isolation.py`:open/sqlite 拦截器 + 缺失/损坏/非对象回归 + AST 静态扫描。 + +## 定向测试(命令 / 退出码 / 结果) + +| 命令 | 退出码 | 结果 | +|---|---|---| +| `python tests/test_config_isolation.py` | 0 | ALL PASS(14 项断言) | +| `python tests/test_error_persist.py` | 0 | 39 PASS(与交接基线 39 一致) | +| `python tests/smoke_bash_panel.py` | 0 | 116 PASS(与交接基线 116 一致) | +| `python tests/run_tests.py`(test_agent_core.py 的仓库标准运行方式,pytest 由 harness stub) | 0 | 41 passed, 0 failed(与交接基线 41 一致) | + +注:`python tests/test_agent_core.py` 直接运行在本仓库不可用(文件无独立 runner 且 .venv 不装 pytest,见 requirements.txt 说明),按其设计经 `tests/run_tests.py` 运行;P2-04 聚合入口将统一固化该运行方式。 + +## 完成证据对应 + +- **拦截器证明**:MainWindow 构造 + `save_panel_width` 写回全程,所有 `config.json`(含原子写 `.tmp`)打开路径均位于 `tempfile.gettempdir()/haocode_test_cfgiso_/`;真实配置路径(仅以字符串比较)从未出现在打开记录中。未读取、未散列真实配置。 +- **临时配置读写**:`llm_engine._load_config()` 读到 `testprov`;`save_panel_width(340)` → `load_panel_width() == 340`,写路径落临时目录。 +- **数据库隔离**:sqlite3.connect 拦截记录中临时库之外零连接/写入。 +- **缺失/损坏/非对象**:三个回归用例均返回 `{}` 且 stdout 含明确警告(`[config] 配置文件缺失/读取/解析失败/不是 JSON 对象`),进程正常退出。 +- **静态扫描**:`core/`、`ui/`、`tools/`、`main.py` 中除 `core/config_paths.py` 外不存在 `config.json` 字符串字面量(AST 级,docstring/注释排除)。 + +## 观察项(未扩范围,留待后续) + +- `tests/smoke_offscreen.py`、`smoke_mode.py`、`smoke_copy_session.py` 只重定向了数据库、未设置 `HAOCODE_CONFIG_FILE`(不在 P0-01 允许修改清单内)。本轮运行这些套件时在启动环境显式导出临时配置;P2-04 聚合入口将按子进程强制注入临时环境,彻底闭环。 +- `tests/diag_live_agent.py:19`、`tests/tune_model_popup.py:155` 直接引用真实配置路径;二者属 live/tune 人工脚本,默认聚合排除。 diff --git a/docs/agent-handoff/evidence/P0-02.md b/docs/agent-handoff/evidence/P0-02.md new file mode 100644 index 0000000..7d33855 --- /dev/null +++ b/docs/agent-handoff/evidence/P0-02.md @@ -0,0 +1,56 @@ +# P0-02 合并重复的 `MainWindow.eventFilter` — 执行证据 + +日期:2026-09-16(无人值守轮次) +平台:Windows 11 10.0.26200 x64 · Python 3.10.21(`.venv`)· PyQt6/Qt 6.10.0/6.10.2 · 离屏 `QT_QPA_PLATFORM=offscreen` + +## 根因(源码确认) + +- `MainWindow` 类体内定义了两个 `eventFilter`(行 3697 与 3763):后定义者覆盖前者,前者的 + `_active_streams` 守卫是死代码。 +- 生效版本(3763)用 `btn_send.isEnabled()` 做守卫,而 `set_send_button_state` 只切换 + 图标、从不禁用按钮 → 守卫恒真 → 流式生成中按 Enter 会落入 `send_message` 的中断路径 + (触发停止),与注释声称的「生成时按回车无效,防止误触」相反。 + +## 修复摘要(仅 `ui/views/main_window.py` 事件过滤逻辑) + +- 删除行 3763 的重复 `eventFilter`(及其后不可达的两行过期分节注释)。 +- 保留行 3697 处为 `MainWindow` 唯一 `eventFilter`:Enter(无 Shift)→ `send_message(from_enter=True)` + 并消费事件(一次按键至多一次调用);Shift+Enter → 返回 False 放行换行;其他对象/事件交父类。 +- `send_message(self, from_enter: bool = False)`:函数顶部为发送规则单一实现: + 1) `btn_send` 禁用 → 一律不发送; + 2) `from_enter=True` 且当前会话在 `_active_streams` → 直接返回(Enter 不参与停止/中断语义); + 3) 按钮点击路径行为完全不变(流式中点击 = 原有红色停止按钮中断语义,含 Fix B/C)。 +- `_update_send_button_state` 未改(其语义与规则一致)。 + +## 定向测试(命令 / 退出码 / 结果) + +| 命令 | 退出码 | 结果 | +|---|---|---| +| `python tests/test_main_window_event_filter.py` | 0 | ALL PASS(18 项断言) | +| `python tests/smoke_offscreen.py` | 0 | ALL PASS: 8/8 | +| `python tests/smoke_mode.py` | 0 | ALL PASS | +| 回归 `python tests/test_config_isolation.py` | 0 | ALL PASS(14 项) | +| 回归 `python tests/test_error_persist.py` | 0 | 39 PASS | +| 回归 `python tests/smoke_bash_panel.py` | 0 | 116 PASS | + +注:`smoke_offscreen.py` / `smoke_mode.py` 自身只重定向数据库、未设置 `HAOCODE_CONFIG_FILE` +(不在 P0-02 允许修改清单内)。本轮运行时在进程环境显式导出指向临时配置的 +`HAOCODE_CONFIG_FILE`;P2-04 聚合入口将按子进程强制注入临时环境,彻底闭环。 + +## 完成证据对应(test_main_window_event_filter.py) + +- **AST 静态断言**:解析 `ui/views/main_window.py`,`MainWindow` 类体内 `eventFilter` 定义恰好 1 个(行 3697); +- **Enter 可发送**(A1–A5):空闲 + 按钮可用 + 有文本 → 一次 Enter 恰好一次 `send_message(from_enter=True)` + 调用(计数 wrapper 包住真实实现),流同步注册、输入框清空、错误路径自清理; +- **Enter 被禁用**(B1–B3):`btn_send.setEnabled(False)` → 至多一次调用且无流、输入内容保留(规则在 `send_message` 内生效); +- **流式时 Enter 被拦截**(C1–C3):注入假流 → Enter 后假流对象未被替换、字段未被改动(未触发中断)、输入保留; +- **Shift+Enter 换行**(D1–D3):零调用,事件放行到输入框(光标处插入 `\n`)、无流; +- **其他键/事件交父类**(E1–E2):按 `a` 正常插入字符、零发送调用; +- **一次按键至多一次调用**:A/B/C/D 各用例均以调用计数断言(全部 ≤1 且语义正确)。 + +## 行为变化说明 + +- 流式生成中按 Enter:旧(生效)代码会触发停止/中断;新代码 no-op(Enter 只管发送, + 停止只走按钮)。这与被覆盖版本注释中声明的原始意图(「生成时按回车无效,防止误触」) + 和 P0-02 硬约束(「流式生成时不得发送」)一致,属本任务预期的确定性修复。 +- 发送按钮点击路径(含流式中点击 = 中断):逐行未动。 diff --git a/docs/agent-handoff/evidence/P0-PHASE_REGRESSION.md b/docs/agent-handoff/evidence/P0-PHASE_REGRESSION.md new file mode 100644 index 0000000..c046f24 --- /dev/null +++ b/docs/agent-handoff/evidence/P0-PHASE_REGRESSION.md @@ -0,0 +1,42 @@ +# P0 阶段完整回归 — 执行证据 + +日期:2026-09-16(无人值守轮次) +平台:Windows 11 10.0.26200 x64 · Python 3.10.21(`.venv`)· PyQt6/Qt 6.10.0/6.10.2 · Node(test_math_extract) +运行方式:全部子进程统一注入 `HAOCODE_CONFIG_FILE` 指向临时配置(`smoke_offscreen/smoke_mode/smoke_copy_session` 自身不设该变量,属 P2-04 前已知观察项);GUI 套件 `QT_QPA_PLATFORM=offscreen`。 + +## 结果(命令 / 退出码 / 摘要) + +纯逻辑(13 个套件,全部 EXIT=0): + +| 套件 | 摘要 | +|---|---| +| `python tests/run_tests.py` | 41 passed, 0 failed | +| `python tests/test_tool_params.py` | ALL PASS | +| `python tests/test_compaction_persist.py` | ALL PASS | +| `python tests/test_copy_session.py` | ALL PASS | +| `python tests/test_bash_stream.py` | ALL PASS | +| `python tests/test_error_persist.py` | ALL PASS(39 项) | +| `python tests/test_wv2_guard.py` | ALL PASS | +| `python tests/test_debug_window.py` | 22 PASS / 0 FAIL | +| `python tests/test_think_code_neutral.py` | ALL PASS | +| `python tests/test_file_attach.py` | OK | +| `python tests/test_pdf_reader.py` | OK | +| `python tests/test_config_isolation.py` | ALL PASS(14 项,P0-01 新增) | +| `python tests/test_main_window_event_filter.py` | ALL PASS(18 项,P0-02 新增) | + +离屏 GUI(4 个套件,全部 EXIT=0): + +| 套件 | 摘要 | +|---|---| +| `python tests/smoke_offscreen.py` | ALL PASS: 8/8 | +| `python tests/smoke_mode.py` | ALL PASS | +| `python tests/smoke_copy_session.py` | ALL PASS | +| `python tests/smoke_bash_panel.py` | ALL PASS(116 项) | + +JS(1 个套件,EXIT=0):`node tests/test_math_extract.js` — 39 passed, 0 failed。 + +## 判定 + +P0 阶段回归通过:测试不访问真实配置/数据库(拦截器证明 + 临时环境),`MainWindow` +输入行为无回归(P0-02 四态断言 + 既有 116 项 bash 面板回归)。真实桌面矩阵属 +P1/P2 阶段(PLATFORM_PLAN 验收矩阵),本阶段不声称桌面已验证。 diff --git a/docs/agent-handoff/evidence/P1-01.md b/docs/agent-handoff/evidence/P1-01.md new file mode 100644 index 0000000..a9005bd --- /dev/null +++ b/docs/agent-handoff/evidence/P1-01.md @@ -0,0 +1,90 @@ +# P1-01 双向消息渲染窗口 — 完成证据 + +**状态**: COMPLETE +**完成时间**: 2026-07-21(会话时间) +**环境**: Windows 11 x64, CPython 3.10.21 (.venv, uv), PyQt6 / Qt 6.10.0, Node + +## 设计 + +**数据流(引擎无关)** +- JS → Python:`bridge.onRequestWindowPage(sessionId, direction, boundaryId, generation)` + (QtWebChannel slot 与 WebView2 postMessage 白名单同名,单一实现)。 +- Python → JS:`run_js("rwPageResponse()")` / `rwInitWindow()` / `rwNoteLive(...)` / `rwBegin(...)` / `rwConfig(...)`。 +- JS 端只做"窗口游标 + DOM 搬移":Python 用既有逐消息 bridge 调用渲染 DOM, + JS 状态机 `ui/web/render_window.js` 跟踪 `order`(消息 id 序列)+ `indexById`(链内下标), + 不解析消息内容。 + +**关键参数** +- 窗口 = `render_window_size`(10/40/200,非法静默回落 40;mode `auto`/`manual` 回落 auto)。 +- **页 = 半窗**(`max(1, size//2)`)。若页 = 整窗,"首个可见消息"锚点必然被裁出窗口, + 需求中的锚点恢复(≤2px)永远不可达——诊断实测验证了这一点后才改为半窗。 +- 活动流式消息受保护(`trimHead`/`trimTail` 跳过 `activeStreamId`),计入上限; + `streamFinished` 解除保护。`rwNoteLive` 经 DOM `.streaming` 类自动接管流式保护, + 并携带实时 `chainLen` 维持 hidden 计数新鲜。 + +**滚动语义** +- 批次渲染期间 `window.__rwPageRendering = true`,抑制 `softScroll()` 与 + `finishMessage` 的 `scrollIntoView`。 +- **守卫在调度时刻捕获**(`var rwBatchSuppressed = !!window.__rwPageRendering` 后立即 + rAF+50ms 延迟回调):延迟回调触发时批次已结束、标志已被 Python 复位,届时再读会漏放 + `scrollIntoView(smooth)` → 平滑滚底 → `rwAutoCheck` 误判贴底 → 触发 'newer' 反向换页振荡。 + 这是诊断中 `scrollY 0→4367` 振荡的根因,已修复。 +- 向上换页锚点恢复:`scrollTop = oldScroll + (anchor.newTop - anchor.docTop)`, + 绝对顶部(`oldScroll <= 1`)例外:停在 0 露出新页;auto 模式沿顶部 60ms 链式补页。 +- 向下换页两模式均自动恢复(`isNearBottom() && canRequest('newer')`)。 +- 过期响应(会话/代次不匹配、pending 已被新请求替换、重复投递)整批丢弃,已渲染 DOM 回滚。 + +**生成代次(generation)** +- Python `MainWindow._rw_generation` 为唯一权威源:每次 `load_messages_to_web` / 新建会话 +1。 +- JS `clear()` 本地防御性 +1,Python 下次推送重新同步。 + +## 文件变更 + +| 文件 | 变更 | +|---|---| +| `core/config_paths.py` | 新增 `render_window_settings()`(16 类配置归一化)、`DEFAULT_RENDER_WINDOW_SIZE`、`ALLOWED_RENDER_WINDOW_SIZES` | +| `ui/web/render_window.js` | 新增:DOM 无关状态机(id+chainIndex 模型,双遍 recompute) | +| `ui/web/app.js` | `rwState`/`rwInitWindow`/`rwPageResponse`/`rwApplyOlder`/`rwApplyNewer`/`rwNoteLive`/`rwRequestPage`/`rwAutoCheck`/`rwCaptureAnchor`/`rwRemoveMessageDom`/`rwEnsureLoadButtons`/`rwUpdateLoadButtons`;`clearChat` 联动状态机清空;`finishMessage` 步骤 C 调 `streamFinished`、步骤 F 守卫捕获式;`createMessage`/`createLongMessage`/`createUserMessageWithAttachments` 的 `softScroll` 守卫;scroll 监听挂 `rwAutoCheck` | +| `ui/web/index.html` | 引入 `render_window.js`(先于 app.js);WebView2 shim 增加 `onRequestWindowPage` | +| `ui/web/style.css` | `.load-window-btn` 样式(含 `[hidden]` 规则) | +| `ui/views/chat_bridge.py` | 信号 `window_page_requested`、slot `onRequestWindowPage`、推送方法 `rw_config`/`rw_begin`/`rw_init_window`/`rw_note_live`/`rw_page_response` | +| `ui/views/wv2_view.py` | `_BRIDGE_METHODS` 白名单加 `onRequestWindowPage` | +| `ui/views/main_window.py` | `init_browser` 窗口状态初始化 + 信号连接;`_on_js_ready_checked` 一次性 `rwConfig` 推送;`load_messages_to_web` 窗口化重写(代次+1、`rwBegin`、最新 size 条窗口渲染、`rwInitWindow`、流式恢复保留);新增 `_rw_visible_chain`/`_rw_note_live`/`_render_history_one`/`_on_window_page_request`(边界缺失安全降级空页);`on_new_chat_clicked` 代次+1;发送/重答/完成 5 处 `_rw_note_live` 挂点 | +| `tests/test_render_window.js` | 新增:状态机 Node 测试 | +| `tests/diag_render_scale.py` | 新增:400 条链 offscreen 规模诊断(真实 viewport:resize+show+等待 innerHeight>0+显式重载) | +| `tests/smoke_timeline.py`、`tests/smoke_midswitch.py` | 转换到 `tests/_test_env.isolate()`(临时 DB+临时配置,P0-01 铁律) | +| `tests/_probe_rw.py` | 调试探针(保留,供后续排障) | + +## 验证(全部 EXIT=0,全部带显式超时执行) + +| 套件 | 结果 | +|---|---| +| `node tests/test_render_window.js` | **424/424 PASS**(配置归一化 16 例、auto/manual×10/40/200 初始窗口、双向连续换页 39 页/向、短链、过期响应 4 类、活动流保护、clear 语义、锚点几何、原子性、noteLive) | +| `python tests/diag_render_scale.py 400`(offscreen) | **6/6 PASS**:初始窗口=最新 40 条(链长 400);中部锚点保持(误差 ≤2px、无 newer 振荡);自顶部向上分页至头部(绝对顶部例外);头部状态+全链 400 条可达无重复;自顶部向下回翻 2 页;auto 模式顶部自动补页。**锚点误差 0.00px**;18 页 @ 169/183/204 ms(min/avg/max);DOM 节点 1057–1080;页面高度 7127–7213 px | +| `python tests/smoke_offscreen.py` | 8/8 ALL PASS | +| `python tests/smoke_timeline.py` | 11/11 ALL PASS(流式/历史路径,含 streaming 类收尾) | +| `python tests/smoke_midswitch.py` | 7/7 ALL PASS(切走切回时间线完整) | +| `python tests/test_file_attach.py` | 9 tests OK | +| `node tests/test_math_extract.js` | 39/39 PASS | +| 回归:`smoke_bash_panel` / `test_config_isolation` / `test_main_window_event_filter` / `smoke_mode` / `test_error_persist` | 全部 ALL PASS | +| `python tests/run_tests.py`(agent core 规范入口) | **41/41 PASS** | + +## 诊断过程记录(问题 → 根因 → 修复) + +1. **`rw_init_window` 链下标偏移**:初始窗口传入局部下标 0..39 而非链下标 → `hiddenOlder` 恒 0。 + 修复:`offset = total - len(window_items)`,传 `offset + i`。 +2. **offscreen 零视口**:未 `resize`+`show` 前 `innerHeight=0`,锚点几何全废。 + 修复(诊断侧):`window.resize(1400,950)` + `show()` + 等待 `innerHeight>0` + 显式 `load_messages_to_web` 重载。 +3. **`runJavaScript` 不能返回 DOM 元素**:回调转换失败 → 用 `cond ? 1 : 0` / 原语返回值。 +4. **分支兄弟偷叶**:链尾补兄弟消息使 `add_message` 自动改叶 → 链被截断。 + 修复(夹具):兄弟消息在循环内 `i==298` 处插入。 +5. **页 = 整窗导致锚点必被裁**(设计缺陷):半窗页修复(见"关键参数")。 +6. **`finishMessage` 守卫延迟求值 → 滚底 → 'newer' 振荡**(见"滚动语义"第 2 条)。 +7. **中部换页落点贴底**:15% 视口位置向上换页后锚点落 65%(不贴底);底部半窗换页本身会 + 落向底部属半窗几何固有——真实入口(顶部"加载更早消息"按钮 / auto 顶部观察器)不触发该位置, + 且落底后自动 'newer' 恢复符合"向下自动恢复"需求。 + +## 已知观察项(不在本任务范围) + +- `diag_render_scale` 的每页耗时(~180ms)只作真机基准参考,非硬阈值(符合任务要求)。 +- 帧耗时真机人工基准报告留待人工验收环节。 diff --git a/docs/agent-handoff/evidence/P1-02.md b/docs/agent-handoff/evidence/P1-02.md new file mode 100644 index 0000000..b1762c7 --- /dev/null +++ b/docs/agent-handoff/evidence/P1-02.md @@ -0,0 +1,74 @@ +# P1-02 证据:Windows/Linux shell 与进程树终止 + +日期:2026-07-09(无人值守轮次) +状态:**完成(Windows 侧自动化全绿;Linux 侧逻辑已实现并单测覆盖参数/提示词,进程组用例在 Linux 上运行时生效)** + +## 目标(摘自 REPAIR_BACKLOG.md) + +- Windows 明确通过 `cmd.exe` 执行;Linux 明确通过 `/bin/bash -lc` 执行,不依赖 `shell=True` 的平台默认值。 +- 超时与主动中止都终止完整子进程树(Windows `taskkill /F /T`;Linux 独立 POSIX 进程组,SIGTERM→宽限→SIGKILL 整组)。 +- 只保留一份通用 `SYSTEM_PROMPT.md`,运行时插入**短**平台 shell/path 段;两平台互不串段。 +- 保留输出流、超时、截断、工具结果结构;不加命令审批/沙箱/路径限制。 + +## 改动文件 + +| 文件 | 改动 | +|---|---| +| `core/platform_shell.py` | **新增**窄平台适配:`shell_command()`、`popen_flags()`、`kill_process_tree()`、`shell_prompt_section()`、`apply_platform_section()`、占位符 `{{SHELL_PLATFORM_SECTION}}` | +| `core/agent/tools.py` | `tool_bash` 的 Popen 改 `shell_command(command) + popen_flags()`;`_kill_tree` 委托 `kill_process_tree`;移除 `ctx["shell"]` 隐式开关 | +| `core/llm_engine.py` | `load_system_prompt()` 读文件后过 `apply_platform_section()`(每次请求仍重读,既有行为不变) | +| `SYSTEM_PROMPT.md` | 通用正文化:第 1 节去 Windows 路径/conda 环境名;原 1.1「shell 真相」cmd 表整体移入运行时 Windows 段;工具表与 3.2 去掉 `cmd.exe`/`dir`/`findstr` 字样;占位符落在原 1.1 位置 | +| `tests/test_cross_platform_shell.py` | **新增** 20 项断言(A 平台参数 / B 提示词 / C 进程树 / D 安全边界) | + +## 关键设计决策 + +1. **Windows 用字符串命令行,不用 argv 列表。** + `["cmd.exe","/d","/c",cmd]` 列表形态会被 CPython `list2cmdline` 把内部引号转义成 `\"`,cmd 不认, + 带引号路径直接 `'...\python.exe"' is not recognized`(实测复现)。最终契约: + `cmd.exe /d /s /c ""` 作为**字符串**交给 CreateProcessW,cmd 按 /s 规则解析 /c 参数 + (外层引号剥离、内部引号保留)。实测矩阵:引号 Python 路径 `rc=0`;`echo a && echo b` 正确; + `%USERPROFILE%` 展开;`;`/单引号行为与旧文档一致。 + 注:旧 `shell=True` 之所以能跑,是因为 CPython 对带引号程序名直接 CreateProcess(不经 cmd); + 新契约统一显式过 cmd,行为更可预测且与提示词一致。 +2. **POSIX 安全不变量**:`kill_process_tree` 仅当 `os.getpgid(pid) == pid`(确认 `start_new_session` + 生效、子进程是组首)才 `killpg`,否则退化单进程 `kill`,绝不误杀调用方所在组。 + 流程:SIGTERM 整组 → 轮询至 `grace_s=3.0s` → SIGKILL 整组。 +3. **提示词单一来源**:`SYSTEM_PROMPT.md` 唯一;`apply_platform_section` 只做占位符替换, + 无占位符(兜底提示词)原样返回。`load_system_prompt()` 每请求重读 → 平台段永远对应当前平台。 + +## 验证结果(本机 Windows 11 x64,CPython 3.10.21,全部显式超时) + +| 命令 | 结果 | +|---|---| +| `python tests/test_cross_platform_shell.py` | **20/20 PASS**(EXIT=0) | +| `python tests/test_bash_stream.py` | **30/30 PASS**(EXIT=0) | +| `python tests/test_tool_params.py` | **35/35 PASS**(EXIT=0) | +| `python tests/run_tests.py`(agent core,含 test_agent_core) | **41/41**(EXIT=0) | +| `python tests/smoke_mode.py`(完整 agent 回合,走 tool_bash) | ALL PASS(EXIT=0) | +| P0-03 一致性检查(AGENTS.md 指针 / ARCHITECTURE 历史资料标记 / 无第三方任务书正文) | 3/3 PASS | + +### 测试点明细(test_cross_platform_shell.py) + +- **A 平台参数**:Windows 命令 == `cmd.exe /d /s /c "echo hi"`;Linux argv == `["/bin/bash","-lc","echo hi"]`; + Linux `popen_flags() == {"start_new_session": True}`,Windows 为空。 +- **B 提示词**:通用正文含占位符且无平台泄漏(无 `cmd.exe`/`/bin/bash`);Windows 段含 `cmd.exe` 无 + `/bin/bash`,Linux 段反之;替换后通用正文逐字节相同(B6);`load_system_prompt()` == 文件+当前平台段。 +- **C 进程树(真实进程,父挂 30s + 孙每 0.2s 写心跳文件)**: + - C1 超时 3s → 错误结果含「超时」,耗时 <15s,**父与孙都不存在**(心跳静默 >0.6s 且无完成标记); + - C2 主动中止 1.5s → 错误结果含「中止」,**父与孙都不存在**; + - C3 Linux 独立进程组(Windows 上 SKIP;`pgid==pid` 断言 + killpg 后子进程消失,Linux 运行即生效)。 +- **D 安全边界**:已退出进程、`None` 输入均不抛异常。 + +### 调试过程记录(铁律:所有调试命令显式超时) + +1. 首跑 C1 失败:`'...\python.exe"' is not recognized` → 定位为 `list2cmdline` 引号转义; + 读 CPython 3.10 `subprocess.py` 确认 `shell=True` 实为直接 CreateProcessW(带引号程序名不经 cmd)。 +2. 尝试 `["cmd.exe","/d","/s","/c", '"'+cmd+'"']` 列表 → 仍失败(同样被转义)。 +3. 改**字符串**命令行 + 实测矩阵(引号路径/&&/管道/%VAR%)→ 全过,定稿。 +4. 首跑进程树用例 `hb_last=None`:父脚本模板漏传 `child.py` 脚本路径(把心跳路径当脚本)→ + 手动 `subprocess.run` 复现(超时 6s/3s 探针)→ 修模板,20/20 通过。 + +## Linux 侧待办(不阻塞本任务) + +- 在 Linux 环境跑一次 `python tests/test_cross_platform_shell.py`(C3 生效)+ `test_bash_stream.py` + 即可闭环;代码路径与 Windows 共用同一套 `kill_process_tree`/`shell_command` 分派。 diff --git a/docs/agent-handoff/evidence/P1-03.md b/docs/agent-handoff/evidence/P1-03.md new file mode 100644 index 0000000..4c799d2 --- /dev/null +++ b/docs/agent-handoff/evidence/P1-03.md @@ -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__`,测试经 `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: /profile__*` | `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= 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_` 子目录)+ `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_guard` 10/10 无回归。 +- **Windows 真机 QtWebEngine 回落:已验证(2026-09-17 真桌面,`HAOCODE_FORCE_QTWEBENGINE=1`)**。日志:`[WV2] ... 跳过 WebView2,回落 QtWebEngine`(强制标志生效,**未执行 taskkill**)→ `[Renderer] QtWebEngine 独立 profile: data/webengine/profile__`(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 进程。 diff --git a/docs/agent-handoff/evidence/P1-04.md b/docs/agent-handoff/evidence/P1-04.md new file mode 100644 index 0000000..5b6e0a5 --- /dev/null +++ b/docs/agent-handoff/evidence/P1-04.md @@ -0,0 +1,59 @@ +# P1-04 证据:Linux 截图热键与截图实现 + +状态:Windows 侧自动化验证全绿;Linux X11/Wayland 真实宿主验证按 VERIFICATION.md 手动待办(本环境为 Windows 桌面)。 + +## 交付物(文件级) + +| 文件 | 变更 | +|---|---| +| `ui/views/system_tools/desktop_session.py` | **新增**(纯 stdlib):`session_kind()` → `win32/x11/wayland/unknown`(WAYLAND_DISPLAY / QT_QPA_PLATFORM=wayland / DISPLAY 判定,offscreen→unknown);`hotkey_plan(kind)` / `capture_plan(kind)` 能力路由 + 明确能力说明文案 | +| `ui/views/system_tools/x11_hotkey.py` | **新增**(窄适配器,零新依赖,ctypes→libX11):`X11HotkeyThread`(与 Windows `GlobalHotkeyThread` 同一公开面 `triggered/start/stop`);XOpenDisplay→XKeysymToKeycode('s')→XSelectInput(KeyPressMask)→XGrabKey(root, keycode, Mod1Mask, owner_events)→select(X 连接 fd, 0.2s)+XPending/XNextEvent 循环;命中 Alt+S 发射 `triggered`;XEvent 结构体按 xproto.h XKeyEvent 布局(64 位 keycode@76);`_open_x11()` 可注入(测试替身);`wait_ready()` | +| `ui/views/system_tools/portal_capture.py` | **新增**(窄适配器,系统 gdbus CLI,零 pip 依赖):`detect_portal()`(Linux + XDG_RUNTIME_DIR/DBUS_SESSION_BUS_ADDRESS + gdbus 探测);`portal_screenshot_sync()`:gdbus 调 `org.freedesktop.portal.Screenshot.Screenshot(handle, "/", {})` 取 request 对象路径 → `gdbus monitor --session --object-path ` 监听 `FilePicked`(成功,file:// URI 剥前缀+unquote 解码)/`Request.Finished`(无 FilePicked → 用户取消/拒绝);显式预算:request 10s + 等待 120s,超预算 → timeout;`PortalScreenshotWorker(QThread)` 信号 `done(ok, path)` 回主线程 | +| `ui/views/main_window.py` | 热键注册块:`desktop_session.hotkey_plan(session_kind())` 平台路由(win32→GlobalHotkeyThread 行为字节级保持;x11→X11HotkeyThread;wayland/offscreen→None + 明确"全局热键不可用"日志),非 Windows 保留应用内 `QShortcut(Alt+S)` 兜底;`_start_screenshot()` 路由:win32/x11→现有覆盖层、wayland→`_start_portal_screenshot()`(worker 完成→`_on_image_pasted([path])` 进现有图片附件流程)、unknown→明确"截图不可用,聊天与其他功能不受影响"日志 | +| `ui/views/system_tools/screen_capture.py` | `start()` 增加空画面守卫:无主屏幕 / grabWindow 返回空图(X11 个别 compositor 限制)→ 明确日志 + 不显示覆盖层(Wayland 已在路由层改走 portal) | +| `tests/test_global_hotkey_platforms.py` | **新增** 23 检查:H1 session_kind 矩阵(win32/x11/wayland/offscreen/无显示 + XDG_SESSION_TYPE 三分支 + 矛盾时 WAYLAND_DISPLAY 优先);H2 hotkey_plan 四路由;H3 X11 成功路径(fake libX11:XGrabKey 参数 keycode=39/Mod1Mask=1/root=123/owner_events=1、命中发射 triggered、stop 后 XUngrabKey+XCloseDisplay);H4 失败三分支(键被占用 XGrabKey=0 / 无显示 XOpenDisplay=None / 不支持组合不打开显示)均安静退出+明确日志;H5 Windows 路径保持(本机实测 RegisterHotKey 线程运行 + stop 干净释放) | +| `tests/test_screen_capture_platforms.py` | **新增** 17 检查:C1 capture_plan 四路由;C2 detect_portal 四分支(非 Linux/无 D-Bus/无 gdbus/齐备);C3 成功路径(fake subprocess 校验 gdbus 命令行 `--dest/--object-path/--method=...Screenshot/parent=/`、monitor 监听 request 对象、file:// 含空格文件名 URI 解码 → 真实文件);C4 授权被拒不伪造成功;C5 portal NotSupported 原因透出;C6 超预算 timeout(迟到信号不算成功);C7 worker 信号回主线程;C8 覆盖层 offscreen 构造+空画面守卫不崩 | + +## 关键设计决定 + +1. **Windows 字节级保持**:`GlobalHotkeyThread`(Win32 RegisterHotKey 线程)与覆盖层路径零改动,仅调用处改为经 `hotkey_plan("win32")` 取回同一工厂;`capture_plan("win32")` 仍返回 overlay。 +2. **X11 全局热键 = 原生 XGrabKey,无新 pip 依赖**:libX11 是 X11 桌面必然存在的系统库,ctypes 直调;只映射现有 Alt+S(`_VK_TO_KEYSYM` 窄表,扩展需显式加表项);`owner_events=1`;stop 走 XUngrabKey+XCloseDisplay(关连接本身即释放 grab,双保险)。 +3. **Wayland = xdg-desktop-portal,不绕过 compositor**:compositor 安全模型禁止应用直接抓屏,故 Wayland 截图走 `org.freedesktop.portal.Screenshot`(交互式授权窗口,用户批准/取消);`gdbus`(GLib 系统组件)CLI 完成 D-Bus 调用,不引入 dbus-python;成功返回文件路径 → 直接进现有 `_on_image_pasted([path])` 附件流程(不经过 Qt 覆盖层,因为 Wayland 下无法把画面抓进 Qt widget)。 +4. **Wayland 全局热键:明确"不可用"而非硬做**:通用全局快捷键在 Wayland 依赖 compositor 桌面协议(ext-global-shortcut-unstable-v1 等,无统一 portal API),免依赖实现需完整 Wayland 客户端协议栈,超出窄适配器范围 → `hotkey_plan("wayland")` 返回 None + 日志明确说明(保留应用内 Alt+S + 截图按钮),符合硬约束"不支持时界面/日志必须明确说明能力不可用,主程序仍可聊天"。 +5. **能力不可用的一等公民**:`session_kind()=unknown`(offscreen/无显示)时,热键与截图都有显式日志("全局热键不可用"/"截图功能不可用;聊天与其他功能不受影响"),不再静默。 +6. **异常绝不逃逸 QThread.run()**:X11 事件循环整体 try/except——PyQt6 中 QThread.run() 未处理异常会 **abort 整个进程**(实测:CArgObject TypeError 逃逸 → 进程静默 127 退出、无 traceback、stdout 缓冲丢失)。这是本任务最贵的一个坑,已把"QThread.run() 必须全捕获"写进教训。 +7. **超时语义**:portal 等待中,FilePicked 之前/之后的信号都看预算——`Finished` 在预算内 → denied(用户取消);任何结果晚于预算 → timeout(不伪造、不无限挂起)。 + +## 验证(全部显式超时) + +| 套件 | 结果 | +|---|---| +| `python tests/test_global_hotkey_platforms.py` | **23/23 PASS** EXIT=0(timeout 120) | +| `python tests/test_screen_capture_platforms.py` | **17/17 PASS** EXIT=0(timeout 180) | +| `python tests/test_file_attach.py`(回归) | 9 tests OK(timeout 90) | +| `python tests/smoke_offscreen.py`(回归,offscreen+software+--disable-gpu) | **ALL PASS 8/8**(timeout 240),日志含 `[GlobalHotkey] Windows:系统级全局热键 Alt+S(RegisterHotKey)` | +| `python tests/run_tests.py` | 41/41 | +| `tests/test_main_window_event_filter.py` / `test_config_isolation.py` / `test_wv2_guard.py` / `test_renderer_matrix.py` / `test_cross_platform_shell.py` | ALL PASS / ALL PASS / ALL PASS / 19/19 / 20/20 | +| `main.py` 真实启动链(offscreen + 临时 config,timeout 45→124 kill 预期) | 无 traceback;`[GlobalHotkey] Windows:系统级全局热键 Alt+S` 打印;到达"JS 引擎已就绪";`data/config.json` mtime 前后一致(未触碰);error 行仅为 offscreen 已知 GPU 回落噪音 | + +## 测试点细节(对应目标测试要求) + +- **平台路由矩阵**:H1(session_kind 9 分支,含 XDG_SESSION_TYPE 与矛盾优先级)+ H2(hotkey_plan 4 路由)+ C1(capture_plan 4 路由)。 +- **X11 替身**:FakeX11 鸭子类型 libX11(socketpair 提供可 select 的 fd;XEvent 用 `ctypes.memmove` 填充;`CArgObject._obj` 从 `ctypes.byref(ev)` 还原原 struct——生产走真实 CDLL 不受影响)。 +- **注册失败→明确消息**:H4.1 键占用("Alt+S 可能已被其他程序占用")、H4.2 无显示("XOpenDisplay 失败")、H4.3 不支持组合("暂不支持的快捷键组合")——均无信号、`_registered=False`、线程安静退出。 +- **portal 替身 subprocess**:FakeRun/FakePopen 记录 argv 并回放 gdbus 输出(成功/拒绝/NotSupported/超时四态),验证真实 CLI 命令行与 JSON 事件解析,不依赖真实 portal。 +- **X11/Wayland 真实行为**:本环境为 Windows 桌面,X11/Wayland 真机按 VERIFICATION.md 手动(见下)。 + +## 待办(需真实 Linux 宿主,按 VERIFICATION.md 手动) + +1. Ubuntu 22.04/24.04 x64 X11 会话:`python3.10 main.py` → 日志 `[GlobalHotkey] X11:原生全局热键 Alt+S(XGrabKey)`;窗口失焦按 Alt+S 弹出覆盖层、框选截图成功;XGrabKey 被占用时日志明确。 +2. Wayland 会话(GNOME/KDE):启动日志 `[GlobalHotkey] Wayland:…未启用 → 仅提供应用内 Alt+S…`;点截图按钮(或应用内 Alt+S)→ xdg-desktop-portal 授权窗口出现;批准 → 文件进入附件;取消 → 日志"portal 截图未完成…已取消";无 portal 时日志"portal 不可用"。 +3. 其他发行版/DE 标记"未验证"(硬约束:不做发行版泛化)。 + +## 教训(持久) + +- **PyQt6:QThread.run() 内任何未处理异常 = abort 整个进程**(退出码 127、无 traceback、stdout 缓冲丢失,极难诊断)。QThread.run() 必须顶层 try/except 全捕获 + 日志。 +- `ctypes.byref(x)` 返回 `CArgObject`:真实 CDLL 调用正常,但传给**普通 Python 可调用对象**(测试替身)时对方 `byref()` 会 TypeError——替身端用 `getattr(arg, "_obj", arg)` 还原。 +- Windows 上 `os.pipe()` 的 fd 不能可靠用于 `select()`(无 WSAStartup 时 WinError 10093;初始化后是普通 pipe 又 10038)——跨平台可 select 的假 fd 用 `socket.socketpair()`;且 `a.send()` 的数据在 **b** 的接收缓冲(方向别写反)。 +- `file://` URI 解析用"剥前缀 + unquote"而非 `urlparse().path`:`file://D%3A%5Cx`(无第三斜杠)会被 urlparse 当成 netloc → path 为空。 +- gdbus 的 Screenshot 结果信号(FilePicked/Finished)都发在 **request 对象**上(方法返回的句柄路径),不是单独 handle 对象。 diff --git a/docs/agent-handoff/evidence/P2-01.md b/docs/agent-handoff/evidence/P2-01.md new file mode 100644 index 0000000..d213c8d --- /dev/null +++ b/docs/agent-handoff/evidence/P2-01.md @@ -0,0 +1,50 @@ +# P2-01 证据:Bash 任务按启动时间倒序 + +完成日期:2026-07-21(无人值守轮次) +结论:**完成**。两栏均按启动顺序降序显示(最新启动在第一项);运行中→已完成保持原启动位置;重排复用同一批 `BashLayer` 实例,全部 UI 状态保持。目标测试与回归全绿。 + +## 交付物 + +| 文件 | 改动 | +|---|---| +| `ui/views/bash_panel.py` | 模块 docstring 口径更新;`_refresh()` 两栏显示顺序改为启动序号降序;`layer_ids()` 的 running/done 返回真实显示顺序("all" 仍为原始启动正序,调试口径不变) | +| `tests/smoke_bash_panel.py` | 新增第 11 节(P11.1–P11.20 共 23 项断言);收尾改 `os._exit`(offscreen 铁律,修复解释器退出挂起) | + +未改动:`main_window.py`(事件转发链已是实时、按启动到达顺序带 `call_id` 转发,面板从到达顺序推导启动序号,无需新增元数据)、数据库 schema、`set_layers`(本就复用实例)。 + +## 关键设计决策 + +1. **排序键 = `self._order` 中的位置(稳定启动序号),不引入时间戳、不改 schema。** + 面板的 `_order` 在三个入口按启动先后追加: + - `set_session()` DB 重建:消息链顺序 + 时间线内顺序(= backlog 要求的「稳定启动序号」构造方式); + - `set_session()` 活动流:时间线内顺序(当前轮次天然晚于历史); + - 实时 `on_started()`:事件到达顺序。 + `_order` 即启动序号本身,`_refresh()` 只需对其取逆即可,无需任何新字段。 +2. **只在显示层取逆,不改内部数据。** `run_ids`/`done_ids` 仍按 `_order` 正序过滤;`reversed()` 只作用于传给 `set_layers` 的 widget 列表。已完成栏限量窗口 `done_ids[-LAYER_LIMIT:]` 的成员不变(仍是「最近启动的 30 个」),仅窗口内显示顺序反转,提示语文义保持。 +3. **完成时间从不参与排序。** `on_finished` 对已知层只改状态集合(`_running`→`_done`),绝不移动 `_order` 位置;仅当层完全未知(先收到 finished 事件)才以首次感知时间追加——这是唯一的信息可用时刻。因此「先启动后完成」的任务永远压在「后启动先完成」的任务之下,与结束先后无关(P11.10/P11.13 断言)。 +4. **状态保持靠「同一对象」。** `set_layers` 逻辑未动:`takeAt → setParent(None) → addWidget → show`,操作的是同一批 `BashLayer` 实例。展开/折叠(`expanded` + `body` 显隐)、实时缓冲(`_live`)、代码框滚动值(`out_box`/`arg_box` 子控件属性)、两栏 section 滚动位置(`QScrollArea` 自身属性,子层重排不触碰)全部天然保持,测试逐项断言(P11.4–P11.8、P11.13b–d)。 + +## 验证(全部显式 timeout) + +| 套件 | 结果 | 预算 | +|---|---|---| +| `tests/smoke_bash_panel.py`(含新增 P11 节) | **ALL PASS(140 项断言)EXIT=0** | 240s | +| `tests/test_bash_stream.py` | **30/30 ALL PASS EXIT=0** | 180s | +| 回归 `tests/smoke_offscreen.py` | ALL PASS 8/8 EXIT=0 | 240s | +| 回归 `tests/run_tests.py` | 41 passed / 0 failed EXIT=0 | 300s | +| 回归 `test_main_window_event_filter` / `test_config_isolation` / `test_wv2_guard` | 均 ALL PASS EXIT=0 | 各 120s | + +新增断言要点(对应 backlog「完成证据」三条): +- **≥3 项任务以不同启动/完成顺序**:s1/s2/s3/s4/s6 + t0 + t1..t31 共 36 项已完成、交错完成(s2 先完成仍居顶、s3 最后完成插入启动位而非顶格),两栏均断言启动降序(P11.1/P11.2/P11.9–P11.13); +- **完成中间任务前后状态保持**:对象 identity(`is`)、展开态、实时输出文本、代码框水平滚动值(先强制非 0)、section 垂直滚动值(用 20 行内容撑出真实滚动范围后设 30)在重排后逐项相等(P11.3–P11.8、P11.13b–d); +- **DB 重建与实时一致**:切换会话后 3 条时间线条目按 `db2,db1,db0` 显示(消息链+时间线序的逆),`layer_ids()` 原始正序不变(P11.19/P11.20)。 + +## 测试中发现并处理的问题 + +1. **Qt 布局 flush 会重置代码框水平滚动(测试时序伪影,非产品 bug)**:展开层与 `setValue` 同 tick 执行时,`out_box` 的终宽布局尚未 flush,随后任何布局事件(如新任务触发的 `set_layers`)应用挂起 resize 会把水平滚动清零。探针矩阵(N1×N2 settle 圈数)证实:`setValue` 前至少一次事件循环(N2≥1)则滚动稳定保持。真实用户不可能在未渲染的框上滚动,故测试在设滚动值前补 `settle(120)` 并在注释中记录该伪影。 +2. **`smoke_bash_panel.py` 解释器退出挂起**:末行 `sys.exit(0)` 后 QtWebEngine 渲染/GPU 子进程(offscreen)不回收,进程挂到 timeout 124;此前跑该文件若经管道只看输出会误判通过。改为仓库 offscreen harness 惯例 `os._exit(code)`(stdout 已 flush、临时文件已清理),EXIT=0 即时返回。 +3. **`layer_ids()` 口径**:原返回启动正序;改为 running/done 返回真实显示顺序(便于测试直接断言所见即所得),"all" 保持原始正序。既有断言(单元素/`set()`/`len`)全部不受影响,140 项一次通过。 + +## 未验证项 + +无平台相关项(纯 UI 排序逻辑,offscreen 已全量覆盖)。 diff --git a/docs/agent-handoff/evidence/P2-02.md b/docs/agent-handoff/evidence/P2-02.md new file mode 100644 index 0000000..8639496 --- /dev/null +++ b/docs/agent-handoff/evidence/P2-02.md @@ -0,0 +1,48 @@ +# P2-02 证据:右侧 Bash 面板滚动条与横纵交汇角 + +完成日期:2026-07-21(无人值守轮次) +结论:**完成**。右侧任务面板的代码框(`#bl_code`)与两栏 section 滚动区(`#bl_scroll`)滚动条统一为 8px、无箭头、handle 可见且 hover;横纵交汇角用 `QPlainTextEdit::corner` 子控件染成代码框背景 `#fbfcfe`,原生亮色 corner 方块消除。所有选择器均限定在 `#bl_code`/`#bl_scroll`,未添加任何无作用域的 `QScrollBar`/`QAbstractScrollArea` 规则。 + +## 交付物 + +| 文件 | 改动 | +|---|---| +| `ui/views/main_window.py` | 主窗口全局 QSS 中 `#bl_code` 规则后插入一段**完全限定作用域**的滚动条 + corner 规则(`QPlainTextEdit#bl_code QScrollBar:*`、`QPlainTextEdit#bl_code::corner`、`QScrollArea#bl_scroll QScrollBar:*`、`QScrollArea#bl_scroll::corner`);`#bl_code` 原有背景/边框/圆角/文本样式一字未动 | +| `tests/diag_panel_scrollbar.py` | 新建:离屏测量 + 断言 + 局部截图(面板全貌、代码框 render 图、角落 4x 放大) | + +截图(`docs/agent-handoff/evidence/`):`p2-02-panel.png`、`p2-02-outbox-render.png`、`p2-02-codebox-corner-4x.png`。 + +## 关键设计决策 + +1. **作用域 = objectName 限定,零全局规则。** 主窗口全局 QSS 此前没有任何 `QScrollBar` 规则(各弹窗/附件区各自 `setStyleSheet`),右侧面板因此落到原生 Windows 滚动条(带箭头、17px、亮色 corner 方块)。新增规则全部写成 `QPlainTextEdit#bl_code …` / `QScrollArea#bl_scroll …` 形式,只可能匹配右面板内的对象名,结构上不可能泄漏到其他控件。 +2. **corner 用 `QAbstractScrollArea::corner` 子控件语法**(`QPlainTextEdit#bl_code::corner { background-color: #fbfcfe; }`)——Qt 文档支持的子控件,与代码框背景同色,即 backlog「corner 与代码框背景一致」;未使用不存在的 `QScrollBar::corner`。section 滚动区 `::corner` 置透明(其横向滚动条恒关,corner 本不显示,属保险)。 +3. **口径与仓库既有风格一致**:8px 厚、`add-line/sub-line` 置 0 隐藏箭头、`#d0d0d0` handle + `#a0a0a0` hover、圆角 4px——与 `modern_scrollbar_qss` 及附件预览区风格同源,只是作用域不同。 + +## 验证(全部显式 timeout) + +| 套件 | 结果 | 预算 | +|---|---|---| +| `tests/diag_panel_scrollbar.py`(新) | **18 项 ALL PASS EXIT=0** | 180s | +| `tests/smoke_bash_panel.py` | **140 项 ALL PASS EXIT=0** | 240s | +| 回归 `tests/smoke_offscreen.py` | ALL PASS 8/8 EXIT=0 | 240s | +| 回归 `tests/run_tests.py` | 41/41 EXIT=0 | 300s | +| 回归 `smoke_timeline` / `smoke_midswitch` / `test_main_window_event_filter` | 11/11、7/7、ALL PASS,均 EXIT=0 | 各 ≤300s | + +诊断脚本断言要点(对应 backlog「完成证据」): +- **厚度**:代码框横/纵滚动条实际几何 = 8px 且 `sizeHint` = 8px(S1/S1b/S2/S2b);section 竖滚动条实际 = 8px(S3); +- **箭头 extent**:`subControlRect(CC_ScrollBar, SC_ScrollBarSubLine)` 在样式代理下 = 0(S4 三项)——即箭头子控件零尺寸; +- **corner**:代码框 `render()` 图中,右下角 8×8 交汇块渲染出 `#fbfcfe`(9 px)、无 `(255,255,255)` 亮白像素(S5c/S5d); +- **无泄漏**:未命名 `QPlainTextEdit` 横滚动条仍为原生口径(14px,S6);附件预览滚动条保持自身 6px `sizeHint`(S7); +- 截图三张落盘 evidence 目录,含角落 4x 放大图。 + +Windows 真机截图(backlog「完成证据」第二条):**已完成(2026-09-17,Windows 11 真桌面,非 offscreen)**:`diag_panel_scrollbar.py` 直接运行 → EXIT=0、ALL PASS;实测代码框 H=8px V=8px、section V=8px、corner #fbfcfe 9 像素(与 offscreen 测量一致);三张截图已用真机渲染覆盖:`p2-02-panel.png`、`p2-02-outbox-render.png`、`p2-02-codebox-corner-4x.png`(07:20 时间戳)。运行期间生产 app 以 WebView2 在前台,diag 实例经 instance-lock 守卫自动回落 QtWebEngine,未误杀对方 WebView2 进程(T0 守卫真机验证)。Linux 真机截图仍待对应环境。 + +## 测试中发现的问题与教训 + +1. **offscreen 下 `widget.grab()` 对 `QPlainTextEdit` 的文档区不填充(黑图)**:`out_box.grab()` 整块 (0,0,0),但 `panel.grab()` 正常。改用 `ob.render(painter)`(渲染到透明 QPixmap)后:文本色 `#243043`、边框 `#e6eaf2`、handle `#d0d0d0`、corner `#fbfcfe` 全部出现——**样式子控件在 offscreen 下正常渲染,只有文档区背景填充缺失**(离屏渲染怪癖,非产品 bug)。像素断言一律走 `render()`,且必须先做健全性检查(文本色/handle 色像素计数 > 0)防黑图假通过。 +2. **PyQt6 API 坑(三连)**: + - `Qt.Vertical`/`Qt.Horizontal` 短名已移除 → `Qt.Orientation.*`; + - `QStyleOptionSlider(widget)` 构造器未绑定(只收无参/拷贝)→ 用 `QStyleOptionSlider()`; + - `subControlRect` 参数序是 `(ComplexControl, QStyleOption, SubControl, widget)`,且滚动条箭头子控件在 PyQt6 枚举里叫 `SC_ScrollBarSubLine`/`SC_ScrollBarAddLine`(不是 C++ 文档里的 `SC_DownArrowButton`);`CC_ScrollBar` 属于 `QStyle.ComplexControl` 而非 `ControlElement`。 + - 裸控件(无样式表祖先)的 `style()` 是基础风格且空 option 下 `subControlRect` 返回 0 矩形——「原生参照」只能靠 `sizeHint`/实际几何(如原生横条 14px)对照,不能靠 subControlRect。 +3. **测试数据**:`out_box` 要同时出横纵滚动条,必须「超宽单行(NoWrap 触发横条)+ 足够行数(触发纵条)」,只给长单行时纵条不可见。 diff --git a/docs/agent-handoff/evidence/P2-03.md b/docs/agent-handoff/evidence/P2-03.md new file mode 100644 index 0000000..b6d4a8c --- /dev/null +++ b/docs/agent-handoff/evidence/P2-03.md @@ -0,0 +1,95 @@ +# 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`(隔离临时 DB,offscreen,34 项断言,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 项断言) diff --git a/docs/agent-handoff/evidence/P2-04.md b/docs/agent-handoff/evidence/P2-04.md new file mode 100644 index 0000000..81e7c02 --- /dev/null +++ b/docs/agent-handoff/evidence/P2-04.md @@ -0,0 +1,104 @@ +# P2-04 证据:跨平台聚合测试入口 + +状态:COMPLETE +日期:2026-07-17(本机执行时间) +平台:Windows 11 10.0.26200 x64(主)+ WSL Ubuntu-22.04 / CPython 3.12.3(Linux 路径验证) + +## 交付物 + +- `tests/run_all.py`(新增,唯一新文件):跨平台聚合测试入口。 + - `python tests/run_all.py --group logic|offscreen|all`(默认 all) + - `--list` 只列条目;`--only ` 跑子集(调试);`--keep-logs` 保留全部子日志(默认只留失败/超时)。 + - 设计:每个子测试 = 独立子进程 + 独立临时目录(`haocode_RUN_`/`haocode_CONF_`,POSIX 走 TMPDIR、Windows 走 TEMP)+ 显式秒级 timeout(`subprocess.run(timeout=...)`);offscreen 组子进程注入 `QT_QPA_PLATFORM=offscreen`、`HAOCODE_RENDER=software`、`QTWEBENGINE_CHROMIUM_FLAGS=--disable-gpu`(均 setdefault,不覆盖外层)。 + - 退出码语义:任一 FAIL/TIMEOUT → 退出码 1(并打印"失败命令"清单);SKIP 不影响退出码。 + - SKIP 双闸门:①依赖探测(`importlib.util.find_spec`:node/openai/PyMuPDF/PyQt6,缺失即 SKIP 并给理由);②平台令牌(`platform:win32` 等,平台不适用即 SKIP 并给理由)。 + - 聚合鲁棒性:子进程崩溃/超时只记 FAIL/TIMEOUT,不中断后续;输出统一 UTF-8(`errors=replace`,Windows cp936/cp1252 宿主安全);WSL 时钟回拨保护(耗时 clamp ≥0)。 + - 不要求 pytest / npm / 网络;聚合器自身零第三方依赖。 + +## 默认聚合范围(25 条)与排除项 + +- logic 12 条:run_tests.py(agent core 41)、test_tool_params、test_bash_stream、test_copy_session、test_compaction_persist、test_file_attach、test_cross_platform_shell、test_global_hotkey_platforms、test_wv2_guard、test_pdf_reader、test_math_extract.js、test_render_window.js。 +- offscreen 13 条:smoke_offscreen、smoke_mode、smoke_copy_session、smoke_bash_panel、smoke_timeline、smoke_midswitch、test_main_window_event_filter、test_config_isolation、test_error_persist、test_think_code_neutral、test_debug_window、test_renderer_matrix、test_screen_capture_platforms。 +- 默认排除(有注释理由,不进默认聚合): + - `diag_*`(rename_overlay / panel_scrollbar / render_scale):人工/像素诊断,需真机或交互。 + - `verify_*`:人工验证脚本。 + - `tune_model_popup.py`:调参实验。 + - `smoke_offscreen.py` 之外的旧式 real-DB 冒烟(引用真实 `data/haocode.db`,未接临时库)。 + - `diag_live_agent.py`、`probe_agent_loop.py`:需真实 API 凭据。 + - `_probe_*`、`_test_env.py`:探针/工具模块(被其他测试 import,非独立用例)。 + - `run_all.py` 自身:聚合器不入聚合。 + +## 汇总结果(硬约束要求:Windows/Linux 各一份) + +### Windows 11 / CPython 3.10.21(.venv,PyQt6+openai+PyMuPDF 齐全) + +| 组 | 结果 | 耗时 | +|---|---|---| +| `--group logic` | **PASS 12 / FAIL 0 / SKIP 0**(EXIT=0) | 43.9s | +| `--group offscreen` | **PASS 13 / FAIL 0 / SKIP 0**(EXIT=0) | 81.9s | +| `--group all` | 25/25 PASS(EXIT=0,两组顺序执行) | ~126s | + +- 日志样例:`D:/tmp/runall_win_logic2.log`、`D:/tmp/runall_win_offscreen.log`。 +- offscreen 明细:smoke_offscreen 8.3s、smoke_bash_panel 30.3s、smoke_midswitch 8.7s、test_think_code_neutral 9.3s 等 13 条全 PASS。 + +### WSL Ubuntu-22.04 / CPython 3.12.3(系统 python3,无 PyQt6/openai/PyMuPDF,离线环境) + +| 组 | 结果 | 耗时 | +|---|---|---| +| `--group all` | **PASS 4 / FAIL 0 / SKIP 21**(EXIT=0) | 0.9s | + +- PASS 4 = test_copy_session、test_file_attach、test_math_extract.js、test_render_window.js(纯 stdlib/node)。 +- SKIP 21 全部带明确理由:无 openai(5,禁止联网安装)、无 PyQt6(14,offscreen 组整体)、无 PyMuPDF(1)、平台不适用 win32-only(1,test_wv2_guard:msvcrt 单实例互斥是 WebView2 守卫的 Windows 专属机制,Linux 无 WebView2 链路)、无 PyQt6 的 GUI 逻辑(1,test_global_hotkey_platforms 含 QShortcut 构造)。 +- 证明:聚合器在 Linux/3.12 上可运行、隔离约定在 POSIX(TMPDIR)成立、诚实 SKIP 不伪装通过、JS 条目跨平台运行。 +- 注:WSL 无 .venv 且离线,依赖型条目按硬约束 SKIP;在按 VERIFICATION 矩阵备齐依赖的 Linux 真机上同命令即可执行全部条目(offscreen 组仍需真实 Linux 桌面环境完成 P1-03/P1-04 的平台验证,状态不变)。 + +## 故意失败夹具演示(硬约束:崩溃/超时不阻断汇总、退出码反映失败) + +- 临时夹具(用后即删,未提交): + - 崩溃夹具(`raise RuntimeError`)→ 聚合器记 **FAIL** 并继续跑后续健康测试。 + - 超时夹具(`time.sleep(60)` + budget 5s)→ 聚合器记 **TIMEOUT** 并继续。 +- 演示运行:`总计 3: PASS 1 FAIL 1 TIMEOUT 1`,退出码 **1**,完整汇总 + 失败命令清单正常打印。 +- 演示日志:`D:/tmp/runall_fixture_demo.log`;夹具文件已删除,仓库无残留(已 rg 复核)。 + +## 等价性(硬约束:单文件命令仍可直接运行,输出与聚合子进程一致) + +- `test_copy_session.py`: + - 独立运行:`===== 54/54 PASS ===== / ALL PASS` + - 聚合子进程(保留子日志 tail):`===== 54/54 PASS ===== / ALL PASS` —— 逐字一致。 +- 其余条目同理(聚合即 `subprocess.run([sys.executable, ...原命令...])`,命令形态未变)。 + +## 聚合入口暴露并修复的两个真实缺陷 + +1. **`tests/test_compaction_persist.py` T9 陈旧断言**(跨平台共同项,Windows 上 FAIL): + - 断言用 `assertIn('msg["role"] not in', src)` 硬编码变量名,而 `ui/views/main_window.py` 现行代码用 `m["role"] not in`(P1-01/P2-01 期间变量名演化,测试未跟进)。 + - 修复:改为变量名无关的语义断言(同时接受 `msg["role"] not in` / `m["role"] not in` 两种等价写法,检查"摘要标记行被过滤"这一语义本身)。 + - 验证:单跑 41/41 PASS;聚合 logic 组 12/12 PASS。 +2. **`core/agent/compaction.py` dataclass 不可哈希默认值(Python 3.11+ 崩溃,真跨平台 bug)**: + - `CompactionPreparation.settings: CompactionSettings = DEFAULT_COMPACTION_SETTINGS`:默认值是 eq-dataclass 实例(`__hash__=None`)。Python ≤3.10 的 dataclass 只拒 list/dict/set 默认值 → 合法;Python 3.11+ 追加"不可哈希默认值"检查 → **import 即 ValueError**。 + - 影响面:Ubuntu 24.04 自带 CPython 3.12(VERIFICATION 矩阵明确支持的 Linux 目标)—— 任何 import `core.agent` 的模块/测试在 3.11+ 全灭。由聚合入口在 WSL/3.12 上首次系统性暴露。 + - 修复(1 行,语义完全等价):`settings: CompactionSettings = field(default_factory=lambda: DEFAULT_COMPACTION_SETTINGS)` —— 仍返回同一共享默认实例,3.10 行为不变。 + - 验证:3.10 本地 `test_compaction_persist` 41/41 + `run_tests` 41/41 无回归;WSL/3.12 上 `core.agent` 链 import 通过(3 个 openai 依赖条目越过 compaction 后才在 openai 处 SKIP,证明 3.12 兼容)。 + +## 注册表元数据修正(诚实 SKIP 的前提) + +- `test_tool_params.py` / `test_bash_stream.py` / `test_cross_platform_shell.py`:补 `openai` 依赖(均 import `core.agent.tools` → 顶层 `from openai import OpenAI`)。 +- `test_global_hotkey_platforms.py`:补 `pyqt6` 依赖(含 QShortcut/overlay 构造)。 +- `test_wv2_guard.py`:标 `platform:win32`(T4 msvcrt 跨进程互斥为 Windows 专属;非 win32 上 `acquire_instance_lock()` 按设计返回 None)。 + +## 边界遵守 + +- 未动任何独立测试命令与既有入口(`run_tests.py` 原样保留)。 +- 未要求 pytest/npm/网络;聚合器零第三方依赖。 +- 默认聚合未启动任何真实 API / 人工诊断 / 真实桌面 / 凭据脚本(见排除清单)。 +- 未读取真实配置(`data/config.json` 零访问);未删除用户运行数据;夹具用后即删。 +- 产品代码改动仅 2 处且均为聚合暴露的缺陷修复:`tests/test_compaction_persist.py`(测试断言更新)+ `core/agent/compaction.py`(1 行 3.11+ 兼容)。 + +## 复现命令 + +```text +python tests/run_all.py --list +python tests/run_all.py --group logic +python tests/run_all.py --group offscreen +python tests/run_all.py --group all +python tests/run_all.py --only test_copy_session --keep-logs +``` diff --git a/docs/agent-handoff/evidence/p2-02-codebox-corner-4x.png b/docs/agent-handoff/evidence/p2-02-codebox-corner-4x.png new file mode 100644 index 0000000000000000000000000000000000000000..3bf9ce4446713b6e42c5f005068bd2e8b7e0f6c4 GIT binary patch literal 1875 zcmd5-drXs86t99#MZsw{Up(eCYX%!MlQ%7$f>S329U{+Ch|0H3u~L+-)P5a01cYfE zS$qIRjfzkxj|vP*DTshlpN`#b06 z90@(JVcqt1Fc@sZp@YFc%*CmBTeEu32S$wlF&EaJI~bh`gV{fuw-tZ7+3$eCK3a7s zc<-@a3YB6^_HMwb^KBX9n(8yx!`aWCk3LCp@IP5|^*7tkPs00hxAX-gzr6a-n4KT# zTzBi`)-lYD?BTB;!#$e!XWz|cB%KV+C|rL{v07NQH|h7%vbf?B|Cd#_Z%S}Hd8&~( zfW+SPXlG)vL*6Zz({gDfI=V+$&Z;Z~I?4VmiJK6%n-R7J9!A@Q+wR!L^MD^uuEB@d z>&F5QBO~px1E}~Og+k%MhM1@Cr107CJ<48DL=U57W5@!^vD&pQDT5M8V0aHjYNZgK zkK2IH`Uzu=Dixu*5A_xuZ&5qRE^-CdhlSXTowt5nfI7qPsG*rkn0~ca*A(8Lj-MtK zH{57fH7T6}WKF5Em|A$+sw)XA7ocq^^fZcxE()A|yV~E?Kq27W%1#qWjat6CSpR=s zU=pDmB6tGQ-+JjW-mo95npADbO)*8G?4=&^G;iZ^KcaGS7o22f?BK-rn0qY`{6Rig z=7=*5;jPCit-~eskpe9X(tBTza6x;b55`!NPZ)CuV1*O)s0$i=7(-PM(G`3&hlIZ{ zi?E!j)Zo09(IjZ=zJoiSiZ=b_$ciT3E7XZMhZfC3CFG~kUc_XhEM0FAPE4KE>Iot$ zZ(E(^4NaSqZWhWXpseD^&iaw?enUP=!{_q_U-fl#bYxi2HAQX%Wg6lVO`1pBI8p3x zJf)9E>qrkAOUR9-jAUw5kJ;BGG_)I7R=T#=TpEt67)>$(Y|CVU%vZdJF=)sotGXl0 zpw7e^Fjkp@6Q$SPi7awk(8atod@#oIP}fYnL;=wNG=7?025}+WY#SXiI0Qzx9F@8u z#rc&6gIyAaZRr)&%Z>27W!WQBKSJLRnMxduwY8NP%jlbUJ9iOxFqY$KHeW!`goW4( z84`Sl8Y7`T5C)kHaupSTbmk~!X+fR&gjbjZO;lO3xTHp48J%Y&Pnba0mQPiyXeOFw zQr|s6=4Fy&vmQ12@C}f`>1J(kQX++KuC+fuON;(ARbQW18UZAcomPl5 zp#eVVc^f|_-TkUzWe{S;JgP|bEMw7hW;+?{Lq`qIG_U}5^AQgE8qihE0@;Ji!Y7cF zQyonS^A7>3>?zT>S?vWjg4U;4$e5_0j@=_h;&pB9KdUC>p&sQ_FqKd_GfRJ==kUuB zkqK+QSU}oc;Pt-kyv)^Sw=YWkL-3kY#V|)EH(#&aWoe>1$=)of_oef#{LzLNrm>Hy z67_XL%F;cO9A4(Ux*#5riFD?+{Gd>msa*^eM9S*^Wb3813#jD=u84Z1R4Vt(@5#>( zauxtN6J*IajYy%GAJ&6uxoM2J9bDm3hR`NxBRL^z=Z8Sa`<-Ed$Pz0jFpq08#f~zy zh=J6lE`7g}i&-?g4L;RHH*xxN&{IL2>w#tOu-=DxkFINBwmNS;p1C;v=H_EpqSe}T zAbG1XmfPS2Xj`gWZP&NX;l9?UYQh}{LYHO8^CNp?7dw;HhHw2?|8)UEYSXp}qidoB zSZe(fCe>nvdimUSyq^g~VL)C$UUvmFGy>2^;ADx8EuYblDYSRJua0Z*b~o-$I2A%0 pLQ<|o1l08-+4cJG-Zh&K42jGVxRTS{=YC1pq5Yx3jC~20{sm}riQfPK literal 0 HcmV?d00001 diff --git a/docs/agent-handoff/evidence/p2-02-outbox-render.png b/docs/agent-handoff/evidence/p2-02-outbox-render.png new file mode 100644 index 0000000000000000000000000000000000000000..5b58460ebb7d1e4b98a57f47f7be159c664065d7 GIT binary patch literal 2420 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 ZG2LzhzxMVJSpT1TproN#pz!GB{{Uq*Mgjl; literal 0 HcmV?d00001 diff --git a/docs/agent-handoff/evidence/win_real_wv2_mainwindow.png b/docs/agent-handoff/evidence/win_real_wv2_mainwindow.png new file mode 100644 index 0000000000000000000000000000000000000000..04a77b6a99799009a49c59d16307a6786a1c70f6 GIT binary patch literal 29657 zcmb4qWn9zm|E`2c$QUg!VuYlW#70PrR60Z&Bn9d2W^@bEjF9f`kVd+@TTqbBGk?GD z_x#U!a2}jJ+vm02yYF{i*Y&;^s-z%|i%pLGVzpF9zZ1>1pBDyyyG3Wd}thUJu1|FOaVfe`;3ZoM=b_O1;XP7cX0y(|t>= zqMNCYXNu%!-{ax#^RD^bDB~PwqJEt&;s5*jimx&Fs7`x9&tV0^{z`bCt(uqbZD-$G zQHg;LplfY6%CEP9m*wqlAD(e?a8}$r%{n_tmj(MLvkHCO+bTqu! z#Hgj4A)H^f8sKa^&c+8geVV?J?R*~%5}u)l*-%JPB|<&4n~+r8%&4rOEATa1jAWMhTu zFT&B4F@W4IzCRXRj=!KifBw7#g^d7UXjWiysPGkbA0ax?=5_nVC9BC6^O}K)!rm)G zUMCj^t499T^#?RpARPK>j4ZOj902C^d%~B_xZ+HZifrN8u_PnBxd16*iAVI_{Y@Hu?T1QIDs zWjmS0?-C>a&#fxVv4+6zRG;#N{yK~S?o=4LDr2gm-yge3qIQ=8$wZF63 zOW}o`Jf3pQ^m_y+h8N+$BSOUaK0t!DV=P*hkn#C4y2K*9h$4{Zg*o3VEmiBO1)f|d zG&IY*8FfQEd=Cjw-m)a9V7YM{t)K9KNdR`FwK7}&t^^-0;Bvh5K1Z`rga#3*b7HfY zujhHfeD??+)z57XvADb&i<+&gj4*^b#0rVzE>=qD6j!k0+Xt&DMYO>miJ70GB zyy230PkEnyO`>q8kg&a@IwpzluYa%#Vg5T)uvOIfH=5gzMVAYLKNEZ+;$v^8N+_M=8CB#!4O^&L|kW=(IT_+6M zH+OI!!HUcOErB40=JHy(js?Qptz&4hrG+8{txK-!-z3^pKpub4-mUxjY0goVh#Z!f zrEAGr$6z9EIRmvmVhVfC>Qf)S_|uMYFjr|rAzXU*dv83a9Blb|yOw$HJnIYVvs&oQ z`r8`lc5P0;dv8dAd@5eF_Z$R5#1-h+WrvN*?3HCY_uP)+-NsR zpN|0`i3)z|7s1)ls$TkCSeek;_;9liVv(D@{Utx{uZH?4hmd0U?!K{cOI z%_v*?gtj>qDIWe9XyR!x1W;6VVutPzi7LQ0bvhbSesq7U-Q!{sz)=u3;d6aHdaeBZ!q7 z5cdIziih|;RT$Y=tBo)W6Ec-RGc|&J75Wv{aNbV(Xq5r7`1wZ3;l#Vk!QA8EthX0z zC82-}^^CtAMr7L3K$SKo4Ax>ec}8Q5bs-Uwxigp%;+Z8VrAFxBTjEd63t!1dPnhxP z$Dxx(w9gz}_xc0OVd^d71IDH(fL`8Meec5dt)MayOay z`3@SdGvG?rLjTD1j-o@Gi7(^RR+80dLx1r6bqLZm99;7Lbx{ws1Qjz~jKd zeCr$U=vE8oo6AO7w`0~Mj>m`Fo^VpuGGtr2J~fGN0HI)AzhlbI9x2*=2#)xs)B$`J zVjt2%?@f#!lzfhfo&hWu1zj@fpg9!iWj?d`d9{9gFY!#I1N;=;+t%pbHKQK7m?XdW z<-ObfL=X}aw6?t!(NUE}@7W}WLkYM(0!$ql?%1xj*tqvmj> zk6ZV!+}%*BMOR37I)glccp~;7F9UT!294S($RYuSe=P<8-*^U?ebW~vWAyRz?%Z2% z^f}T};J{;I#*pd>kR5Jtb}(7Q*d9%{Nr_m1c*tuw;AnX@22>^dB0e>hVjfnQmZ@bW z1hi-YZJ>c2Aj_5sB8#E!FzV`AqduKP>}&Sn^Y9>a?9F_+3lDp zEKGpudtc}Au|ES@B!V{ZsL(8}Gh*Htk>dqsk03CptPQX*d50U8p4r%}p3Hc#_?X)~ zzT)K)Vo@N8?GZ#lL1OI?lLmEC=*@6M56Mj`zwYI`hY z>a3`UWwmEjcf`#Qs-~inEvSHqwGZLa5q$oZ; z<_TA5%>#d#23=TIq!}S8!Yb}-eC7UN-cSP94v>5FGf@&u!yh7{|6W6P_2J-s&3Z=7 z@03TD^|6$o{Ui=`0-wFXFRW1)_gL`1p@Fo{c7CQe3kQeD7Icgr+D#$y1i_?T9onUQ zA+hzG7md1r9l=NEQ(y-by#Zxg15dTQA5IuwG4AnYt8Lv2>?B7zgE8@$CX(!x$~H57 z9%R!H6#UjSJ|QJSHGhU%Y@*Y%!rypSFeskEiCFO{q4v3|L(hC+wViVDen6iIi6;ot z9`J9!gOybWGhHzq&Pm>qLTGm!T>;Tk*lB!xOj3Xf(!jXkwDCWhKt)&E^&6w@Du!5v zQba@GB@uf2y`bx2Bq&mQ1Z^nNSYE-F8Yj+Zy3>WvBdK`<$)oYQ{a`z6g@k@=Vbm}H z*)>cGfQ!2k#A{`0b_a8jLe3|S7&6VG$yTGHo>xV(kt~0zQXr4}pIMx^W^vA#! zS3J!&7c_if2FhG(KujXZpgTnP3eKLBLhs8&6yp}qLXYzta772n0=8F$QX80WfouE^V+W~Mq zY7Bs%WREY5Mu{jdYDjZN%?>(n{9agO>OD>Po3(4ADbT^xsbBr7cd7>=#tiy%2s)~y z@^Cn84e#HtO`GqiJ^}b?_Wfs(ryx|c;#w5AXckk^fPDx%HUNYg-t^!1b`DHNoerwc zqMiZ#cq{&WwimtXwVHVafS}KGV1Q8lvI{st*faK_Sr|LO?rkJ7vPD`mTO4$A-IwV< z$Ursc>6jQu1N(+#Tw}w4{o&?tROHd~c&YKV*@)-k{bd4-&I9k{=k3qo;nYTcLj(Da z{~Tu8UnS6XgJBkj?S((N&)#C(L>!uQKc7E|?l72X)c7q4Xu=;G5t0Zp{-%rB?24YO z?RN$HWi_aN4PyZ)`|KS)f_!#s>oMUNHOGIZHT3;N&`DmQ0sKUj6V5=a>5DvZ4LUI= zevS?kM@a8F3Q*T{j%1M!kh>?qp8h)K3LDzDH{ZnJ@VPvaB~Mz^_`QFJRgH{^aqj=b zL@f=NJLj6n{~IW~I)~g&#)&)8xxbJ$G`4#?Oyn`FZW+G!o+f>_JWU4jc$HUQJA>iqFP4hd6Fem`ryUvKXPc>;$KQ(ZNj*F~}r;xZZWk z$*%?pG(7rxTzvcu@5nak00WuAu+Q;gCCSG0XT-#4z$iRlX2k*T^J0K$%8$TBJPB!7 zR+svA%s}0I!sKcKKESKRsy+^OgatRoMB=ggs;lr(fTPWMg-|l89#M>nm`JYy zKP3D&#tu&G7t~b;x^RomvKkTsSe`b%5wnZM8w`E`##`%@&j>GXi?EDlLi@gb z_HjBqw^T=lJmZ5`+7We8Q4#ItHVZ3j`?VtTdd%#hu+M|1o7?sNWPbbOgGU!)q?8IY z@6q2!?R7V!ZDVKC)sf=+--wJFt)#69)SY5)`o^z-@QT=2M>v1nje+PX?Bjs}P8QcR zSO41u(jMltasQJxk?!7&Ob&a| z?~?T0OulD{1;oXh-EZ7~Syp$t?{!(t9Gt(~F^U#mY4y1DDbaPg9#lUr1;60<@3J|& zV-*0j>>kA3oiG;{m)A!le(R;J+Z>hD^(94-a@|XnUvIpqyw12rA4Z*U+~&tOGvMw^ zoBiu6?++GMBsaE&d7^H=@L2qJHA?7N+~Lz{$WwL@`HW$f#nMH(Ez@D9L2K z#@%ehshZvevZ;BGW(cp2cuDi4*%mgr9Ez{6vrPEVWN|?%k``=dcf*kFI z+e$7^;aWo=_q7f`v>lLu*SD7JjmzTMvi6hSc670d`OkFHk>YEOSGUy6e|GW6c?ysv z;9U|sEJ56unxQqn1IZxy5QzWNgLiHtH}@ zF-o`5F6mba?fvTQ*`#E%-qj9#gS%vhdYjd@J81@O)VJXHq5JFIOpp4V z>e0%BDAl>MjiK%B?bVDc(}9Gifv}#Q9%P-C0&)99)gB1g|3PinE%?gAT*{G8 zKijXa6t((1)<+-TALP;Z=*@2n{COalR>q6aQH*YKx5sHdJn^#t;98YK>Q)_Yys40G zK%I{7x`Hvqmds`?Q%u>0{Kyrh;-n2l){hBhPsbsC)Hn+ZYF!_hBp7x3<9Ox9BhY8> zkD^fRDO?5<02Tcnj0*!DjOR!nbOrU3iSN(PQ+Qk+M-s%oQ*+}u?klKn0TL}&9AhY$23@?xVim?o>uh@!3SeCYFzhnwzf4@>@@*y^QL>NEM-e) z_cvZkFJfV{Q88zFst)X{4Vq22HY}&IzWYKLlKMn))JiIvJ){UO!?7-8xazIn!;h#cvc_ZgNV6zP`RXQK>q- z8*LYQ^b`S}Ir}N#x{Nm?){%8VS(%uxF*~6dAO;Q&2 zDi02Iz3ZLDUu_RJ-P4w{L!%D~JNC(TAyKMKo{2%jL1-yPfM;atc_@4N#W+2qiR zq^CLHF%-Ee-iv9lMg8S4zL~~{vW>C4)yFH>MQaHfXUdiw{kz*qyf^Rd_V$u`YJALZ z6E7`#5Z230=f}&pM@ueq$sXR2`^``rh2y2XL$qmSbaom_jK^zDoKkqGEeep^?kT;enq>#+P@#DhxcP4T>qgG z{&QGBcFbOPT10}|#puL&`1d^Xe>RO51d$Lo(_q*A#_BdH$0PZL$~f@@F8LKK1jh5a z89la4{qB^47MML=9U6|kvJ9JyeeHPA?c(!rZMGk4WSB|q?(yAp+7;S`fNp+EjZO*= z4?kRLbST$rm5Ks2e(V`E>T8n_CiM+tlq7acO>wtosQ2lJJ1T?tG1De=n|Zbdzw!ppp45zsm?)hmmv1zcn{1RS{N zQL97PKg>!e%)vjX|GCj=v}fqrg?EkQR~QdWAaSx|;`y9W^xyglOuIQh#)8Rcs3wwv~M1drww-7+`Qf-rxGtcXQ6M+g5!(>6Iz`=-Kpr-Bn8s z*%~uEPY1HU>+^|vEUQIIt3UG-B~jn}x?5uiacmX%eKhdPY&az-Ed$xiUNVl}RNjH+ z&%NgHWMx%>$?oR|yjBZmKwvA=$HH+N`)ShWo0Ny7Sy2v{OmaYX$T!)PC#$-LVzd@4i*Hxx6@U_1Bhu1MuB%gUi`AaL-k{$Ujx2) zwyAq0MiUc+4no~DXek!PM6Ze=M}fP~M#1n10ZzDfdt%A3OA#zkRq#pKh18Ap?S*c} zeFmQM=he3R0CFxYD+G~oJ11GpI%&pHGqzBscMHz8ShA0B72J5G_Ro{$@YgTEcOhJFS1xM! zE8O%zuUp-e+^E-FF72?O7lKf_L1#EM2-A`ApQEnGAimw@XX`EsHz3~Ejfd4y#rFOY zUJ-pFa7tl4-`++wbYbd$^uD)Vu zV9dpEo{c%J|CS;~AF>*HB00rhi?qgm{dRXVYwXd?21SeA!UIoCsXaEOL)bMGjrc8o zk*a7?L5pSRB%vWNYR`)M07WMb|1GhEn=^^puSx+r-~;i>xKUr`f_0$4K|t1?nrxnH zG$PGIh@r?R+2tAP3BE06M8IFueoQ%i{jAO>B=xg{k4!`hWtpkCB{8EA`V+%@@K)AX z7APQoZ8mewVBy)v5)P&tt~LXv1nF#_uX5iWl+HsHnT;-}^441ou4Ba$fapG$#ti>> z+J?T7xm`0fWk+8UAVH8sC*Yi`bLSHYB1nYM@0N!hgOz97=iBm?qDG#|mdEW*3NMPx z(OsYg8FwLC5ll;pi}k<6AKxmdt!ePK$FZUS{7UHliJM6Jpk?QySGnwfbC#_+4W^hp z+Tne=k0qA7Wv4^DVlUbA!+x#X8}EMW^$3J0vLHUgecrz$>)uWPWzSyVXtUXuD$Js{ zzo{XG1Hh^xcin73zb(81wJK3!{hwj={VAfur7F4X<~<(4od1OPNVc42a;Q%%Yf1a{?D%j z?*gkT7?I3j&@|ZFef4iqJ+quLfDEGnBtwpE{n! z9&|*KkvZOmmwSg^;Rc90;oCA{gWS*Ergqv5x)E_{WOflsgKuMDvZ|XK-`c*L(nPdf;e$)>=Ew&|UN-pK zckk#1T~5A!>+un!GoqVsd8b;`AVG5G>p_i43T?xI;Ygtklq55R0xtF!ZagrZ&+nbHoy zf`y2W%w!hp8e(e<=O>vu=w>_B1z7Oh;B~-hG=3^ga=v-BzokF9pr zytHlf`Vur!T);h506Dr{rlV#$ z#0}M5)I;j9n~M|SXy8YT%6!+uh1pB}2Dghp5xf+I`H~8LVGHl=&{SI_H5W!(X%+TC$ATHXJCvqmMd%N{Ctf}Hb2^|!t7p9RnCs|hA z36WT$k>aO-uuCg#UEU5?n{cAAD}w>*A3qn|N2h9dwH3Q^Y>X|!w;(HpcM+vM4K=)$ z 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�IQuvtHexXQc@E{qY!^E^vZY4#RCJG{HpbDH-1nK)DP3g*r!UDaQPKP zmvH3MxNE(#C7<4M z%2LMIfhVn;F0Mi_D_*ON_bf<;2CrxyH4|$f8eC?U1bSBNuF?09Wg?$R>;3t&(+Aag zt)3SrO6ks@wNcDEM68cmB_-i?;zZjk0s=_T%{RU#$kYYdP#I5w{XR?c;>+N)a$cMz z3kMFI?yJ`Nx7o!XU_4dl+1#JHU^z=f`tJSPSN1m*K;irZ)zEpDc?Sc^SLNz)n9V6E z;NFcrtq}Pi6Mc=smq$;)XL0oxbI_3`bXT8ti(v$PRR#S%E?A6Z4fhS@&3ywEZSZ+} zY75dp*6+xT;^SHbwdqE5TKBux+b)3-$u@S+6ex`x_&FaWx}2=c+Wfo_*52$3m7Uc( zn`?E{&EFW#U;LTZ>2-w;79;#MVuC}JQyKwXLi^jOD4M`f7c#};=-%WqmB_3KUo{J9 zP?_-gP_3}h3bpi*eJ`vyOP-;xod0h9dG;)L4AnGX@j*Qr~iX~zp`uTb@UNR?{#3eu*yN)E^sVx&|81* zV3JS$fP`G$<0Pr~c(ZSK8uTo?h~r(TB#tVty-=}k_%h}P7=G5fxkc>XHV@|`*j#mR zq8T%2tGUwdiFcFg4#jRFyI8kRaE`UWA*pwb2MM~K_8r$<{phXNcRA^=_Bsd3tbvgl zRR`_iv0fKH=YDViUFRML%3E3eIEAQ10u$V85LBFd1JZ@s_jkd-+4^%Xn9kw@kQ>id zano(C7Y2dRUyi^KdBEqls?B%%JqB3pfCZSU9|&?53w&P;iR&Q?m>7Bqt{QSh=5LOp zRIQQLr9WRhO1<&_@O>04pe_q`Bd8A|KKswdv*{scTt2`G-1rXe526M?!1U`8XXIC^V~pASz@x0~SW2NbSl)4JIKETZC64_@KWr)4+S0$1`P&{hABXHo zk_W>teuURs%|M`G*iDNfRRd0Q645SZ^`L~{CGsen@>!bJtE15?uGXmZ87!VeCwjsl z#c%~_WXBTR-C2iEjrrlUixl5t0fl)9kqoY`|B)M*Wu%>UZfvtX5EqyUlEN#~V9R_vyJ0Xf1;iZ3Q9IWjUwhc*0g zI@+aeONl#)>zu$6c{WomNCqqlVJ6@RB}1sKUZF!y24|}rHz#dxH?P%i1wuUh1e@(s zV$T#3nYP5HJi+iC9OIQv;BBlbOVqVEj*&811KXaJt>E>_3q3~mtu-ZHQZf(Wu_Jv0 zXSC~K1*X|nwqrOX&<)`dl#AQ6Um8P`+0RLIx-WT-YoM>tsd_!3iV1T0MvPG;EY==l zH87`{8uO7QIiwNUQ*dJU^2+kEiK(eESz`3`8o4O9`=o(e2%e4BGmA4H(++U%f7)MZ=&xsceq ztWwuJ&v=l>2Kmef*h!Z3b-<}19UUFRti>#9VN37?7mRwj>qjX923mcWPeA(=PBAFG z-(vasxfAIW8{yzST!NMN3uj>x70@tsww|qZ*{H_rvz8VQI5B)>d0ul8WoV63|CATU zSgv1X!l#z3ovX&U?1I_Z*@?iYC%qo~X8T)hqP0FbTa~W2QQb_GFIcJfLh++CKTb}J zA-v%BS+JvuvgeQ2SJ}Z6Gc#rTO0{}C`%1l)uaEieZVJSZ2FfF|wGxmGb3YCa5R+d1 zbN8-iy(fpvq?~g?uRfmO>gz~PPL9;Ldm|8H8TxE?y$vi)r{9Rh#dzN zbR8sbCU0?Im(Gh(?r6qD-^?j08Oc=1JNT^k=~!@We*4MdboP_SvIPV|Ww5ovCOPUE zLnN!$q2bd=%kzRK0ylq!L$5N(zHnUgW$Q;vdh;W_PGYyXRT1MGa3{{OXV8QoOXNcp z{~7xo89&$FTB=_bD1!}KI4qu}s2^F?yRK*P3*JI~bF;c4d`l?mKWk`b%(ABzzf&wm9M3WgQv&P4ts z9p-F#4ZEaqE1mrickk;D{~dx`(_wBxb>5)4!gs)O%@@GFh3V<{>XHNxZ&kCHi$btzhYRGcsy=AoHYPt3* zr=7Esy(#P7bRCz%5>AfMpTMth06-X#(%Gb9LI&rj-j6`6Mw#k{xnR7zw!r33cPJ0b zQ6;~la!(DrO>b~_+R^u~7zs|>E=AP)O2HfOVTs1}FH$+)WQ1!+D%Nov3Z)Gvva zHnoyHVvjs)BjID!;*Mld98Vz$qm?p+U7ZJ@&4`PP)W-@QZ2ASUBU!*5nK!r!K;ptL z6=h}IaZDB9MClLKwJs^3xd+z~es8Q~tg`j!LwstvO?$M~m{O%()METy8(Hr~5zjD0 zn?U8tR}8MSgd_KzM$dBsnXITul)KQZ-PyYqP$;tH?`=UO8t_V+uKN&z)(~FnoK0EsE^4C%qjW>=Q7*QKaWhlfV}>@bw-*k@XDLHUn_tgi_8;9# zwRAOg!j3|nw{e=q`sHXj9%7G!LPJCS#UOltZ7yNW`vgY^5ozQT_GI&yxVgWaWsZMn zZL9Cjn$lfxL&kL?_9xHyPh0+fo2D#-s}82XRhx)aEhdS;euQpZ25gM=8h5>cU7-5@nEfM0Iitmk7Oi1H zD1LH`U)w2=j{bC%{itL~2C*Oisk_;{ds%2Kn_OQ==x^daS9d+nv2{jWnFwRbzZ7MNLabhulG5!)27Y_B!YFfiVb_N;Q~NiqS}` zh{^eAY^%?c=5fy4ZahX{pm&Z{_(7w67W+ld_mLKp;0F`?hQ;*drbu+Q>C2Z3 zx5{A~8yk+gJ&rj_^kWXtHQWt&4(qST;wYYdvS>)OyC{PGu?6tpprNd05SJeF$4{{% z-Mj7rJVd=!U`DorP(-Z_KS8a7jk$F{f}Wiusfx#%pVNIKA3={t8u~T^fiZlIcLLMd z6z01KdPn=F=!9EDBcMupx`h|B(L>_~ZH_tWpDND@I$kd&nexjc)nB~W67tvKCaUDe zBO|T6?3mnC{hIM>=PH5u2ZpRDgkv*t6K*pm*UHs37f)_QfQy3RpYiMtqL*G8)G85K-&ouKA?uH|mT8ON)R-7Fa0vvu-(cj0aDr z!1o3>ftke-%g1AQD2?ZIdX2I=rZo(uwCOOK@^OL_Nxg)P_z*u|+Yn;<>#zdmSN#|> ziNZqs1!6VMy{cm?`>+BTWXs*g?G@xIE?MNYPvQ@wPAgBxFD&5#LR{Po`$}FbEC7N* z06Q;=JLH`16RhG|Tm#U*yZV1P5=ea&8lqsw)s