# haocode > Windows 桌面 AI Agent 客户端 —— **PyQt6 外壳 + 本地 Web 渲染(WebView2 优先 / QtWebEngine 兜底) > + 与 [pi](https://github.com/badlogic/pi) **1:1 对齐的 agent 核心**(工具调用 / 自动重试 / 上下文压缩)。** 能读写文件、执行 bash、带图片与 PDF 附件聊天、把「思考 / 工具调用 / 工具结果」按时间线持久化, 并在此基础上支持会话树分支、会话复制、实时上下文计量与右侧任务面板。 --- ## 一、快速开始 ### 1. 环境要求 | 项 | 要求 | |---|---| | 操作系统 | Windows 10 / 11 x64 | | Python | **3.10**(开发环境:miniconda env `haocode`) | | WebView2 Runtime | Win11 自带;Win10 需装 [Evergreen Runtime](https://developer.microsoft.com/microsoft-edge/webview2/)(缺失会自动回落 QtWebEngine,仍可用) | | Node.js | 可选,只有跑 `tests/test_math_extract.js` 需要 | ### 2. 安装依赖 ```bash pip install -r requirements.txt ``` > **不需要** pip 安装的运行时依赖(已随仓库提供,勿删): > `vendor/webview2/`(WebView2 .NET SDK + loader)、根目录 `WebView2Loader.dll`。 ### 3. 配置 API Key 编辑 `data/config.json`: ```jsonc { "providers": { "deepseek": { "api_key": "sk-填你自己的", // ← 交接版已清空,必须填 "base_url": "https://api.deepseek.com", "models": ["deepseek-v4-flash"], "model_contexts": { "deepseek-v4-flash": 1000000 } // 决定上下文用量条与压缩阈值 } }, "default_provider": "deepseek", "default_model": "deepseek-v4-flash", "debug_window_autostart": true, // 启动时是否自动开「调试窗口」 "mode_switch": true, // 是否允许会话中途切换 chat ↔ worker "webview_backend": "auto" // auto | webview2 | qtwebengine } ``` ### 4. 运行 ```bash python main.py ``` 首次运行会自动在 `data/` 下创建**空的** `chat_history.db`(含一条初始对话)。 左侧栏「+ 新建对话」开始聊天;输入框上方可贴图/拖文件/截屏(Ctrl+Shift+S)。 ### 5. 打包 exe(可选) ```bash python -m PyInstaller haocode.spec --distpath "输出目录" --noconfirm ``` - 产物为 **onedir**(一个文件夹,里面 `haocode.exe` 双击即用) - **不含数据库**:首次运行在 exe 同级 `data/` 自建空库 - 冻结版没有控制台,启动日志写 **exe 同级 `haocode.log`**(`pyi_rth_trace.py` 钩子) - ⚠️ 打包前务必确认:`main.py` 顶部的 stdout/stderr UTF-8 保护存在(否则 `print` 里的 emoji 会在 GBK 环境直接崩,表现为"双击没反应") --- ## 二、架构总览 ``` ┌──────────────────────────────────────────────────────────────────────────┐ │ ① 表现层 ui/views/*.py (PyQt6) │ │ main_window.py 主窗口 / bash_panel.py 右侧任务面板 / debug_window.py │ └───────────────┬──────────────────────────────────────────────────────────┘ │ QWebChannel(chat_bridge.py:Python → JS 单向调用) ┌───────────────▼──────────────────────────────────────────────────────────┐ │ ② 渲染层 ui/web/(index.html + app.js + style.css + KaTeX/highlight) │ │ 容器二选一:wv2_view.py(WebView2) / custom_web_page.py(QtWebEngine) │ └───────────────▲──────────────────────────────────────────────────────────┘ │ 事件信号(Qt Signal/Slot,跨线程 queued) ┌───────────────┴──────────────────────────────────────────────────────────┐ │ ③ 引擎层 core/llm_engine.py │ │ AgentWorker(worker 模式) / ChatWorker(chat 模式) / TitleWorker(标题) │ └───────────────┬──────────────────────────────────────────────────────────┘ │ ┌───────────────▼──────────────────────────────────────────────────────────┐ │ ④ Agent 核心 core/agent/(pi 1:1 移植) │ │ loop.py 循环 → stream_fn.py 流式+异常分类 → recovery.py 重试/压缩决策 │ │ tools.py 四个工具(read/bash/write/edit) · compaction.py 上下文压缩 │ │ context.py 上下文组装与计量 · types.py 数据结构 │ └───────────────┬──────────────────────────────────────────────────────────┘ │ ┌───────────────▼──────────────────────────────────────────────────────────┐ │ ⑤ 存储层 core/db_manager.py(SQLite:会话树 / 消息 / 压缩标记 / 附件) │ └──────────────────────────────────────────────────────────────────────────┘ ``` **一句话数据流**: `输入框 → main_window 组装上下文(core/agent/context) → AgentWorker 跑 agent 循环 → LLM 流式返回(stream_fn) → 工具在本地执行(tools) → 事件经 chat_bridge 推到 Web 层渲染 → 结束时整轮(正文/思考/工具时间线/usage)落库(db_manager),并成为会话树的新叶子` --- ## 三、目录树(每行说明这是什么) ``` haocode/ │ ├── main.py 程序入口:渲染开关 → UTF-8 保护 → QApplication → MainWindow ├── SYSTEM_PROMPT.md ★ agent 系统提示词(每次请求注入,不进历史、不占压缩) ├── requirements.txt 依赖清单(只列代码真实用到的包) ├── haocode.spec PyInstaller 打包配置(onedir / 不含 db / 带启动日志钩子) ├── pyi_rth_trace.py 打包运行时钩子:冻结版 stdout+stderr → exe 同级 haocode.log ├── WebView2Loader.dll WebView2 loader(工作目录兜底路径) │ ├── core/ ★ 后端核心(无界面依赖) │ ├── db_manager.py SQLite 存储层:会话/消息链表树/压缩标记/附件/复制会话 │ ├── llm_engine.py Qt 线程桥:AgentWorker / ChatWorker / TitleWorker + 配置与提示词加载 │ ├── webview2.py WebView2 集成:pythonnet 环境、单实例锁、无头守卫、残留进程清理 │ ├── debug_log.py 调试事件总线(供调试窗口「对话消息 / 应用日志」两页) │ └── agent/ ★ pi 1:1 agent 核心(9 个文件,见 Frame.md) │ ├── agent.py Agent 对象:状态、事件订阅、prompt/continue、steering 队列 │ ├── loop.py agent 主循环:流式一轮 → 工具批执行 → 轮末钩子 → 停止判定 │ ├── stream_fn.py OpenAI 流式调用 + 异常分类(classify_error) │ ├── recovery.py 重试/压缩三路决策(对照 pi retry.ts / overflow.ts) │ ├── compaction.py 上下文压缩(摘要切点、保留尾巴、标记生成) │ ├── context.py 上下文组装、token 估算、usage 锚定 │ ├── tools.py 四个工具实现:read / bash / write / edit(+ 参数校验) │ └── types.py ModelConfig / AgentConfig / AgentMessage / AgentTool… 数据结构 │ ├── ui/ ★ 表现层 │ ├── views/ PyQt6 窗口与逻辑 │ │ ├── main_window.py ★主窗口(6000 行,全项目最核心):布局/样式/流式状态/附件/入库 │ │ ├── bash_panel.py 右侧「任务面板」:运行中/已完成两栏、层卡片、拖拽调宽 │ │ ├── chat_bridge.py QWebChannel 桥:把 Python 调用翻译成 JS 函数调用 │ │ ├── wv2_view.py WebView2 容器控件(QWidget + 原生子窗口) │ │ ├── custom_web_page.py QtWebEngine 容器控件(回落路径,含 QWebChannel 注入) │ │ ├── debug_window.py 调试窗口:事件流 + 应用日志 + 命令输入 │ │ └── system_tools/ 系统级能力(见 Frame.md) │ │ ├── file_reader.py 文本/代码文件读取与二进制黑名单 │ │ ├── global_hotkey.py 全局热键注册(Ctrl+Shift+S 截屏) │ │ └── screen_capture.py 区域截屏 │ └── web/ 本地 Web 渲染层(离线,无 CDN) │ ├── index.html 页面骨架 + 三个本地库的引入 │ ├── app.js ★前端全部逻辑:消息渲染/流式增量/时间线/KaTeX/滚动 │ ├── style.css 全部样式(气泡/思考块/工具 chip/紧凑模式) │ ├── marked.min.js Markdown 渲染 │ ├── dompurify.min.js HTML 消毒(配合 marked) │ ├── highlight/ 代码高亮(highlight.js + atom-one-dark) │ └── katex/ 公式渲染(KaTeX + 字体) │ ├── data/ 运行时数据目录 │ ├── config.json 供应商/模型/开关配置(★ 需填 API Key) │ ├── chat_history.db 聊天主库(**不在交接版内**,首次运行自建空库) │ └── (运行后还会出现) debug_session.log · app_instance.lock · attachments/ · wv2_cache/ │ ├── vendor/webview2/ WebView2 .NET SDK:net462 Core.dll + webview2loader_x64.dll ├── svg/ 界面图标 18 个(panel.svg 左栏 / panel_right.svg 右栏 / 模式图标…) ├── tools/builtin_tools/ │ └── pdf_reader.py PDF 解析(extract_pdf_text / extract_pdf_images,被主窗口 import) │ ├── tests/ ★ 测试与调试工具(见第五节) │ └── 历史文档(保留备查,非最新结构说明) ├── ARCHITECTURE.md 早期架构文档(其中部分目录已在交接版移除) ├── readme_our.md 早期设计/规划稿(973 行) └── 黑边两现象分析报告.md Windows 窗口缩放黑边问题的分析记录 ``` --- ## 四、我要改 X,该去哪个文件?(功能 → 文件索引) | 需求 | 文件 | 关键位置 | |---|---|---| | 改主界面布局 / 气泡 / 输入框 / 主题 | `ui/views/main_window.py` | `setup_ui()`、`setup_stylesheet()` | | 改前端渲染(Markdown / 公式 / 流式增量) | `ui/web/app.js` + `style.css` | `marked.parse` 统一拦截点、`updateMessage` | | 右侧任务面板(层卡片/状态) | `ui/views/bash_panel.py` | `BashPanel` / `BashLayer` / `_Section` | | Python 调前端 JS | `ui/views/chat_bridge.py` | 每个方法 = 一个 JS 函数 | | 数据库结构 / 会话树 / 附件 | `core/db_manager.py` | `add_message`、`get_message_chain`、`copy_session` | | **系统提示词** | `SYSTEM_PROMPT.md` | 直接改,每次请求重新读取(无需重启) | | 工具的实现与参数校验 | `core/agent/tools.py` | `tool_read` / `tool_bash` / `tool_write` / `tool_edit` | | 工具调用循环 / 停止条件 | `core/agent/loop.py` | `run_agent_loop`、`execute_tool_calls` | | 重试与压缩策略 | `core/agent/recovery.py` | `_handle_post_agent_run`(三路决策) | | 上下文压缩算法 | `core/agent/compaction.py` | `compact_context`、`should_compact` | | token 估算 / usage 锚定 | `core/agent/context.py` | `estimate_context_tokens`、`calculate_context_tokens` | | 模型请求 / 流式解析 / 错误分类 | `core/agent/stream_fn.py` | `openai_stream`、`classify_error` | | WebView2 行为(锁/无头/清场) | `core/webview2.py` | `get_environment`、`acquire_instance_lock` | | 调试窗口 | `ui/views/debug_window.py` + `core/debug_log.py` | — | | 打包 | `haocode.spec` + `pyi_rth_trace.py` | — | --- ## 五、测试方式 ### 1. 两条铁律(写新测试必须遵守) 1. **不得污染真实数据库**:测试启动时必须把 `core.db_manager._DEFAULT_DB` 指向临时文件 ```python import core.db_manager as _dbm _dbm._DEFAULT_DB = os.path.join(tempfile.gettempdir(), f"t_{os.getpid()}.db") # 必须在 import MainWindow 之前 ``` 2. **不得污染真实配置**(会写 `config.json` 的功能):把 `HAOCODE_CONFIG_FILE` 指向临时文件 ### 2. 跑测试(建议先设两个环境变量) ```bat set PYTHONIOENCODING=utf-8 set QT_QPA_PLATFORM=offscreen :: 只有带 GUI 的 smoke_* 需要 ``` **A. 纯逻辑测试(无 GUI,秒级)** ```bash python tests/run_tests.py # agent 核心(流式/工具/重试/压缩) 41 项 python tests/test_tool_params.py # 四个工具的参数校验与错误串 35 项 python tests/test_compaction_persist.py # 压缩标记持久化与上下文截断 41 项 python tests/test_copy_session.py # 会话复制(含附件深拷贝、分支、标题递增) 54 项 python tests/test_bash_stream.py # bash 实时输出/超时杀进程树/50KB 截断 30 项 python tests/test_error_persist.py # 失败轮次入库与回放取舍(含旧库迁移安全) 39 项 python tests/test_wv2_guard.py # WebView2 双守卫(无头/多实例) 10 项 python tests/test_debug_window.py # 调试窗口 22 项 python tests/test_think_code_neutral.py # 思考块/代码块中性化 10 项 python tests/test_file_attach.py # 附件类型判定 python tests/test_pdf_reader.py # PDF 文本/图片解析 node tests/test_math_extract.js # 公式提取(前端 JS 逻辑) 39 项 ``` **B. GUI 离屏端到端(需 `QT_QPA_PLATFORM=offscreen`)** ```bash python tests/smoke_offscreen.py # 主窗口起得来 + 三个基础链路 8 项 python tests/smoke_mode.py # chat ↔ worker 模式切换 16 项 python tests/smoke_copy_session.py # 复制会话 UI 全链路 python tests/smoke_bash_panel.py # 右侧任务面板全链路(含折叠/拖拽调宽) 116 项 ``` **C. 需要真机/真 API 的(默认不用跑)** `tests/smoke_live_guard.py`、`smoke_midswitch.py`、`smoke_persist.py`、`smoke_probe.py`、 `smoke_repro_real.py`、`smoke_timeline.py`、`smoke_manual.py`、`diag_live_*.py`、`verify_onscreen.py` —— 这些是开发期在真窗口/真模型上跑的工具,交接后按需使用。 ### 3. 基线(本次交接版实测全绿) ``` run_tests 41/41 · tool_params 35/35 · compaction 41/41 · copy_session 54/54 bash_stream 30/30 · error_persist 39/39 · wv2_guard 10/10 · debug_window 22/22 think_code_neutral 10/10 · math_extract 39/39 smoke_offscreen 8/8 · smoke_mode 16/16 · smoke_copy_session 全通过 · smoke_bash_panel 116/116 ``` ### 4. 辅助工具 ```bash python tests/check_db_migration.py <某个 .db 或备份 .zip> # 只读校验:迁移(新增列)后旧库数据是否 100% 原样保留(消息数/父指针/叶子/分支点逐项比对) ``` --- ## 六、已知限制 1. **仅 Windows**:WebView2 与 `bash` 工具都按 Windows 语义实现(cmd.exe + taskkill /T)。 2. **`bash` 工具的真实 shell 是 cmd.exe**(不是 git-bash):`;` 不是命令分隔符、 `$VAR` 不展开、`cd` 不跨命令保持 —— 系统提示词里已写明这些陷阱与正确写法。 3. **HTTP 代理/流式**:模型请求走 openai SDK,单次 180s 超时,失败按 2s/4s/8s 重试 3 次。 4. **打包体积**:默认带 QtWebEngine 兜底,产物约 575 MB;若确定只用 WebView2, 可从 `haocode.spec` 去掉 `PyQt6.QtWebEngine*` 的 `collect_all`(降到约 110 MB)。 5. `data/attachments/` 里的图片/PDF 是**文件系统**资源,删库不会删它们;删会话时才会连带清理。 6. 冻结版与源码版会共用同一个库的**唯一例外**:把 exe 放在源码树的 `dist/` 下时 (`exe/../../data/` 存在即共用 `chat_history.db`);放到桌面等其它位置则用 exe 同级 `data/`。 7. **运行时会往项目根目录写诊断产物**(开发期排障用,可直接删): `compaction_diag.log`(压缩决策日志)、`stream_diag.log`(前端流式体检)、 `diag_shot_*.png`(每轮回复结束时的画面快照)、`data/debug_session.log`(调试窗口日志)。 不想要可自行注释 `ui/views/main_window.py` 里的 `diag_log()` / `diag_shot` 调用点。 --- ## 七、交接版说明(haocode_0 相对原项目做了什么) | 动作 | 内容 | |---|---| | ✅ 保留 | 全部源码、`SYSTEM_PROMPT.md`、`vendor/webview2/`、18 个 svg、`ui/web/`(含 KaTeX/highlight)、正式测试套件、`haocode.spec` + 打包钩子 | | 🗑 删除·空文件 | `agents/`(3 个全空)、`workspace/`(4 个全空)、`tools/{registry,conda_env}.py`、`tools/builtin_tools/{file_ops,web_search}.py`、`core/{async_sync,memory_manager,prompt_templates}.py`、`ui/views/components.py`、`ui/assets/`(空 style.qss)、`tests/{test_ast,test_replace}.py`、根目录 0 字节文件 —— **均已确认零引用**(`__init__.py` 属于包结构标记,全部保留) | | 🗑 删除·测试产物 | 根目录 `diag_shot_*.png`(36)、`*.log`、`_fadechk2.py`、`inspect_*.py`、`read_all_py.py`、`ssh_helper.py`、`tmp_timeout_probe.py`、`flowkit.db`、`stress_report.json` 等;`tests/_tmp*`(81 项,含 `_tmp_resize_vis/` 与全部临时日志) | | 🗑 删除·数据库 | `data/chat_history.db` 及全部 `.bak/.pre_clean`、`data/attachments/`、`data/debug_session.log`、`data/wv2_cache/` → **首次运行自动新建空库(含初始对话)** | | 🗑 删除·其它 | `__pycache__/` 全部、旧封装 `geekagent.spec`、未使用的 `untitled.ui` | | ✏️ 重写 | `requirements.txt`(AST 扫描核对)、`readme.md`(本文件)、`Frame.md`(逐目录细节) | | ⚠️ 注意 | `data/config.json` 的 `api_key` **已清空**(交接安全),请填入自己的 Key;`.txt` 文本示例按你的要求**全部保留**(`_out.txt`、`_t0.txt`) | > 详细到"每个子文件夹/每个文件干什么、关键类与函数叫什么",见 **`Frame.md`**。