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.
This commit is contained in:
@@ -0,0 +1,301 @@
|
||||
# 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`**。
|
||||
Reference in New Issue
Block a user