20 KiB
haocode
Windows 桌面 AI Agent 客户端 —— **PyQt6 外壳 + 本地 Web 渲染(WebView2 优先 / QtWebEngine 兜底)
- 与 pi 1:1 对齐的 agent 核心(工具调用 / 自动重试 / 上下文压缩)。**
能读写文件、执行 bash、带图片与 PDF 附件聊天、把「思考 / 工具调用 / 工具结果」按时间线持久化, 并在此基础上支持会话树分支、会话复制、实时上下文计量与右侧任务面板。
一、快速开始
1. 环境要求
| 项 | 要求 |
|---|---|
| 操作系统 | Windows 10 / 11 x64 |
| Python | 3.10(开发环境:miniconda env haocode) |
| WebView2 Runtime | Win11 自带;Win10 需装 Evergreen Runtime(缺失会自动回落 QtWebEngine,仍可用) |
| Node.js | 可选,只有跑 tests/test_math_extract.js 需要 |
2. 安装依赖
pip install -r requirements.txt
不需要 pip 安装的运行时依赖(已随仓库提供,勿删):
vendor/webview2/(WebView2 .NET SDK + loader)、根目录WebView2Loader.dll。
3. 配置 API Key
编辑 data/config.json:
{
"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. 运行
python main.py
首次运行会自动在 data/ 下创建空的 chat_history.db(含一条初始对话)。
左侧栏「+ 新建对话」开始聊天;输入框上方可贴图/拖文件/截屏(Ctrl+Shift+S)。
5. 打包 exe(可选)
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. 两条铁律(写新测试必须遵守)
- 不得污染真实数据库:测试启动时必须把
core.db_manager._DEFAULT_DB指向临时文件import core.db_manager as _dbm _dbm._DEFAULT_DB = os.path.join(tempfile.gettempdir(), f"t_{os.getpid()}.db") # 必须在 import MainWindow 之前 - 不得污染真实配置(会写
config.json的功能):把HAOCODE_CONFIG_FILE指向临时文件
2. 跑测试(建议先设两个环境变量)
set PYTHONIOENCODING=utf-8
set QT_QPA_PLATFORM=offscreen :: 只有带 GUI 的 smoke_* 需要
A. 纯逻辑测试(无 GUI,秒级)
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)
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. 辅助工具
python tests/check_db_migration.py <某个 .db 或备份 .zip>
# 只读校验:迁移(新增列)后旧库数据是否 100% 原样保留(消息数/父指针/叶子/分支点逐项比对)
六、已知限制
- 仅 Windows:WebView2 与
bash工具都按 Windows 语义实现(cmd.exe + taskkill /T)。 bash工具的真实 shell 是 cmd.exe(不是 git-bash):;不是命令分隔符、$VAR不展开、cd不跨命令保持 —— 系统提示词里已写明这些陷阱与正确写法。- HTTP 代理/流式:模型请求走 openai SDK,单次 180s 超时,失败按 2s/4s/8s 重试 3 次。
- 打包体积:默认带 QtWebEngine 兜底,产物约 575 MB;若确定只用 WebView2,
可从
haocode.spec去掉PyQt6.QtWebEngine*的collect_all(降到约 110 MB)。 data/attachments/里的图片/PDF 是文件系统资源,删库不会删它们;删会话时才会连带清理。- 冻结版与源码版会共用同一个库的唯一例外:把 exe 放在源码树的
dist/下时 (exe/../../data/存在即共用chat_history.db);放到桌面等其它位置则用 exe 同级data/。 - 运行时会往项目根目录写诊断产物(开发期排障用,可直接删):
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。