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.
289 lines
25 KiB
Markdown
289 lines
25 KiB
Markdown
# Frame —— haocode 逐目录 / 逐文件说明
|
||
|
||
> 本文件是 `readme.md` 的下钻版:**每个文件夹、每个文件到底负责什么**,
|
||
> 以及关键的类 / 函数名(方便直接搜索定位)。
|
||
> 阅读建议:先看 `readme.md` 的架构总览,再回来看这里的目录细节。
|
||
|
||
---
|
||
|
||
## 0. 一眼看清分层
|
||
|
||
| 层 | 目录 | 职责 | 能否单独跑 |
|
||
|---|---|---|---|
|
||
| 入口 | `main.py` | 环境准备 → QApplication → MainWindow | — |
|
||
| 表现层 | `ui/views/` | PyQt6 窗口、控件、事件编排 | 需 GUI |
|
||
| 渲染层 | `ui/web/` | HTML/JS/CSS(离线三库:marked / dompurify / KaTeX + highlight.js) | 浏览器可单独开 |
|
||
| 引擎层 | `core/llm_engine.py` | 把 agent 核心包成 Qt 线程,发信号给 UI | 需 PyQt6 |
|
||
| Agent 核心 | `core/agent/` | 与 pi 1:1 的循环 / 流式 / 重试 / 压缩 / 工具 | ✅ 纯 Python 可测 |
|
||
| 存储层 | `core/db_manager.py` | SQLite:会话树、消息、压缩标记、附件 | ✅ 纯 Python 可测 |
|
||
| 外部集成 | `core/webview2.py`、`vendor/webview2/` | WebView2 原生子窗口 + .NET SDK | 需 Windows |
|
||
| 资源 | `svg/`、`ui/web/katex|highlight`、`data/` | 图标 / 前端库 / 配置 | — |
|
||
|
||
---
|
||
|
||
## 1. 根目录文件
|
||
|
||
| 文件 | 说明 |
|
||
|---|---|
|
||
| `main.py` (111 行) | **程序入口**。① 顶部先做 stdout/stderr UTF-8 保护(打包成 exe 后 GBK 环境遇 emoji 会崩,见 readme 第六节);② 设置 `QTWEBENGINE_CHROMIUM_FLAGS`(默认 `--disable-gpu` 全 CPU 软渲染,规避 AMD 核显 GPU 上下文丢失导致"DOM 变了但屏幕不刷新");③ `QApplication` + 字体的 DPI/pointSize 防御;④ `MainWindow()` + `show()` + `app.exec()`。 |
|
||
| `SYSTEM_PROMPT.md` (~3400 tokens) | **agent 系统提示词**。0 身份 / 1 运行环境(含 §1.1 cmd.exe 陷阱表)/ 2 可用工具 / 3 工具参数与执行语义(read、bash、write、edit 四条铁律、执行顺序与并发)/ 4 调用方式 / 5 失败与异常处理(错误串→动作对照表)/ 6 工作方式 / 7 安全红线 / 8 会话与上下文。**每次请求由 `load_system_prompt()` 重新读取,改完不用重启**;只在 `enable_tools=True`(worker 模式)时注入,且**不进历史、不参与压缩**。 |
|
||
| `requirements.txt` | 依赖清单(只列代码真实 import 的包,AST 扫描核对过)。 |
|
||
| `haocode.spec` | **PyInstaller 打包配置**(onedir)。关键点:把 `ui/web`、`svg`、`vendor/webview2`、`SYSTEM_PROMPT.md`、`data/config.json`、`WebView2Loader.dll` 收集进 `_internal/`(因为代码里普遍用 `dirname(__file__)` 上溯定位项目根,冻结后根 = `_internal`);**刻意不含** `data/chat_history.db` 与 `data/attachments/`;`console=False`;带 `runtime_hooks`。 |
|
||
| `pyi_rth_trace.py` | 打包**运行时钩子**:把冻结版 stdout/stderr 重定向到 exe 同级 `haocode.log`(GUI 版没有控制台,没有它启动异常会完全静默)。 |
|
||
| `WebView2Loader.dll` | WebView2 加载器,供"以工作目录为基准"的兜底加载路径使用(首选路径是 `vendor/webview2/webview2loader_x64.dll`)。 |
|
||
| `ARCHITECTURE.md` / `readme_our.md` / `黑边两现象分析报告.md` | **历史文档**(早期架构、早期设计稿、窗口缩放黑边问题分析)。其中提到的 `agents/`、`workspace/`、`ui/assets/`、`untitled.ui` 等目录/文件**已在交接版移除**,最新结构以 `readme.md` + 本文件为准。 |
|
||
| `_out.txt` / `_t0.txt` | 文本示例文件(按交接要求保留)。 |
|
||
|
||
---
|
||
|
||
## 2. `core/` —— 后端核心
|
||
|
||
### 2.1 `core/db_manager.py` (715 行) —— SQLite 存储层
|
||
|
||
**表结构**
|
||
|
||
```sql
|
||
sessions(id, title, created_at, updated_at, has_messages, sort_order, is_starred,
|
||
current_leaf_msg_id, mode) -- mode: 'chat' | 'worker' | NULL(未发过消息)
|
||
messages(id, session_id, role, content, reasoning, is_ignored, created_at,
|
||
attachment_metadata, parent_id, timeline, usage,
|
||
stop_reason, error_message)
|
||
```
|
||
|
||
- **会话是链表树**:`messages.parent_id` 指向上一条,`sessions.current_leaf_msg_id` 记录当前叶子。
|
||
`get_message_chain()` 从叶子向上回溯再反转 → 得到"当前激活时间线"(绕开所有废弃分支)。
|
||
- `role` 取值:`user` / `assistant` / `system` / **`compaction`**(压缩标记行,`content` 是摘要)。
|
||
- `timeline`:assistant 行的**事件时间线 JSON**(`{"t":"think"|"text"|"tool", ...}`),
|
||
工具条目形如 `{"t":"tool","id","name","args","ok","result"}`;`ok=None` 表示开始了但没拿到结果。
|
||
- `usage`:本轮精确 token 用量(`{"input","output","cacheRead",...}`),供显示与压缩估算做**锚点**。
|
||
- `stop_reason` / `error_message`:失败轮次标记(`'error'`),NULL = 正常行。
|
||
|
||
**关键方法**
|
||
|
||
| 方法 | 作用 |
|
||
|---|---|
|
||
| `add_message(...)` | 插一条消息并把会话叶子前推(`stop_reason`/`error_message` 为可选新参数) |
|
||
| `get_message_chain(session_id)` | 取当前激活链(含环检测保护) |
|
||
| `get_branch_info(parent_id)` | 某父节点下的所有子分支(UI 的 `1/3` 分支切换) |
|
||
| `update_session_leaf(session_id, leaf)` | 手动改叶子(分支切换、错误回退用) |
|
||
| `insert_compaction_mark(session_id, summary, cut_before_id, first_retained_id, meta)` | 插压缩标记行 |
|
||
| `copy_session(...)` | 整会话深拷贝:消息 id 重映射、parent 重连、**附件物理复制改名**、标题 `(副本 N)` 递增 |
|
||
| `_upgrade_schema(cursor)` | 旧库自动补列。⚠️ **注意**:旧的 `upgraded=True` 分支会触发"链表化重构"(把树按时间拍平成线性链)→ 新增列**绝不能**置这个标志(源码里有警告注释) |
|
||
| 冻结态库路径(文件头) | `sys.frozen` 时优先 `exe/../../data/chat_history.db`(存在才共用),否则 `exe 同级/data/chat_history.db` |
|
||
|
||
### 2.2 `core/llm_engine.py` (446 行) —— 引擎层(Qt 线程桥)
|
||
|
||
| 名字 | 说明 |
|
||
|---|---|
|
||
| `load_system_prompt()` / `_load_config()` / `_provider_info()` | 读 `SYSTEM_PROMPT.md`、`data/config.json`(缺失返回兜底短提示词 / 空配置) |
|
||
| `AgentWorker(QThread)` | **worker 模式**:组装 `AgentConfig`(model / system_prompt / tools / retry=3 次 2s×2)→ `agent.set_stream_fn(openai_stream)` → `AgentRunner(agent, summarize_fn=..., 回调...)` → `runner.run(last_user)`。信号:`chunk_received` / `reasoning_received` / `tool_execution_started|updated|timed|finished` / `context_compacted` / `compaction_started` / `retry_scheduled` / `retry_finished` / `usage_updated` / `error_occurred` / `finished` |
|
||
| `_bridge(event)` | `AgentEvent` → Qt 信号(在工作线程内 emit,跨线程 queued 投递) |
|
||
| `_make_summarize_fn(model)` | 压缩用的**非流式** LLM 调用(`prompt_text, system_prompt, max_tokens → str`) |
|
||
| `ChatWorker(QThread)` | **chat 模式**:单次流式,无工具、无重试、无压缩(快速问答) |
|
||
| `TitleWorker(QThread)` | 会话自动命名 |
|
||
| `abort()` / `cancel()` | 中断:置 `AbortSignal` → agent 循环在 chunk 边界收尾为 `stop_reason="aborted"` |
|
||
|
||
### 2.3 `core/webview2.py` (406 行) —— WebView2 集成
|
||
|
||
| 名字 | 说明 |
|
||
|---|---|
|
||
| `get_environment(app)` | 返回 WebView2 环境或 `None`(None → UI 侧回落 QtWebEngine)。**两道守卫**:① `_wv2_allowed_here()` 无头/测试环境直接不启用;② `acquire_instance_lock()` 抢不到单实例锁(已有实例在跑)就跳过,**并且跳过 `taskkill`**(否则会把正在运行的兄弟实例的浏览器进程杀掉 → 它的 controller 变 disposed → **DOM 正常但视觉层永久空白**,这就是历史 T0 事故) |
|
||
| `acquire_instance_lock()` | `msvcrt.locking` 锁 `data/app_instance.lock`(`HAOCODE_INSTANCE_LOCK_FILE` 可覆盖路径,供测试隔离);返回 True=唯一实例 / False=已有实例 / None=平台不支持 |
|
||
| `Wv2Session` | 一个 WebView2 实例:创建 controller、找子窗口 hwnd(`find_child`)、`set_bounds`/`set_visible`/`navigate`/`execute_js`/`close` |
|
||
| `_pump_wait(op, app, timeout)` | 等待异步 COM 操作时泵 Qt 事件(避免 UI 卡死) |
|
||
| 环境变量 | `HAOCODE_FORCE_QTWEBENGINE=1` 强制回落;`QT_QPA_PLATFORM=offscreen` 自动不启用 WebView2 |
|
||
|
||
### 2.4 `core/debug_log.py` (78 行) —— 调试事件总线
|
||
|
||
`debug_log(msg, tag)` 写内存环形缓冲 + 落盘;`poll_debug_cmd()` 取调试窗口输入的命令;
|
||
`autostart_debug_window(cfg)` 按 `config.json: debug_window_autostart` 决定是否开调试窗口。
|
||
|
||
### 2.5 `core/agent/` —— pi 1:1 agent 核心(9 文件)
|
||
|
||
| 文件 | 行数 | 关键名字 | 说明 |
|
||
|---|---|---|---|
|
||
| `types.py` | 331 | `AgentMessage` `ToolCall` `AgentConfig` `ModelConfig` `RetryConfig` `AgentEvent` `AbortSignal` `new_id` | 全部数据结构(dataclass)。`AgentMessage` 含 `content/reasoning/tool_calls/stop_reason/error_message/usage/timestamp` |
|
||
| `agent.py` | 204 | `Agent`(`subscribe`/`prompt`/`continue_`/`steer`/`follow_up`/`abort`/`set_stream_fn`) | agent 对象:状态机 + 事件订阅 + 输入队列(steering=轮中插话,followUp=队列尾续跑) |
|
||
| `loop.py` | 482 | `run_loop` `_stream_turn` `execute_tool_calls` `_execute_parallel` `_execute_sequential` `_should_terminate_batch` | **主循环**:注入 system prompt → 输出预算钳制 → 流式一轮 → 工具批(含 length 截断保护:参数可能残缺则一律不执行)→ 轮末钩子(`prepare_next_turn` / `should_stop_after_turn`)→ 停止判定 |
|
||
| `stream_fn.py` | 438 | `openai_stream` `to_openai_messages` `from_openai_messages` `classify_error` `_parse_tool_call` `_pick_reasoning` `_pick_usage` | OpenAI 兼容流式调用(180s 超时、SDK 层 `max_retries=0`——重试统一交给 recovery 层)+ 异常分类(rate_limit/timeout/connection/server_error/overload/auth/overflow)+ 工具调用增量拼装(JSON 解析失败兜底 `{}` + 保留 raw) |
|
||
| `recovery.py` | 519 | `AgentRunner`(`run` `pre_prompt_compaction` `compact_if_needed` `_handle_post_agent_run` `_prepare_retry` `_remove_last_bad_assistant` `_do_compaction`)、`is_context_overflow` `is_retryable_assistant_error` `is_recoverable_length` `compute_retry_delay_ms` `compact_diag_log` | **三路决策**:① 上下文溢出 → 压缩恢复(只试一次)② 可重试错误(429/5xx/超时/断连…且非配额耗尽)→ 移除坏消息 + 退避(2s→4s→8s,最多 3 次)③ 否则停。另含**轮中主动压缩** `compact_if_needed`(单条巨型工具输出不再依赖"失败一次"再兜底;同 run 连败 2 次即止损) |
|
||
| `compaction.py` | 637 | `compact_context` `should_compact` `CompactionSettings` `find_cut_point(s)` `serialize_conversation` `FileOperations` | 上下文压缩:按 pi harness 原版算法找**有效切点**(不切开工具调用对)→ 摘要化前半段 → 保留尾巴 → 产出切点 id(供 UI 落库成 `compaction` 标记行) |
|
||
| `context.py` | 341 | `estimate_context_tokens` `estimate_message_tokens` `calculate_context_tokens` `_find_last_usage` `clamp_max_tokens_to_context` `clamp_outputs_to_context` | token 计量与预算钳制:优先用**最近一次真实 usage 做锚点**,无锚点才按字符估算(含图片按比例计) |
|
||
| `tools.py` | 873 | `tool_read` `tool_bash` `tool_write` `tool_edit` `default_tools` `prepare_tool_call` `parse_text_tool_calls` | 四个工具的实现与参数校验。`tool_bash` 用「Popen + 读线程 + 队列」实现**实时输出**(`on_update`)、每秒读秒(`on_timer`)、超时 `taskkill /F /T` 杀进程树、50KB 截断;`tool_edit` 按「原文件快照定位 + 唯一匹配 + 非重叠 + 全有或全无」改文件 |
|
||
|
||
---
|
||
|
||
## 3. `ui/` —— 表现层
|
||
|
||
### 3.1 `ui/views/main_window.py` (6019 行) —— 主窗口(全项目核心)
|
||
|
||
单文件承载了绝大部分 UI 与编排逻辑,按区块读:
|
||
|
||
| 区块(搜关键字) | 说明 |
|
||
|---|---|
|
||
| `MainWindow.__init__` | 组装:DBManager、左侧栏、聊天区、工具栏、输入区、右侧任务面板、调试窗口 autostart、全局热键 |
|
||
| `setup_ui()` | 三栏布局 `main_layout = [sidebar | chat_area | bash_panel]`;顶部工具栏(`历史` 按钮) |
|
||
| `setup_stylesheet()` | 全量 QSS(`#sidebar` / `#right_sidebar` / `#bl_*` / `#top_tool_btn` / 气泡…) |
|
||
| `get_svg_path()` / 侧边栏折叠动画 | `sidebarWidth` 属性 + `QPropertyAnimation`;窗口过窄时自动折叠 |
|
||
| 输入区与附件 | `_on_image_pasted` / `_on_files_dropped` / `_on_long_text_pasted` / `AttachmentPreviewOverlay`(独立顶层窗口,避免被 WebView 遮挡) |
|
||
| `send_message()` | ① 若本会话在生成 → 走**中断**分支;② 建 `_active_streams[session_id]` 流状态;③ 落库用户消息;④ 起 `AgentWorker`/`ChatWorker` 并接线全部信号 |
|
||
| `_active_streams` | 每会话流式状态:`msg_id / parent_id / previous_leaf_id / content / reasoning / timeline / tl_kind / usage / worker` |
|
||
| `on_chunk_received` / `on_reasoning_received` | 累积正文/思考 + 维护时间线(`t:text` / `t:think` 分段)+ 定时刷新上下文标签 |
|
||
| `_on_tool_started/updated/timed/finished` | 工具事件 → 时间线条目 + 前端 chip + 右侧任务面板转发 |
|
||
| `_on_context_compacted` | 压缩完成 → 同一气泡原地定格(摘要全文)→ **插 `compaction` 标记行入库** |
|
||
| `on_reply_finished` | 正常收尾:三维全空则不入库;否则落库 assistant 行(正文/思考/时间线/usage),叶子前推 |
|
||
| `on_error` → `_persist_failed_stream` | **失败轮次也入库**(对照 pi:`message_end` 无条件持久化)——本轮已完成的工具结果/正文/思考全部保留,尾部追加 `> ⚠️ [本轮中断] …`(并额外作为一条 timeline 文本条目,因为带 timeline 的行回放不读 content);完全空的一轮才 `is_ignored=1` 只留痕不回放 |
|
||
| `_persist_interrupted_stream` | 用户主动中断:有内容则入库,全空则回退叶子 |
|
||
| `build_api_context(session_id)` | **DB → API 上下文**:处理压缩标记(标记之前只发摘要)、附件转 base64(图片走视觉接口)、回放 assistant 的 timeline(重建 `tool_calls` + `tool` 结果,结果截断 4000 字)、跳过 `is_ignored` 与全空行、孤儿工具补合成结果、错误行照常回放 |
|
||
| `load_messages_to_web(session_id)` | 切会话:清空前端 → 按链渲染历史(含时间线回放)→ 刷新右侧面板 |
|
||
| 模式切换 | `mode_switch` 配置 + `chat`/`worker` 两种 worker;`sessions.mode` 锁定已发过消息的会话模式 |
|
||
| 右侧面板接线 | `bash_panel.set_session(...)` + 四个 tool 事件转发(仅当前会话) |
|
||
| 关闭清理 | `closeEvent`:中断所有 worker、持久化进行中的回复、清理后台任务 |
|
||
|
||
### 3.2 `ui/views/bash_panel.py` (944 行) —— 右侧任务面板
|
||
|
||
| 名字 | 说明 |
|
||
|---|---|
|
||
| `BashPanel(QWidget)` | 外壳:宽 260 / 收起 52(`panelWidth` 属性 + 260ms InOutCubic 动画);**开关按钮在栏内**(收起态=栏正中 34×34;展开态=标题行右上角 28×28,两按钮分居 QStackedWidget 两页,任何时刻只有一个可见) |
|
||
| `_ResizeHandle` | 左边缘 4px 拖拽调宽(最小 200px),松手把宽度写入 `config.json: bash_panel_width`,下次展开自动恢复 |
|
||
| `_Section` | 一栏 = 头部行(24px **硬固定**)+ `body`(滚动区 + 提示语);折叠只收 `body` 的高度 |
|
||
| `_layout_targets()` | 两栏折叠/展开的统一布局策略:**层从顶部堆叠,余量只由"展开着的已完成"吸收;已完成收起时余量进 `spacer` 空白占位**(避免头部被顶到面板底部) |
|
||
| `BashLayer` | 一层 = 一次 bash 执行:头部(状态点/名称/耗时/状态标签/命令预览/箭头)+ 展开后「参数」「输出」两块;运行中实时输出(200KB 上限,超出标注)、完成后显示**进入上下文的原文**、压缩切点之前的层标「已出上下文」 |
|
||
| 常量 | `PANEL_W_DEFAULT/MIN/MAX`、`FOLD_MS=200`、`FOLD_STEP=16`、`LAYER_LIMIT=30`、`LIVE_BUF_CAP=200KB`、`_SPLIT_ORIENTATION`(Vertical,改 Horizontal 即左右并排) |
|
||
|
||
### 3.3 `ui/views/` 其余文件
|
||
|
||
| 文件 | 行数 | 说明 |
|
||
|---|---|---|
|
||
| `chat_bridge.py` | 188 | `ChatBridge(QObject)`:**Python → JS 单向桥**(QWebChannel)。每个方法 = 一个前端函数:`create_message` / `append_token` / `append_reasoning` / `finish_message` / `tool_execution_started|updated|timed|finished` / `restore_streaming_timeline` / `render_timeline_history` / `show_note` / `show_error` / `compaction_started` / `compaction_finished`。用 `json.dumps` 转义全部文本,杜绝注入 |
|
||
| `wv2_view.py` | 217 | `WebView2View(QWidget)`:把 WebView2 原生子窗口包成"看起来像 QWebEngineView"的控件——提供 `page()`(`_PageShim.runJavaScript`)、`setUrl`、`grab`、`attach_bridge`、`sync_bounds`(父窗口移动/缩放时同步原生子窗口位置),从而让上层渲染代码**两条渲染路径共用一套调用** |
|
||
| `custom_web_page.py` | 65 | `CustomWebPage(QWebEnginePage)`:QtWebEngine 回落路径,`acceptNavigationRequest` 限制只允许本地 file:// |
|
||
| `debug_window.py` | 214 | `DebugWindow(QWidget)`:两个页签「对话消息 / 应用日志」+ 命令输入框;`_TailReader` 增量读日志文件 |
|
||
|
||
### 3.4 `ui/views/system_tools/`
|
||
|
||
| 文件 | 说明 |
|
||
|---|---|
|
||
| `file_reader.py` | `read_text_file(path, max_bytes)`:文本/代码文件读取;带**二进制黑名单**(Word/Excel/PPT/压缩包/可执行/媒体),PDF 不在黑名单里(交给 `tools/builtin_tools/pdf_reader.py`) |
|
||
| `global_hotkey.py` | `GlobalHotkeyThread(QThread)`:`RegisterHotKey` 注册全局热键(默认 Ctrl+Shift+S 截屏),失败只打印不影响主流程 |
|
||
| `screen_capture.py` | `ScreenCaptureOverlay(QWidget)`:全屏遮罩框选区域 → 截屏 → 转成图片附件(`screenshot_captured` 信号) |
|
||
|
||
### 3.5 `ui/web/` —— 本地渲染层(完全离线)
|
||
|
||
| 文件 | 行数 | 说明 |
|
||
|---|---|---|
|
||
| `index.html` | 82 | 页面骨架 + 引入本地 `marked` / `dompurify` / `highlight` / `katex`(无任何 CDN) |
|
||
| `app.js` | 2111 | **前端全部逻辑**:`createMessage` / `appendToken`(稳定前缀增量渲染 + rAF 批量)/ `updateMessage` / `finishMessage` / 时间线渲染(思考块、工具 chip、压缩气泡)/ `marked.parse` 统一拦截点(先抽公式占位符再渲染,最后 KaTeX 回填)/ 自动滚动与"贴底"判定 / 分支切换 `1/3` |
|
||
| `style.css` | 902 | 全部样式:气泡(assistant 85% 宽、透明正文)、思考块、工具 chip、紧凑模式、公式块、错误提示条 |
|
||
| `marked.min.js` / `dompurify.min.js` | — | Markdown 渲染 + HTML 消毒 |
|
||
| `highlight/` | — | highlight.js + atom-one-dark 主题 |
|
||
| `katex/` | — | KaTeX 0.16.11(js + css + woff2 字体) |
|
||
|
||
---
|
||
|
||
## 4. `tools/`
|
||
|
||
| 文件 | 说明 |
|
||
|---|---|
|
||
| `builtin_tools/pdf_reader.py` | **PDF 专用解析**(`extract_pdf_text` / `extract_pdf_images`),基于 PyMuPDF。被 `ui/views/main_window.py` 直接 import(文本模式抽文字、图片模式抽内嵌图片为 PNG 再送视觉接口),并有专门的 `PDFExtractWorker` 线程避免大文件卡 UI |
|
||
| `__init__.py` / `builtin_tools/__init__.py` | 包结构标记(**必须保留**,主窗口按 `tools.builtin_tools.pdf_reader` 路径 import) |
|
||
|
||
> 交接版已删除该目录下**全空且零引用**的 `registry.py` / `conda_env.py` / `file_ops.py` / `web_search.py`。
|
||
|
||
---
|
||
|
||
## 5. `svg/` —— 图标(18 个)
|
||
|
||
`panel.svg`(左侧栏开关)/ `panel_right.svg`(右侧任务面板开关)/ `chevron_down|right.svg`(弹窗箭头)/
|
||
`mode_chat.svg` / `mode_worker.svg`(模式)/ `model.svg` / `provider*.svg`(模型弹窗)/ `send*.svg` / `stop*.svg` /
|
||
`upload.svg` / `cross.svg` / `check.svg` / `main.svg`。全部经 `MainWindow.get_svg_path()` 按项目根解析。
|
||
|
||
---
|
||
|
||
## 6. `data/` —— 运行时数据
|
||
|
||
| 文件 | 说明 |
|
||
|---|---|
|
||
| `config.json` | 供应商/模型/开关(**交接版已清空 api_key**)。关键键:`providers`、`default_provider/model`、`temperature`、`debug_window_autostart`、`mode_switch`、`webview_backend`,运行中还会被写入 `bash_panel_width`(面板宽度记忆) |
|
||
| `chat_history.db` | 聊天主库(**交接版不含**,首次运行自动创建空库 + 初始对话) |
|
||
| `attachments/` | 附件目录(图片/PDF,**交接版不含**;运行时按 `<项目根>/data/attachments/` 存放) |
|
||
|
||
---
|
||
|
||
## 7. `vendor/webview2/`
|
||
|
||
WebView2 的 .NET SDK(随仓库提供,不走 pip):
|
||
`net462_Microsoft.Web.WebView2.Core.dll`(`clr.AddReference` 加载)、`webview2loader_x64.dll`(loader)、`sdk.nupkg`(原始包)。
|
||
|
||
---
|
||
|
||
## 8. `tests/` —— 测试与调试工具
|
||
|
||
### 8.1 正式测试套件(交接后应保持全绿)
|
||
|
||
| 文件 | 内容 |
|
||
|---|---|
|
||
| `run_tests.py` | **离线 harness**(自带用例收集 + pytest stub),跑 `test_agent_core.py` |
|
||
| `test_agent_core.py` | agent 核心:流式增量、工具批、重试、压缩、读秒、超时杀树 |
|
||
| `test_tool_params.py` | 四个工具的参数校验与错误串(35 项) |
|
||
| `test_compaction_persist.py` | 压缩标记入库 + 上下文截断行为 |
|
||
| `test_copy_session.py` | 会话复制(消息/分支/附件深拷贝/标题递增/叶子)(54 项) |
|
||
| `test_bash_stream.py` | bash 实时输出、每秒读秒、超时杀进程树、50KB 截断、非零退出 |
|
||
| `test_error_persist.py` | **失败轮次入库 + 回放取舍**(T1–T9,含旧库迁移不得破坏分支的安全用例) |
|
||
| `test_wv2_guard.py` | WebView2 双守卫(无头环境 / 跨进程单实例锁) |
|
||
| `test_debug_window.py` | 调试窗口事件与日志 |
|
||
| `test_think_code_neutral.py` | 思考块/代码块内容中性化 |
|
||
| `test_file_attach.py` / `test_pdf_reader.py` | 附件类型判定 / PDF 文本与图片解析 |
|
||
| `test_math_extract.js` | 前端公式提取逻辑(Node 运行,39 项) |
|
||
| `smoke_offscreen.py` | 主窗口离屏冒烟(8 项) |
|
||
| `smoke_mode.py` | chat ↔ worker 模式切换(16 项) |
|
||
| `smoke_copy_session.py` | 复制会话 UI 全链路 |
|
||
| `smoke_bash_panel.py` | 右侧任务面板全链路(含折叠动画/布局策略/拖拽调宽,116 项) |
|
||
| `check_db_migration.py` | 只读工具:校验库迁移后旧数据 100% 原样(消息数/父指针/叶子/分支点逐项比对) |
|
||
|
||
### 8.2 真机 / 真 API 工具(按需使用,不属于回归)
|
||
|
||
`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_agent.py`、`diag_live_onscreen.py`、`diag_live_text.py`、
|
||
`verify_onscreen.py`、`verify_math_render.py`、`inject_math_demo.py`、`debug_inject.py`、
|
||
`tune_mode_popup.py`、`tune_model_popup.py`(两个弹窗调参工具)。
|
||
|
||
### 8.3 写测试的两条铁律
|
||
|
||
1. 不得污染真实库:测试开始时把 `core.db_manager._DEFAULT_DB` 指向临时文件(**必须在 import MainWindow 之前**)
|
||
2. 不得污染真实配置:把 `HAOCODE_CONFIG_FILE` 指向临时文件(会写 `config.json` 的功能)
|
||
|
||
> 交接版已清除 81 个开发期临时物(`_tmp*` 脚本 / `_tmp_resize_vis/` 截图 / `_tmp_*.log`)。
|
||
|
||
---
|
||
|
||
## 9. 一次提问的完整生命周期(把上面所有文件串起来)
|
||
|
||
```
|
||
① 用户输入(ui/views/main_window.py: send_message)
|
||
└─ 落库 user 行(core/db_manager.add_message)→ 建 _active_streams[sid] 流状态
|
||
|
||
② 组装上下文(main_window.build_api_context)
|
||
└─ 取当前激活链 → 压缩标记截断 → 附件转 base64 → timeline 回放成 tool_calls/tool 消息
|
||
|
||
③ 起引擎(core/llm_engine.AgentWorker.run)
|
||
└─ AgentConfig(model, system_prompt=SYSTEM_PROMPT.md, tools=default_tools(), retry=3×2s)
|
||
+ AgentRunner(summarize_fn=..., on_retry_* / on_compaction_* 回调)
|
||
|
||
④ agent 核心(core/agent/loop.run_loop)
|
||
├─ 轮首/轮中压缩检查(recovery.compact_if_needed → compaction.compact_context)
|
||
├─ 流式一轮(stream_fn.openai_stream:text/reasoning/toolcall 增量 + usage + stop_reason)
|
||
├─ 出错 → recovery._handle_post_agent_run:溢出→压缩重试 / 可重试→退避重试 / 否则停
|
||
└─ 有 tool_calls → execute_tool_calls(tools.py 的四个工具,并行/串行按批次规则)
|
||
|
||
⑤ 事件回流 UI(llm_engine._bridge → Qt 信号 → main_window 的 on_* 处理器)
|
||
└─ chat_bridge 调前端 app.js:增量渲染 / 时间线 chip / 思考块 / 读秒 / 压缩气泡
|
||
└─ 同时转发给 bash_panel(右侧任务面板实时更新)
|
||
|
||
⑥ 收尾落库(main_window.on_reply_finished 或 on_error→_persist_failed_stream)
|
||
└─ assistant 行 = 正文 + 思考 + timeline(工具) + usage(+stop_reason),叶子前推
|
||
└─ 压缩发生 → 另插 role='compaction' 标记行
|
||
|
||
⑦ 下一次提问回到 ②,此时模型能看到上一轮的正文、工具调用与结果(失败轮次亦然)
|
||
```
|