Files
HomeAgent/plan.md
JianFeeeee 147d0baaf9 fix: LLM 工具循环 400、中断消息注入、ConPTY 终端支持
- agent: 工具轮请求尾部补 user 占位(zen 网关强制),tool 消息正确配对
- agent: 工具提醒/中断以 system 角色注入并带 [中断消息] 前缀,不进用户履历;系统提示词说明中断消息格式
- agentcli: 基于 ConPTY 的交互式终端(ptywin fork),terminal_create/read/write/resize/close/watch
- webui: server 输出通道适配器(保留 reasoning_content/disable_thinking)
- GUI: 沉浸式标题栏、icon 圆角重制、mascot 等打磨
2026-08-14 00:48:40 +08:00

23 KiB
Raw Blame History

HomeAgent 生产问题修复计划

设计意图备忘(核心架构原则)

本框架的两大核心设计意图,贯穿所有插件/记忆/工具设计,所有改动必须符合

  1. 插件即 Agent 的 App —— Agent 像人用 App 一样用插件。

    • QQ 插件应像 QQ 客户端:通知到来 → 看到预览/上下文 → 一键回复。
    • 认知负荷最小:中断/通知只给「发送者昵称 + 消息预览」,元数据user_id/group_id/message_id完全走工具不进 prompt,防提示词注入、防昵称欺诈、低认知负荷。
    • 工具语义自解释qq_get_messageoutput_send__qqqq_get_history 的 Description 要让 Agent「读完即知怎么用」无需额外指令。
  2. 基于相关性的分层记忆架构 —— L1 文本流 / L2 图谱 / L3 文档向量,按相关性蒸馏、检索、归档。

    • GraphDB 去重、docToTriples 模板清理、Pipeline 增量蒸馏、嵌入模型内存优化,均服务于此。

0.1 紧急healthcheck 健康检查污染真实存储 ⚠️ 正在持续污染

现象2026-08-11 22:02 起,每 30 分钟一次):日志反复出现 [knowledge] added: _hc_knowledge_test_<ts>,且知识库出现 gotestluatest_hc_knowledge_test_* 等测试残留。

根因internal/plugins/healthcheck/plugin.go 的三个"写入通道"自检全部在真实生产存储上写入再删除

函数 写入 清理 固化问题
testMemoryRaw (:460) Memory().Commit(_hc_<ts> triple) Purge(hard) GraphDB 空实体/AUTOINCREMENT id 膨胀
testKnowledgeRaw (:485) Knowledge().Add(_hc_knowledge_test_<ts>) Remove(marker) Add 异步 go writeIndex vs Remove 异步重写竞态dirName(用了 / 解析)与 Removeid=sanitize(name) 计算不一致→ 删除可能失效 → 残留目录固化成文件
testDocStoreRaw (:521) DocMemory().Insert(...) Query 后 Remove 真实 docStore 写文件再删,抖动

原则:健康检查验证的是"写入通道是否可用"结果不应固化进生产记忆。改为独立虚拟/影子空间不落盘验证

实现

  • 核心实现SDK 新增 VirtualInstanceinternal/sdk/selftest.gohealthcheck 的三个 raw 自检改为在完全隔离的虚拟空间(os.MkdirTemp 独立图库/知识库/文档/文本)上做真实"写→查→删",绝不碰生产存储。
  • PluginSDK.Selftest()/SelftestReset() 暴露隔离实例(含 mutex 防并发),每轮自检前 SelftestReset 重建清空上轮数据。
  • LLM 驱动自检防护collectToolDefsForLLM 改为只收集只读白名单工具(isSafeReadonlyTool),写/删/改生产数据及外部副作用工具memory_commit/doc_commit/knowledge_create/cmd_run/files_write/terminal_*/output_send/spawn_child 等)一律不交给 LLM 自检,防止 LLM 乱调污染生产。
  • 单测healthcheck 自检后注入的"生产"实例内容不变(快照对比 + _hc_ 无残留);读/写工具白名单过滤测试通过。
  • 存量清理:删除生产残留的 gotest/luatest/_hc_knowledge_test_*/ 目录(保留真实知识库);备份留于 /tmp/opencode/knowledge_garbage_backup_20260812_122322
  • 代码复核healthcheck 三自检memory/knowledge/doc全部经 s.Selftest("hc") 隔离虚拟实例写→查→删生产实例零接触plugin.go:339/466-473/476-549
  • 部署验证:编译新 homed 部署后,knowledge/memory/graph.dbmemory/documents/ 不再出现 _hc_* 残留。—— 验证脚本已就绪:scripts/verify_deploy.sh [data_dir],部署后一键检查 0.1 残留 / 1 去重与 UNIQUE 迁移 / 2 archived 残留 / 5 嵌入规格

0.2 紧急QQ 消息被无视agentcli 幽灵终端自喂送风暴)⚠️ 优先处理

现象2026-08-11 20:4x用户发 QQ 私聊消息agent 不回应。日志显示 agent 被 agentcli 终端 echo 洪水完全阻塞。

根因(两个耦合缺陷,非记忆层)

  1. agentcli 幽灵终端自喂送风暴(直接原因)

    • internal/plugins/agentcli/plugin.go:602 — 终端一旦有输出就 s.InjectText("agentcli","agentcli","[终端 X 有新输出]\n...") 注入 agent 事件循环。
    • 残留 term_3bash 的 git sparse clone 进度)持续吐数据 → 插件每 NotifyOutputDelay(几秒)注入一次新输入 → agent 每 6-14 秒跑完整 LLM 工具循环去 terminal_read/terminal_list → 读到新进度 → 再注入 → 无限自循环,独占整个 eventLoop
    • 日志2026-08-11 20:4820:52 期间约 20+ 次 input from agentcli,无一例外。
  2. QQ 消息无真正优先级(放大原因)

    • QQ 走 interceptLoopcancelLLM + 塞入 interceptCheventloop.go:84-85)。
    • process() 打断后 continue 回到同一回合process.go:139QQ 的 [打断消息] 只被追加到幽灵回合对话里夹带响应,拿不到独立处理回合
    • 面对 agentcli 持续自喂送QQ 永远排不到前面 → 20:51 之后的 QQ 消息一条未回。

止血(运维,立即执行)

  • 杀掉残留 term_3 bash本机 PID 3716282→ 幽灵回声立停QQ 事件循环恢复。
  • QQ 身份注入增强third_party/homeagent-sdk/example/qq/plugin.go 中断模板新增 QQ号/群号(evt.UserID/evt.GroupIDagent 无需先调 get_message 即可知发送者身份。
  • 后续:agentcli 终端用后应 terminal_close,避免残留。

根治(改代码,入本计划)

  • Phase 6agentcli 终端通知节流/去重,同源同 tag 的"有新输出"合并;避免对长输出逐段注入。

HomeAgent 记忆层修复计划

基于生产实例诊断2026-08-11清理完成graph.db 从 6225 条 relations99.4% 垃圾) → 13 条真实关系entities 从 29 → 17。备份文件:memory/graph.db.backup.20260811_163301

核心问题清单

# 问题 影响 位置
1 GraphDB.Commit 对 relations 裸 INSERT 无去重 同一三元组每次归档无限重复,生产 5613 条 文档→来源→context_archived 重复垃圾 internal/memory/graph.go:209
2 docToTriples() 对每篇冷文档永远生成固定模板三元组文档→来源→context_archived文档→主题→{summary} 归档即写垃圾配合问题1指数级累积 internal/agent/core/distill.go:390-437
3 core.agent.distill_interval=2d 配置 静默失效Go time.ParseDuration 不支持 d 单位),回退 30 分钟默认值 archiveColdDocs 每小时跑而非每 2 天,放大问题 1/2 internal/config/registry.go:698
4 Pipeline Distiller 标称"10min 心跳"实为 7 天一批回放 文档与行为严重不符,虽未直接制造垃圾但不可信 internal/memory/pipeline/pipeline.go:194
5 嵌入模型双模加载(中 200k + 英 378k300 维)直接导致 2.4G 常驻 OOM 风险、启动慢,文档未提内存代价 部署配置 /data/cc.*.vec

修复计划

Phase 1图记忆去重最小改动、最高收益 进行中

目标Commit 对 relations 加唯一约束 + 冲突即跳过,彻底阻断重复累积。

  • Schema 迁移initSchema 新增 migrateRelationUnique——检测旧 relations 表无复合唯一约束(旧 DD表自动重建为带 UNIQUE(source_id, target_id, relation_type, session_id) 的新表并 INSERT OR IGNORE 去重(官方 12 步迁移),无需人工干预。
  • Commit 逻辑:改为"查存在 → 不存在才 INSERT 并计数;已存在则仅刷新 confidence/updated_at",重复提交不新增、不重计。
  • 验证TestCommitDedupSameSession(同会话重复 commit 不增行)、TestCommitDedupDifferentSession(跨会话允许重复)、TestMigrateRelationUniqueDedupsOldTable(旧表重建去重)全绿;go build ./... 通过。
  • 生产部署后确认graph.db 36→ 去重2 组 like/plugin 重复消失),跑 1 周不再新增重复。—— scripts/verify_deploy.sh 已含 relations 重复率 + UNIQUE 索引检查

entities 已有 UNIQUE(name) 保护,仅 relations 缺失。


Phase 2归档三元组模板清理治本 已完成

目标docToTriples 不再把 context_archived/Topic 摘要当成实体写入图库。

  • 重构 docToTriples:仅当 doc.Sourcecontext_archived 且非空时写 文档→来源文档→主题 仅当 summary 长度合理(<80 字)且非模板化时写,否则跳过。
  • 引入 doc.Meta["is_archived_context"] 标记上下文归档文档,供 docToTriples 识别并跳过(ContextToDoc 在 source=context_archived 时自动打标)。
  • 单测验证:构造冷文档 → archiveColdDocs → 无模板垃圾产出(TestDocToTriplesArchivedContext/TestDocToTriplesTemplateSummary/TestDocToTriplesLongSummary/TestIsTemplateSummary 全绿,既有 4 个 docToTriples 用例回归通过)。

Phase 3配置持久化解析修复防配置失效 计划中

目标:支持 2d/1w 等人类可读时长单位,配置即时生效。

  • 实现 parseDurationExtended(string) time.Duration:正则识别 \d+[dhw] → 换算为 time.Hour*24 等,再调 time.ParseDuration
  • 替换 GetDuration 加载点(registry.go GetDuration 共用),main.go:423-426 的四个间隔配置自动受益。
  • 单测:TestParseDurationExtended"2d"==48h"1w"==168h2d12h、复合/无单位、错误输入)+ GetDuration 集成用例全绿。
  • 生产核实:当前生产 core.agent.distill_interval=30m(可解析,非失效态);修复为防御性,未来 2d/1w 写入即可生效。

Phase 4Pipeline Distiller 行为对齐文档(可选,低优) 已完成

  • 改为真正的增量蒸馏:每 tick 取前 N 条(BatchSize,默认 50未蒸馏记录 → extractKeyTriplesCommit移除 RetentionDays 时间门槛——新记录下个 tick 即蒸馏(文档所述 10min 频率),不再等 7 天。
  • 蒸馏失败重试:distillBatch 返回成功标志Commit 失败时记录写回队头,下个 tick 重试(原实现无论成败都移除,会丢数据)。
  • 单测验证启动即蒸馏 + 不重复蒸馏 + batch 分批消化(TestDistillOnceFreshRecords/TestDistillOnceBatchLimit 新增,既有用例回归通过)。

Phase 5嵌入模型内存优化运维侧 已完成(代码层)

  • 提供 量化/裁剪 选项:embedding_model_path 支持 #topN 规格(如 /data/cc.zh.300.vec#top50000),只加载前 N 个词向量fastText 词频降序,前 N 词覆盖绝大多数文本命中);parseModelSpec 解析规格,ensureModelFile/load 均按裁剪路径处理,未命中词走 unkVec 兜底。无规格行为不变。
  • 文档补充:配置项 Description 已写明 #topN 用法与内存预算建议(双模 300 维全量 ≈ 1.5G RAM/模型,top50000 级裁剪可显著降低)。
  • 生产可选降级:仅保留中文模型(主语言)——部署时在 embedding_model_path 只填中文模型或加 #topN 即可,无需改代码。
  • 单测:TestStaticEmbedderTopNSpec(规格解析 + topN 裁剪加载词数)+ TestStaticEmbedderTopNVectorize(裁剪后 unkVec 兜底不空向量)全绿。

Phase 6agentcli 终端通知频率策略QQ 被无视的直接原因) 进行中

背景readLoopinternal/plugins/agentcli/plugin.go:556-608)用写死常量 NotifyOutputDelay = 500msplugin.go:24)做定时节流:只要终端持续输出(如 git sparse clone 进度),就每 500ms 注入一条 [终端 X 有新输出] 到 agent造成无限自喂送、占满 eventLoop使 QQ 消息永远只能塞进回环通道且被 echo 上下文淹没。

原则(明确保留通知,不改中断机制)

  • 通知机制必须保留——agent 需要感知"终端仍在运行、可能有待读取的输出",否则会忘记终端的存在、不知何时去 terminal_read
  • 真正要改的是写死的 500ms 定时节流,改为基于输出语义/任务生命周期的通知策略"多少输出一通知 / 命令执行结束再通知"由可配策略决定,而非插件写死。

实现

  • 通知从"按 500ms 定时"改为按终端生命周期事件触发
    • 进程结束 / 超时 / 读取错误 → 立即通知(已存在 + 增强 EOF 即时触发)。
    • 持续运行、仅产出进度 → 低频"有新输出"通知(累计 notify_bytes 默认 2KB 未读字节,或距上次通知 notify_interval 默认 2s两条件满足任一即触发而非每 500ms。
    • 首次创建 → 立即通知"已启动",确保 agent 感知终端存在。
  • 通知频率可配置per-plugin settingsnotify_bytesnotify_interval),把控制权交还 agent不写死。
  • 纯进度输出仍吸入 t.bufagent 需要时用现有 terminal_read 主动拉全量(保持 agent 可感知存在、可自主决策取量)。
  • 单测mock ptyTerm + capture IOInjector 三用例——TestReadLoopNotifyThrottle3s 持续 2KB/s 吐进度仅 ≤3 条通知,远低于 500ms/条的 6 条)、TestReadLoopNotifyOnExit(进程退出立即通知)、TestReadLoopNotifyOnReadError(读取错误立即通知)全绿。
  • 运维止血:杀掉残留 term_3 bashPID 3716282验证 QQ 消息恢复响应。(生产侧,代码已就绪)

Phase 7中断机制核验确认不需改动仅作记录 已确认

结论:中断机制本身正确,无需改动。文档ARCHITECTURE.md:9,368-384明确

QQ 等外部插件提示走 InjectInterrupt → interruptCh → interceptLoop → 塞 interceptCh内部回环通道→ process() 每轮前 drainInterrupts() 以 [打断消息] 注入当前对话流 —— 同一段 LLM 记忆内连贯处理,不割裂、不另开新回合。

  • 验证回环通道存在:interceptCheventloop.go:85+ drainInterrupts()eventloop.go:404)在 process() 每轮 LLM call 前非阻塞排空注入。
  • 确认设计约束:"不能开新回合,保证 LLM 记忆连贯" —— 中断注入当前对话流QQ 在该语境下 qq_get_message 看消息、回复、接着干。
  • 仅作回归验证:修复 Phase 6 后QQ 消息在 agentcli 不泛滥时能正常经回环通道被响应20:49 已证明机制可达)。

验收标准

指标 当前 目标 验收方式
Graph relations 重复率 ~90% (5613/6225) 0% `SELECT count(*), count(DISTINCT source_id
context_archived 关系残留 5613 0 grep 关系表
distill_interval 配置生效 失效(30m) 2d 日志 heartbeat distill tick 间隔 = 48h
Pipeline distiller 频率 7天一批 10min 日志 distilled N records 每 10min
启动内存占用 2.4G <1.5G(单模)或可配 systemd MemoryCurrent
agentcli 通知注入频率 每 500ms/条 仅在生命周期事件/低频里程碑 日志 input from agentcli 密度显著下降
healthcheck 污染生产存储 每 30min 写 _hc_* 0不落生产存储/虚拟空间) knowledge/memory/_hc_*gotestluatest 残留;快照对比

实施顺序建议

  1. 立即Phase 0.1healthcheck 污染隔离)——正在持续污染生产,最高优先;顺手清理存量 _hc_*/gotest/luatest
  2. 立即Phase 1去重+ Phase 3配置解析—— 互不依赖,风险最低,收益最大。
  3. 次日Phase 2模板清理—— 需确认 Phase 1 生效后,防止旧垃圾再次写入。
  4. 终端洪水紧急项Phase 6agentcli 通知频率策略)——解决 QQ 被无视的直接原因;先运维止血杀残留终端,再上代码。
  5. 后续Phase 4/5 按需求排期。Phase 7 确认中断机制无需改动。

相关文件清单

internal/plugins/healthcheck/plugin.go  # Phase 0.1:自检写入改为隔离空间/不落盘
internal/plugins/agentcli/plugin.go     # Phase 6终端通知频率策略
internal/memory/graph.go           # Commit 去重 + Schema 迁移
internal/agent/core/distill.go     # docToTriples 重构
internal/config/registry.go        # parseDurationExtended
internal/memory/pipeline/pipeline.go # distillOnce 增量化
internal/memory/static_embedder.go # 量化/裁剪入口(可选)

已知遗留问题(非阻塞,记录待后续排期)

  • 内置 WebUI 插件 (internal/plugins/webui/) 设计低劣、Bug 多前端交互廉价、API 不稳定、与核心插件机制契合度差。属于技术债,不在当前核心修复路径Phase 0-7待核心记忆层/通知层稳定后统一重写或剥离。

WebUI 改进计划(参考 NapCat WebUI Design DNA

NapCat WebUI 设计特征ACG 樱花/霜蓝配色 + Glassmorphism 玻璃态 + HeroUI 组件 + Framer Motion 弹簧动效 + Canvas 数据可视化。 HomeAgent WebUI 为 Go 嵌入式 HTML/JS非 React改进方向在原有技术栈内尽可能逼近设计原则,重点提升信息架构、交互反馈、视觉层次。

设计系统适配Go 模板 + 原生 CSS/JS

维度 NapCat 参考 HomeAgent 适配策略
配色 樱花粉 #FF7FAC / 霜蓝 #88C0D0 / 玫瑰红 #F33B7C;粉色调中性色 CSS 变量定义同色系;浅/深色模式切换;主色用于 CTA/聚焦态
字体 Quicksand/Nunito 圆润无衬线 + JetBrains Mono 引入 Google Fonts Quicksand + JetBrains Mono标题 -0.02em tracking
间距 4px 基准单位;卡片 p-4~6区块 gap-4/6 CSS Grid/Flex 统一 4px 节奏;卡片内边距 16/20/24px
圆角 6/8/12px + 胶囊全圆角 --radius-sm:6px --radius-md:8px --radius-lg:12px --radius-full:9999px
玻璃态 backdrop-blur-sm~xl + 半透明白/黑 + 微边框 CSS backdrop-filter: blur(8px) + rgba(255,255,255,0.6) / rgba(0,0,0,0.4) + border:1px solid rgba(255,255,255,0.25)
阴影/层级 软扩散 + backdrop-blur 分层 box-shadow: 0 1px 2px rgba(0,0,0,.05) 低层 / 0 4px 6px rgba(0,0,0,.07) 中层 / 0 20px 25px rgba(0,0,0,.1) 高层
动效 弹簧物理 120-150 stiffness150-400ms CSS transition: 200ms cubic-bezier(.34,1.56,.64,1) 模拟弹簧;页面切换 fade-up+scale
图标 Lucide 2px stroke 引入 Lucide 静态 SVG内联或同风格 iconfont

信息架构重构(核心痛点)

现状 目标(对标 NapCat Dashboard
单页面堆砌所有功能 左侧可折叠侧边栏16rem 固定)→ 导航分组:概览/记忆/工具/插件/配置/日志
无面包屑、无状态反馈 顶部面包屑 + 悬停微动效;关键操作 Toast 反馈(右上角)
表格/列表无视觉分组 卡片网格布局:每卡片 = 一个功能模块(记忆统计/插件状态/工具调用/系统资源)
无数据可视化 Canvas 2D 绘制记忆增长趋势图、CPU/内存环图、工具调用热力图

交互体验对标

场景 NapCat 做法 HomeAgent 改进
卡片悬停 3D 透视倾斜 + 光标跟随渐变光斑 CSS transform: perspective(1000px) rotateX/Y(±5deg) + 伪元素光斑跟随鼠标
按钮点击 弹簧 scale + loading 态 :active { transform: scale(0.97) } + 内置 spinner
页面切换 Framer Motion fade-up+scale stagger CSS @keyframes fadeUpScale + JS 交错延迟 50ms
空状态 插画 + 友好文案 + 引导 每模块空状态统一组件:图标 + 说明 + 主操作按钮
错误处理 Toast 右上 + 破坏性操作确认弹窗 统一 showToast(type, msg) + confirmDialog(action, onConfirm)

实施路线非阻塞Phase 8+

Phase 8.1: ✅ CSS 变量系统 + Glassmorphism 基础样式(浅/深色)— dashboard.html :root 重写 NapCat DNA tokenssakura/frost 色板、玻璃变量、阴影、圆角、字体、动效)
Phase 8.2: ✅ 布局重构 — 左侧 16rem 可折叠侧边栏 + 顶部面包屑 topbar + 卡片网格响应式toggleSidebar/switchTab 联动)
Phase 8.3: ✅ 核心页面卡片化 — 全部 .card 玻璃态 + hover 抬升 + 语义色 badge/dot/按钮;星图/终端/设置面板统一换肤
Phase 8.4: ✅ 交互微动效 — 3D 透视倾斜 + 光标光斑(事件委托 .card.tilt、弹簧按钮 scale、Toast 滑入动画、Loading、tab 切换 fade-up
Phase 8.5: ✅ 数据可视化 — Canvas 记忆分布环图drawDonut 扫掠动画)+ 运行时资源条形图(延迟生长动画)
Phase 8.6: ✅ 空状态/错误/确认弹窗统一组件库 — showToast(type,msg) + confirmDialog(action,onConfirm)Esc/Enter/遮罩关闭)
Phase 8.7: ✅ 无障碍/键盘导航/移动端适配 — :focus-visible ring、prefers-reduced-motion 全停动效、主题滚动条、移动端自动折叠侧边栏
Phase 8.8: ✅ 用户反馈迭代8.x 收尾)— ①健康检查 UI 去冗余(系统操作卡仅保留重载插件,健康检查卡自带右上角「运行」按钮+空态文案);②主题跟随系统(无手动偏好时用 prefers-color-scheme并监听系统实时切换③设置页内容列 max-width 860px 居中④emoji 清理(☰/☀️/🌙/⛔/🔧/🧠 → 内联 SVG/纯文本,聊天工具调用状态用语义色图标);⑤总览页重构为插件页式全宽单列卡片(移除看板娘大照片卡、移除不准确的记忆分布环图+运行时资源条形图runtime/memory 改 kv-row 精确数字展示kernel 页同化

实现均在 internal/plugins/webui/dashboard.html(纯 CSS + Vanilla JS无构建链handler_test.go:641 修复上游遗留断言失配(api('/settings'api("/settings")。

技术约束

  • 保持 Go html/template + 内嵌静态资源 —— 不引入 Node/构建链
  • 静态资源CSS/JS/字体/图标)以 embed.FS 内嵌二进制
  • 复杂动效用纯 CSS + 极简 Vanilla JS无框架依赖
  • 优先修复现有 BugAPI 500、WebSocket 断连、表单提交无反馈)再做视觉

回滚预案

  • Phase 1/2 修改数据库 Schema/写入逻辑:保留 graph.db.backup.*,出问题 systemctl stop homeagent && cp backup graph.db && systemctl start
  • Phase 3 仅改配置解析,回滚即改回 time.ParseDuration
  • 所有改动需先跑 make test(内存/图/文档/配置全绿)再部署。

更新时间2026-08-11 生产实例:/home/newqqagentsystemd 托管,二进制 /usr/local/bin/homed (v0.8.0, 2026-07-28 build)