Files
Haocode/readme.md
T
sorrow404null a7412824e0 chore: import original project baseline
Import the pre-repair source tree as the history baseline.
Runtime data (data/), virtualenvs, bytecode caches and logs are
gitignored so local secrets and user state stay out of the repo.
2026-09-17 16:40:01 +08:00

302 lines
20 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 │
└───────────────┬──────────────────────────────────────────────────────────┘
│ QWebChannelchat_bridge.pyPython → 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.pySQLite:会话树 / 消息 / 压缩标记 / 附件) │
└──────────────────────────────────────────────────────────────────────────┘
```
**一句话数据流**
`输入框 → 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 SDKnet462 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`**。