# 项目文件架构 > 仅描述目录与文件的基础组织,不涉及具体实现细节。 ``` 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.py,QThread 胶水层); 多轮工具循环在一次发送内部闭环,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/assistant;toolResult 不可切); 切点非 user → **断轮**:轮起点前的历史与轮前缀**分两次 LLM 摘要**, 拼接 `{history}\n\n---\n\n**Turn Context (split turn):**\n\n{prefix}`。 - **迭代式更新**:上次压缩摘要(`kind="compaction_summary"` 消息)不重摘, 作为 `` 走 `UPDATE_SUMMARIZATION_PROMPT`(pi 四套提示词逐字移植)。 - **文件操作附录**:摘要范围内 read/write/edit 工具的 path 提取, 摘要尾部追加 `/`(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 节流直接写 DOM(token 由 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 0(rAF+全部页面定时器 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 主因结论) 人肉 debug(token 级日志 + 屏幕截图 + 逐层盒模型探针 + 页面内对照/克隆实验)锁定: **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 后正文段体检日志