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.logpyi_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_messageget_message_chaincopy_session
系统提示词 SYSTEM_PROMPT.md 直接改,每次请求重新读取(无需重启)
工具的实现与参数校验 core/agent/tools.py tool_read / tool_bash / tool_write / tool_edit
工具调用循环 / 停止条件 core/agent/loop.py run_agent_loopexecute_tool_calls
重试与压缩策略 core/agent/recovery.py _handle_post_agent_run(三路决策)
上下文压缩算法 core/agent/compaction.py compact_contextshould_compact
token 估算 / usage 锚定 core/agent/context.py estimate_context_tokenscalculate_context_tokens
模型请求 / 流式解析 / 错误分类 core/agent/stream_fn.py openai_streamclassify_error
WebView2 行为(锁/无头/清场) core/webview2.py get_environmentacquire_instance_lock
调试窗口 ui/views/debug_window.py + core/debug_log.py
打包 haocode.spec + pyi_rth_trace.py

五、测试方式

1. 两条铁律(写新测试必须遵守)

  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 之前
    
  2. 不得污染真实配置(会写 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.pysmoke_midswitch.pysmoke_persist.pysmoke_probe.pysmoke_repro_real.pysmoke_timeline.pysmoke_manual.pydiag_live_*.pyverify_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% 原样保留(消息数/父指针/叶子/分支点逐项比对)

六、已知限制

  1. 仅 WindowsWebView2 与 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.mdvendor/webview2/、18 个 svg、ui/web/(含 KaTeX/highlight)、正式测试套件、haocode.spec + 打包钩子
🗑 删除·空文件 agents/3 个全空)、workspace/4 个全空)、tools/{registry,conda_env}.pytools/builtin_tools/{file_ops,web_search}.pycore/{async_sync,memory_manager,prompt_templates}.pyui/views/components.pyui/assets/(空 style.qss)、tests/{test_ast,test_replace}.py、根目录 0 字节文件 —— 均已确认零引用__init__.py 属于包结构标记,全部保留)
🗑 删除·测试产物 根目录 diag_shot_*.png(36)、*.log_fadechk2.pyinspect_*.pyread_all_py.pyssh_helper.pytmp_timeout_probe.pyflowkit.dbstress_report.json 等;tests/_tmp*81 项,含 _tmp_resize_vis/ 与全部临时日志)
🗑 删除·数据库 data/chat_history.db 及全部 .bak/.pre_cleandata/attachments/data/debug_session.logdata/wv2_cache/首次运行自动新建空库(含初始对话)
🗑 删除·其它 __pycache__/ 全部、旧封装 geekagent.spec、未使用的 untitled.ui
✏️ 重写 requirements.txtAST 扫描核对)、readme.md(本文件)、Frame.md(逐目录细节)
⚠️ 注意 data/config.jsonapi_key 已清空(交接安全),请填入自己的 Key.txt 文本示例按你的要求全部保留_out.txt_t0.txt

详细到"每个子文件夹/每个文件干什么、关键类与函数叫什么",见 Frame.md

S
Description
Harness
Readme
10 MiB
Languages
Python 82.8%
JavaScript 13.4%
CSS 3.4%
HTML 0.4%