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:
2026-09-17 16:40:01 +08:00
commit a7412824e0
124 changed files with 26747 additions and 0 deletions
+235
View File
@@ -0,0 +1,235 @@
# 项目文件架构
> 仅描述目录与文件的基础组织,不涉及具体实现细节。
```
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 后正文段体检日志