Files
Haocode/ARCHITECTURE.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

236 lines
13 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.
# 项目文件架构
> 仅描述目录与文件的基础组织,不涉及具体实现细节。
```
GeekAgent-Studio/
├── main.py # 程序入口
├── requirements.txt # 依赖清单
├── untitled.ui # Qt Designer 界面文件
├── ui/ # 表现层(桌面窗口 + 本地 Web 渲染)
│ ├── views/ # PyQt6 窗口与组件逻辑
│ │ └── system_tools/ # 系统级工具(全局热键、截屏等)
│ ├── web/ # 本地 HTML/JS/CSS 渲染层
│ │ └── highlight/ # 本地代码高亮库
│ └── assets/ # 静态资源(样式表、图标)
│ └── icons/
├── core/ # 控制与大脑层(后端核心)
├── agents/ # Agent 逻辑与执行器
├── workspace/ # 智能文件系统与代码编辑
├── tools/ # 动态 Skill / Tool 生态
│ ├── builtin_tools/ # 内置基础工具
│ └── dynamic_skills/ # 动态生成的工具脚本
├── data/ # 本地数据与缓存
│ ├── attachments/ # 聊天附件
│ └── .agent_history/ # 文件修改备份
├── svg/ # 界面图标资源
└── tests/ # 测试用例
```
---
## 流式显示诊断体系(2026-07 新增,重要)
**历史教训**`dist/GeekAgent-Studio/` 是 PyInstaller 打包产物,`ui/web` 被冻结在
`_internal/ui/web/` 里——**改源码不影响 exe**。修 UI 问题后必须重新打包
`python -m PyInstaller geekagent.spec --noconfirm`),否则用户跑 exe 永远看不到修复。
**链路诊断日志**`stream_diag.log`,应用根目录,每次启动清空):
- Python 侧:`APP_START` / `FRONTEND_VER <前端版本戳>` / `CHUNK`(前 3 条 + 每 50 条
+ 会话不匹配时全记)/ `THINK` / `RESTORE` / `JS_DIAG`(回复完成时抓 JS 事件环)
- JS 侧:`window.__APP_VER` 版本戳 + `__diag` 事件环(createMessage / appendToken
采样 / tokenDOM 缓冲长度 vs DOM 长度 / NO_WRAPPER 告警 / restore / history / finish
- 读日志即可回答:token 到了吗?wrapper 在吗?缓冲有内容吗?DOM 写进去了吗?
**助手正文透明气泡**(用户方案):`.message.assistant .md-segment` 浅灰半透明底 +
圆角边框,流式中/完成后视觉一致;正文段元素在首个 token 时同步创建,气泡即时出现。
---
## Agent 核心(core/agent)— pi 1:1 移植
```
core/agent/
├── types.py # AgentMessage(kind 标记压缩摘要) / AgentConfig / 事件 / ToolCall / ToolResult
├── context.py # token 估算(usage 锚定 + CJK 感知)/ 输出钳制 / 压缩触发公式
├── stream_fn.py # openai_stream:流式 + tools 序列化 + OpenAI⇄pi 消息转换
├── tools.py # bash(shell=True) / read / write / edit + 文本兜底解析器
├── loop.py # run_loop:系统提示注入 → 流式 → 工具批执行 → 截断保护
├── agent.py # Agent:事件订阅/发布(subscriber 异常静默,pi 语义)
├── compaction.py # pi harness 压缩算法 1:1(切点/断轮/迭代摘要/文件附录)
└── recovery.py # 429/上下文溢出/截断 重试与恢复(pi 精确语义)
```
- 每次发送 → 新 `AgentWorker`core/llm_engine.pyQThread 胶水层);
多轮工具循环在一次发送内部闭环,UI 线程只收 Qt 信号。
- `stream_fn` 协议:`stream_fn(context, model, signal, max_tokens, tools=None)`
`tools` 必须序列化进 OpenAI 请求体(否则模型只能"文字扮演"工具调用)。
- `SYSTEM_PROMPT.md`(项目根):worker 模式每次请求头部注入,不入历史、不受压缩影响。
### 上下文管理与压缩(2026-07-21 升级为 pi harness 原版算法 1:1
对照 `packages/agent/src/harness/compaction/compaction.ts` + `utils.ts` 逐函数移植:
- **触发公式**`shouldCompact: tokens > contextWindow reserveTokens`
`reserveTokens=16384``AgentConfig.compaction_reserve`),替换旧 40% 启发式。
- **token 估算**usage 锚定 1:1——最后一条有效 assistant(非 aborted/error 且
totalTokens>0)的精确值 + 其后消息逐条估算;无 usage 则全部逐条。
逐条估算用 CJK 感知版(唯一已声明偏差:pi 是 chars/4)。
- **切分点** `find_cut_point`:从尾部回扫累计 token 至 `keepRecentTokens=20000`
取该位置起第一个有效切点(user/assistanttoolResult 不可切);
切点非 user → **断轮**:轮起点前的历史与轮前缀**分两次 LLM 摘要**,
拼接 `{history}\n\n---\n\n**Turn Context (split turn):**\n\n{prefix}`
- **迭代式更新**:上次压缩摘要(`kind="compaction_summary"` 消息)不重摘,
作为 `<previous-summary>``UPDATE_SUMMARIZATION_PROMPT`pi 四套提示词逐字移植)。
- **文件操作附录**:摘要范围内 read/write/edit 工具的 path 提取,
摘要尾部追加 `<read-files>/<modified-files>`1:1 utils.ts)。
- **摘要预算**`maxTokens = min(0.8×reserve, model.maxTokens)`;轮前缀 `0.5×reserve`
- **对话序列化**`[User]:` / `[Assistant thinking]:` / `[Assistant]:` /
`[Assistant tool calls]:` / `[Tool result]:`(超 2000 字符截断)逐字一致。
- **唯一已声明偏差**:摘要 LLM 调用失败时降级为机械摘录(pi 返回错误),
桌面应用优先不丢上下文。
- 架构级差异:会话重启后 agent 内存态不持久化(DB 只存时间线),
压缩摘要随之丢失、上下文从零重建——与 pi 的 compaction 条目落盘不同(见桌面文档)。
## 时间线持久化(2026-07 新增)
agent 模式的完整事件时间线按序落库,切会话/重载后 1:1 还原:
```
messages.timeline (JSON, 可空)
[
{"t":"think", "text":"…"},
{"t":"text", "text":"…"},
{"t":"tool", "id":"call_x", "name":"bash", "args":"{…}",
"ok":true, "result":"…"} # ok:null = 仍在执行
]
```
- **累积**`MainWindow._active_streams[session]["timeline"]`
reasoning/chunk/tool 三组 Qt 信号按到达顺序累积。
- **入库**:正常结束 / 中止 两条路径都带 `timeline``db.add_message`
- **重载**`load_messages_to_web` 对带 timeline 的 assistant 行调
`renderTimelineHistory`(静态时间线:思考折叠、chip 定格、文本完整渲染)。
- **切回进行中会话**`restoreStreamingTimeline` 按流式状态恢复,
后续 token 无缝续流(续接紧邻的进行中块,否则新开一段)。
- **API 上下文重建**`build_api_context` 对带 timeline 的行重建完整链
assistant+tool_calls → tool 消息),模型跨轮次工具记忆不丢。
- 旧消息(无 timeline 列值)走原有聚合渲染,向后兼容。
## 流式渲染鲁棒性 + agent 运行期 UX(本轮新增)
### 1. 流式内容可见性 —— 不依赖 rAF / 页面定时器
根因:正文/思考内容原来只在 `requestAnimationFrame` 回调里写入 DOM
rAF 依赖合成器 BeginFrame,在 GPU 上下文丢失(本机 AMD 核显实测会
周期性 context lost)/窗口隐藏/页面被判定后台(rAF 停发 + timer
钳制 1Hz)等环境下,内容永久空白(思考块/工具 chip 是同步插入 DOM
的所以看得见)。
对策(三层,均不依赖页面帧/定时器):
1. **同步渲染通道** `syncRenderThrottled``appendToken`/`appendReasoning`
在同一 JS 任务内 30ms 节流直接写 DOMtoken 由 Python runJavaScript
送达,必被执行);
2. **恢复路径同步上屏**`renderTimelineEntries(liveMode)` 结束即
`syncRenderThrottled(msgId, 0)` —— 中途切回会话,恢复的正文/思考
不等任何帧立即显示;
3. **Qt 看门狗**`MainWindow._render_watchdog`200ms):对当前会话
活跃流 `forceRenderNow(msgId)`(幂等,尾部无变化时近零开销)。
另有:`doStreamingRender` 逐段 try/catch(解析异常 → 全量重解析);
rAF 通道保留作平滑优化 + 40ms setTimeout 兜底;收尾取消全部定时器。
回归测试:`smoke_live_guard.py` Phase 0rAF+全部页面定时器 kill →
burst 后 400ms 内容必现)+ Phase A(仅 rAF 卡死 → 40ms 兜底);
`smoke_midswitch.py`(流式中切走→切回:块顺序/正文/思考/DB 完整 +
切回后立即同步可见)。
### 2. 深度思考:默认收起 + 进行中蓝色动画
- 流式/恢复/历史三条路径的思考块一律 `open=false`
- 进行中块带 `.streaming-think`:标签蓝色呼吸 + 省略号动画(CSS
`think-breathe` / `think-dots`);
- 收尾移除动画类,标签还原「已完成深度思考」。
### 3. bash chip:耗时 / 命中超时徽章
- bash 工具结果自带 `[exit N] (X.Xs)``命令超时(>Ns)已终止`
- `_parseToolTiming` 解析后在摘要行插入 `⏱ X.Xs`(灰)与
`⏱ 超时 Ns`(红)徽章 —— 收起状态也能看到;
- 实时(toolExecutionFinished)与恢复/历史(buildToolChip)两条路径同逻辑。
### 4. 工具结果:展开必显示 + 长结果尾部预览 + 展开按钮
- 修复旧 bug`toolExecutionFinished` 原来只在 `chip.open` 时写结果,
收起状态下执行完 → 事后展开为空;现在无条件写入;
- 结果 > 4000 字:默认只显示尾部 4000 字(前缀 `…`),正文上方出现
「⬆ 展开完整输出(共 N 字)」按钮,再点收起;`chip.__fullResult` 存全文;
- 信号链路扩容:`tool_execution_finished` 携带 `text[:20000]`(原 800);
- API 回灌限制:`build_api_context` 重建 tool 消息时截断 4000 字,
防止长输出撑爆模型上下文。
---
## 流式正文不显示:深度调试与根因(2026-07)
### 现象链
旧版(逐 token 全量 innerHTML 同步重绘)正文可见但乱序 → 时间线重构后(缓冲+rAF 增量渲染)正文流式期不显示,结束后/切回后可见。
### 诊断数据(用户环境 stream_diag.log,前端 v2
- `FRONTEND_VER 20260721-v2` ✓ 最新前端在跑
- CHUNK/THINK 逐 token 到达 UI 线程,会话匹配 ✓
- JS_DIAG 完成时刻环形缓冲 = 思考事件以 ~95/s 流入 → **JS 线程活着,token 在被处理**
- 结论:不是 JS 没跑,是**屏幕没有把 DOM 变化画出来**
### 根因(本机复现)
AMD 核显 GPU 上下文周期性丢失:
-`--enable-gpu-rasterization --ignore-gpu-blocklist`(旧默认)离屏实测:
连续 `SharedImageStub: context already lost` 错误 → **页面 JS 处理/Qt 定时器全部停摆**
-`--disable-gpu` 后同样代码 100% 正常(真实消息内容回放探针 b/d 双满)
- 用户屏幕上的部分停摆:DOM 已写入,但 GPU 光栅化产物无法合成上屏 → 正文不可见
### 修复
1. **main.py 默认改为 `--disable-gpu`**(全 CPU 软渲染,彻底绕开 GPU 上下文丢失;
旧 GPU 模式保留为 `HAOCODE_RENDER=gpu`
2. 保留双保险渲染通道(同步节流渲染 + 200ms Qt 看门狗 forceRenderNow
3. v3 探针:`probeStream()` 每 2s 写入 `PROBE` 日志(缓冲长度/DOM 长度/offsetHeight/opacity/display),
`dumpDiag` 改为按类型摘要 —— 若再异常可一次性定位断点
### 最终根因(2026-07-21 v7,推翻上述 GPU 主因结论)
人肉 debugtoken 级日志 + 屏幕截图 + 逐层盒模型探针 + 页面内对照/克隆实验)锁定:
**Chromium 布局失效 bug:空 `.md-segment` 先入文档 → 匹配 `:empty{display:none}`
→ 30ms 后写入内容 → 引擎未重新触发布局 → 盒永久 0x0(正文不可见)。**
证据链:
1. token 日志:正文 `段buf``dom`textContent)同步增长(71c 全在 DOM),但 `h=0`
2. 像素分析 `diag_shot_*.png`:完成时刻屏幕上也无正文(切会话才可见 = DB 重渲路径)
3. 逐层探针:seg 的祖先链全部存活(reply-content w=119/h=75),唯 seg 子树 w=0/h=0
4. 对照实验:同容器新建的 `.md-segment`(带内容)h=24 正常;原始流式段 h=0
5. 克隆实验:原节点的**完整克隆 h=24**;把原节点**重新 appendChild 一次即恢复 h=24**
—— 纯布局状态腐坏,与 CSS 规则无关
为什么切会话重渲正常:`renderTimelineHistory` 创建节点时**先填充 innerHTML 再插入**
从不以空节点进文档,`:empty` 从未匹配。
### v7 修复(真正根因)
1. **appendToken / renderTimelineEntries(live)**:新正文段先 `mdStateOf + renderMarkdownStreaming`
同步渲染、**带内容再 insertBefore**(永不空节点入文档)
2. `.md-stable/.md-tail``display:contents``block`(规避 contents 布局风险,加内层边距补偿)
3. 周期性流式截图改为 `HAOCODE_SHOT=1` 可选 —— 实测 `QWidget.grab()` 强制出帧会
阻塞渲染器主线程 1-2s,会干扰正文 token 处理(诊断干扰项)
4. 保留 `--disable-gpu` 默认(对离屏测试环境仍有必要)与双通道渲染 + 看门狗
5. 新增 `tests/verify_onscreen.py`:**真实窗口真实布局**验证(offscreen 无布局,
此类 bug 离屏测不出来;本 bug 即由此漏检 3 轮)
6. v6 起 JS 端带渲染器主线程心跳(dt>1.5s 告警)+ finish 后正文段体检日志