Files
HomeAgent/plan.md

595 lines
46 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.

# HomeAgent 生产问题修复计划
## 设计意图备忘(核心架构原则)
本框架的两大核心设计意图,贯穿所有插件/记忆/工具设计,**所有改动必须符合**
1. **插件即 Agent 的 App** —— Agent 像人用 App 一样用插件。
- QQ 插件应像 QQ 客户端:通知到来 → 看到预览/上下文 → 一键回复。
- **认知负荷最小**:中断/通知只给「发送者昵称 + 消息预览」,**元数据user_id/group_id/message_id完全走工具不进 prompt**,防提示词注入、防昵称欺诈、低认知负荷。
- **工具语义自解释**`qq_get_message``output_send__qq``qq_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>`,且知识库出现 `gotest``luatest``_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`(用了 `/` 解析)与 `Remove``id=sanitize(name)` 计算**不一致**→ 删除可能失效 → **残留目录固化成文件** |
| `testDocStoreRaw` (:521) | `DocMemory().Insert(...)` | Query 后 Remove | 真实 docStore 写文件再删,抖动 |
**原则**:健康检查验证的是"写入通道是否可用"**结果不应固化进生产记忆**。改为**独立虚拟/影子空间**或**不落盘验证**。
**实现**
- [x] **核心实现**SDK 新增 `VirtualInstance``internal/sdk/selftest.go`healthcheck 的三个 raw 自检改为在完全隔离的虚拟空间(`os.MkdirTemp` 独立图库/知识库/文档/文本)上做真实"写→查→删",绝不碰生产存储。
- [x] `PluginSDK.Selftest()/SelftestReset()` 暴露隔离实例(含 mutex 防并发),每轮自检前 `SelftestReset` 重建清空上轮数据。
- [x] **LLM 驱动自检防护**`collectToolDefsForLLM` 改为只收集**只读白名单**工具(`isSafeReadonlyTool`),写/删/改生产数据及外部副作用工具memory_commit/doc_commit/knowledge_create/cmd_run/files_write/terminal_*/output_send/spawn_child 等)一律不交给 LLM 自检,防止 LLM 乱调污染生产。
- [x] 单测healthcheck 自检后注入的"生产"实例内容不变(快照对比 + `_hc_` 无残留);读/写工具白名单过滤测试通过。
- [x] 存量清理:删除生产残留的 `gotest/``luatest/``_hc_knowledge_test_*/` 目录(保留真实知识库);备份留于 `/tmp/opencode/knowledge_garbage_backup_20260812_122322`
- [x] 代码复核healthcheck 三自检memory/knowledge/doc全部经 `s.Selftest("hc")` 隔离虚拟实例写→查→删生产实例零接触plugin.go:339/466-473/476-549
- [ ] 部署验证:编译新 `homed` 部署后,`knowledge/``memory/graph.db``memory/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_3`bash 的 `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 走 `interceptLoop``cancelLLM` + 塞入 `interceptCh``eventloop.go:84-85`)。
-`process()` 打断后 `continue` 回到**同一回合**`process.go:139`QQ 的 `[打断消息]` 只被追加到幽灵回合对话里夹带响应,**拿不到独立处理回合**。
- 面对 agentcli 持续自喂送QQ 永远排不到前面 → 20:51 之后的 QQ 消息一条未回。
### 止血(运维,立即执行)
- [x] 杀掉残留 `term_3` bash本机 PID 3716282→ 幽灵回声立停QQ 事件循环恢复。
- [x] **QQ 身份注入增强**`third_party/homeagent-sdk/example/qq/plugin.go` 中断模板新增 QQ号/群号(`evt.UserID`/`evt.GroupID`agent 无需先调 `get_message` 即可知发送者身份。
- 后续:`agentcli` 终端用后应 `terminal_close`,避免残留。
### 根治(改代码,入本计划)
- **Phase 6**agentcli 终端通知节流/去重,同源同 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 加唯一约束 + 冲突即跳过,彻底阻断重复累积。
- [x] **Schema 迁移**`initSchema` 新增 `migrateRelationUnique`——检测旧 relations 表无复合唯一约束(旧 DD表自动重建为带 `UNIQUE(source_id, target_id, relation_type, session_id)` 的新表并 `INSERT OR IGNORE` 去重(官方 12 步迁移),无需人工干预。
- [x] **Commit 逻辑**:改为"查存在 → 不存在才 INSERT 并计数;已存在则仅刷新 confidence/updated_at",重复提交不新增、不重计。
- [x] **验证**`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` 摘要当成实体写入图库。
- [x] 重构 `docToTriples`:仅当 `doc.Source``context_archived` 且非空时写 `文档→来源``文档→主题` 仅当 summary 长度合理(<80 且非模板化时写否则跳过
- [x] 引入 `doc.Meta["is_archived_context"]` 标记上下文归档文档 `docToTriples` 识别并跳过`ContextToDoc` source=`context_archived` 时自动打标)。
- [x] 单测验证构造冷文档 `archiveColdDocs` 无模板垃圾产出`TestDocToTriplesArchivedContext`/`TestDocToTriplesTemplateSummary`/`TestDocToTriplesLongSummary`/`TestIsTemplateSummary` 全绿既有 4 docToTriples 用例回归通过)。
---
### Phase 3配置持久化解析修复防配置失效✅ **计划中**
**目标**支持 `2d`/`1w` 等人类可读时长单位配置即时生效
- [x] 实现 `parseDurationExtended(string) time.Duration`正则识别 `\d+[dhw]` 换算为 `time.Hour*24` 再调 `time.ParseDuration`
- [x] 替换 `GetDuration` 加载点`registry.go` `GetDuration` 共用`main.go:423-426` 的四个间隔配置自动受益
- [x] 单测`TestParseDurationExtended``"2d"==48h``"1w"==168h``2d12h`复合/无单位错误输入+ `GetDuration` 集成用例全绿
- [x] 生产核实当前生产 `core.agent.distill_interval=30m`可解析非失效态修复为防御性未来 `2d`/`1w` 写入即可生效
---
### Phase 4Pipeline Distiller 行为对齐文档(可选,低优)✅ **已完成**
- [x] 改为真正的增量蒸馏 tick 取前 N `BatchSize`默认 50未蒸馏记录 `extractKeyTriples` `Commit`**移除 RetentionDays 时间门槛**——新记录下个 tick 即蒸馏文档所述 10min 频率不再等 7
- [x] 蒸馏失败重试`distillBatch` 返回成功标志Commit 失败时记录写回队头下个 tick 重试原实现无论成败都移除会丢数据)。
- [x] 单测验证启动即蒸馏 + 不重复蒸馏 + batch 分批消化`TestDistillOnceFreshRecords`/`TestDistillOnceBatchLimit` 新增既有用例回归通过)。
---
### Phase 5嵌入模型内存优化运维侧✅ **已完成(代码层)**
- [x] 提供 **量化/裁剪** 选项`embedding_model_path` 支持 `#topN` 规格 `/data/cc.zh.300.vec#top50000`只加载前 N 个词向量fastText 词频降序 N 词覆盖绝大多数文本命中`parseModelSpec` 解析规格`ensureModelFile`/`load` 均按裁剪路径处理未命中词走 `unkVec` 兜底无规格行为不变
- [x] 文档补充配置项 Description 已写明 `#topN` 用法与内存预算建议双模 300 维全量 1.5G RAM/模型`top50000` 级裁剪可显著降低)。
- [ ] 生产可选降级仅保留中文模型主语言)——部署时在 `embedding_model_path` 只填中文模型或加 `#topN` 即可无需改代码
- [x] 单测`TestStaticEmbedderTopNSpec`规格解析 + topN 裁剪加载词数+ `TestStaticEmbedderTopNVectorize`裁剪后 unkVec 兜底不空向量全绿
---
### Phase 6agentcli 终端通知频率策略QQ 被无视的直接原因)✅ **进行中**
**背景**`readLoop``internal/plugins/agentcli/plugin.go:556-608`用写死常量 `NotifyOutputDelay = 500ms``plugin.go:24`**定时节流**只要终端持续输出 `git sparse clone` 进度就每 500ms 注入一条 `[终端 X 有新输出]` agent造成无限自喂送占满 eventLoop使 QQ 消息永远只能塞进回环通道且被 echo 上下文淹没
**原则(明确保留通知,不改中断机制)**
- **通知机制必须保留**——agent 需要感知"终端仍在运行可能有待读取的输出"否则会忘记终端的存在不知何时去 `terminal_read`
- 真正要改的是**写死的 500ms 定时节流**改为**基于输出语义/任务生命周期的通知策略**"多少输出一通知 / 命令执行结束再通知"由可配策略决定而非插件写死
**实现**
- [x] 通知从" 500ms 定时"改为**按终端生命周期事件触发**
- **进程结束 / 超时 / 读取错误 立即通知**已存在 + 增强 EOF 即时触发)。
- **持续运行仅产出进度 低频"有新输出"通知**累计 `notify_bytes` 默认 2KB 未读字节或距上次通知 `notify_interval` 默认 2s两条件满足任一即触发而非每 500ms
- **首次创建 立即通知"**已启动**"**确保 agent 感知终端存在
- [x] 通知频率可配置per-plugin settings`notify_bytes``notify_interval`把控制权交还 agent不写死
- [x] 纯进度输出仍吸入 `t.buf`agent 需要时用现有 `terminal_read` 主动拉全量保持 agent 可感知存在可自主决策取量)。
- [x] 单测mock ptyTerm + capture IOInjector 三用例——`TestReadLoopNotifyThrottle`3s 持续 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 记忆内连贯处理**,不割裂、不另开新回合。
- [x] 验证回环通道存在`interceptCh``eventloop.go:85`+ `drainInterrupts()``eventloop.go:404` `process()` 每轮 LLM call 前非阻塞排空注入
- [x] 确认设计约束"不能开新回合保证 LLM 记忆连贯" —— 中断注入当前对话流QQ 在该语境下 `qq_get_message` 看消息回复接着干
- [ ] 仅作回归验证修复 Phase 6 QQ 消息在 agentcli 不泛滥时能正常经回环通道被响应20:49 已证明机制可达)。
---
## 验收标准
| 指标 | 当前 | 目标 | 验收方式 |
| ------ | ------ | ------ | ---------- |
| Graph relations 重复率 | ~90% (5613/6225) | 0% | `SELECT count(*), count(DISTINCT source_id | | target_id | | relation_type) FROM relations` |
| `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_*``gotest``luatest` 残留快照对比 |
---
## 实施顺序建议
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 500WebSocket 断连表单提交无反馈再做视觉
---
## 回滚预案
- 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/newqqagent`systemd 托管,二进制 `/usr/local/bin/homed` (v0.8.0, 2026-07-28 build)*
---
## 9. device_ctl 设备接入网关WebUI 监听 + devicedetect 扫描
### 设计意图备忘(对齐核心架构原则)
1. **插件即 Agent 的 AppIO 全在插件层** —— 设备接入是 IO 能力**必须全部收敛到 webui 插件**内核零 IO 原则
设备状态归 webui 插件管理agent 只经工具访问。**不触碰 CLI/waiter**CLI 本机自执行命令已有 `cmd` 插件反向操控 CLI 属重复造轮子不在本计划范围)。
2. **认知负荷最小 / 反提示词注入** —— 设备元数据dev_id/ip/status只出现在工具参数与返回值**绝不进 system prompt**
agent 只通过 `devicedetect` / `device_ctl_*` 工具按需查询避免设备名/IP 污染对话上下文
3. **工具语义自解释** —— `devicedetect` / `device_ctl_*` Description agent读完即知怎么用」,无需额外指令
4. **显式授权为唯一信任源** —— 任何 device_ctl 执行必须 **先授权、后执行**高危操作cmdrun/open**每次二次确认**授权确认响应中的 accept 字段)。授权状态持久化重启不丢
### 目标(需求澄清)
用户明确收敛为**只做 webui**webui 开监听端口作为设备接入网关提供 `devicedetect` 工具供 agent 扫描设备连接状态
CLI 本机自执行命令已有 cmd 插件不做反向操控 CLI
### 一、协议与接入webui 充当“设备注册网关”)
**背景**当前 webui 是纯 HTTP/SSE 服务`internal/plugins/webui``/api/v1/*` RESTwebui API key 认证+ 聊天 SSE设备接入需要一条**独立受控可被反向推送**的通道
**设计**新增 **JSON-over-WebSocket 监听**agent设备反向推送 + 设备agent 上报双向长连接复用 webui 插件
| # | 内容 | 位置 | 风险 |
| --- | ------ | ------ | ------ |
| A1 | 新增 WebSocket 端点 `/api/v1/device/ws`API key 认证仅连接期有效JSON 消息帧 | `internal/plugins/webui/handler.go` | WS 依赖库 `github.com/gorilla/websocket`判断是否已引入若无则用 `golang.org/x/net/websocket` 或静态协议 |
| A2 | 新增 REST`GET /api/v1/device/online`在线设备列表/状态)、`POST /api/v1/device/push`向已连接设备推 JSON | 同上 | |
| A3 | **设备能力声明**连接时设备上传 `{op:"hello", device:{name,kind,caps:[...]}}`webui 记录并登记在线 | 同上 | |
| A4 | **设备侧授权绑定**首次连接需 `{op:"bind", token}`token webui 设置页生成`device_gateway.token` 配置绑定成功后该设备进入已授权集合 | `plugin.go`(设置) + handler | |
### 二、Agent 工具devicedetect 等)
**设计**注册一个 `devicectl` 设备实现 `agentIO.Device` 接口`internal/plugins/webui` 内定义
`Tools()` 返回三工具`Execute()` 检查授权 + 路由到 WebSocket 在线设备
| 工具 | 语义 | 参数 | 返回值 |
| ------ | ------ | ------ | -------- |
| `devicedetect` | 扫描/列出已连接且已授权的设备 | `kind`(可选) | `[{device_id,name,kind,caps,online}]` |
| `device_ctl_status` | 查询单设备实时状态 | `device_id` | `{device,status,last_seen}` |
| `device_ctl_cmdrun` | 向设备发送命令执行请求**高危需授权+二次确认** | `device_id, command` | `{accepted:true, request_id}` 或拒绝 |
**注意**webui 插件属于内置插件注册 `devicectl` 设备走 `s.RegisterChannel("devicectl", dev)`IOManager 自动并入工具集`ExecuteTool` 自动可达)。设备 Execute **同步阻塞** cmdrun 是异步的——需维护 **pending 请求表request_id → chan**WS 收到设备结果后写回工具循环内超时返回
### 三、webui 前端:设备管理页
| # | 内容 | 位置 | 风险 |
| --- | ------ | ------ | ------ |
| F1 | 侧栏新增设备入口 + 页面设备列表在线/离线/已授权/未授权)、连接状态徽标 | `dashboard.html` | 纯前端 |
| F2 | 设备详情基本信息名称/种类/caps)、状态/历史授权/取消授权按钮`device_ctl_cmdrun` 命令输入+结果展示 | 同上 | |
| F3 | 设备接入引导显示 `device_gateway.token` + 接入方式说明WS URL + 绑定 token供外部设备复制 | 同上 | token 明文展示显示/隐藏」) |
### 四、安全与授权
| # | 内容 | 位置 | 风险 |
| --- | ------ | ------ | ------ |
| S1 | `device_gateway.token`启动时生成并持久化复用 `Settings()` 机制webui 插件设置页可显示/重置」) | `plugin.go` / handler | |
| S2 | **授权级别**只读status/detect无需确认**执行类cmdrun需二次确认**——工具返回 `{accepted:false, require_confirm:true}`agent 需再调 `device_ctl_confirm` 或经 webui 前端用户点击允许 | handler + 工具 | |
| S3 | **授权状态持久化**已授权设备集合存 webui 插件配置SQLite config.db复用 Settings重启不丢 | 同上 | |
| S4 | **WS 连接安全**连接即要求 API key`Sec-WebSocket-Protocol` query token每消息帧校验超时/断开自动清理在线表 | handler | |
### 五、实施顺序(本计划按此逐步 push每步可独立验证
| Phase | 内容 | 验证 |
| ------- | ------ | ------ |
| **P1** | 协议与接入新增 WS 端点 + hello/bind 握手 + 在线设备登记内存 | curl/WS 客户端连上 `GET /api/v1/device/online` 可见 |
| **P2** | REST `/api/v1/device/online``/api/v1/device/push`、(device_gateway.token 设置注册 | token 生成/持久化push 到在线设备收到 JSON |
| **P3** | Agent 工具`devicectl` Device + `devicedetect` / `device_ctl_status` / `device_ctl_cmdrun` | agent 工具面板可见三工具devicedetect 返回在线设备 |
| **P4** | 异步 cmdrunpending 请求表 + WS 结果回写 + 超时 | 模拟设备返回 cmdrun 结果agent 拿到 |
| **P5** | 授权bind 绑定 + token 校验 + 持久化已授权集合 + cmdrun 二次确认流 | 未绑定拒绝已绑定可查询cmdrun 需确认 |
| **P6** | webui 前端设备管理页F1F3 | 页面可见在线/授权/执行状态 |
| **P7** | 单测 + 文档handler/工具/授权单测README/架构文档补充 | `make test` 全绿 |
### 六、里程碑外延(明确不做)
- **不做 CLI/waiter 反向操控**本机命令已由 cmd 插件承担预期语义是设备本来就是远程的”)。
- 不做 QQ/微信等具体设备插件——设备按通用 WS 协议接入即可
- 单设备回调/事件推送的复杂路由多设备扇出订阅过滤留给后续迭代
### 七、回滚预案
- P1P5 均为新增代码/路由不触碰现有 webui 路由与 CLI socket既有功能完全不受影响
- WS 依赖库引入失败回退为**独立 TCP 监听**复用 cli 插件逐行 JSON 模式协议一致)——同样满足设备接入网关”。
- 所有改动先 `make build build-cli` + `make test` 后再部署生产回滚即替换旧 `homed` 二进制
> **更新2026-08-16**用户定案——device 接入独立为 `remotedevice` 插件webui 只做前端反代,如 `proxyToPluginmgr` 先例;不把 WS 监听直接长在 webui 插件上,避免 webui 过重)。本插件的所有实现细节沿用上述设计,落点全部移到 `internal/plugins/remotedevice/`
>
> - 设备网关WS 监听 + hello/bind/在线登记)→ remotedevice 自持监听端口(默认 127.0.0.1:9890配置 `listen_addr`
> - RESTonline/push→ remotedevice 自带 HTTP mux地址同上
> - Agent 工具devicectl Device + devicedetect/device_ctl_*)→ remotedevice 内 `s.RegisterChannel("devicectl", dev)`
> - 设置项listen_addr / ws token / 已授权集合)→ remotedevice 插件 Settingsconfig_remotedevice 表)
> - webui 前端设备页 → webui `/api/v1/device/*` **可选反代**到 remotedevice参照 `proxyToPluginmgr` 模式):**默认禁用**,用户配置 `device_gateway_enabled` / `device_gateway_addr` / `device_gateway_token` 后才挂载,避免硬耦合
> - 设备网关鉴权webui 层 API key`requireAPI`+ 网关层 remotedevice token`X-API-Key`)双鉴权
## 10. 实施remotedevice 独立插件(逐步推进)
### Phase 0插件骨架 + 设备注册表 + WS 网关(本轮)
### Phase 0插件骨架 + 设备注册表 + WS 网关 + devicectl 工具(已实施)
**已交付**
- `internal/plugins/remotedevice/` 独立插件`registry.go`设备注册表 + 标准库 WebSocket 网关 + push/await/结果留档)、`plugin.go`设置 + REST + HTTP 服务)、`device.go`devicectl Device + 四工具
- Agent 工具`devicedetect` / `device_ctl_status` / `device_ctl_cmdrun` / `device_ctl_cmdresult` `s.RegisterChannel("devicectl", dev)` 并入 IOManager 工具集
- 设备接入WS 端点 `/api/v1/device/ws`token 认证hello/bind/status/cmd_result 协议默认监听 127.0.0.1:9890
- REST 管理面`/api/v1/device`列表)、`/api/v1/device/online``/api/v1/device/{id}``/api/v1/device/push``/api/v1/device/auth`全部 token 鉴权
- 授权token 校验`ws_token`启动生成持久化+ 已授权集合持久化`authorized_devices`逗号分隔+ `RestoreAuthorized` 重启恢复
- webui 可配置反代新增 `device_gateway_enabled`(默认 false)/`device_gateway_addr`/`device_gateway_token` 设置仅启用时挂 `/api/v1/device/` 反代路由webui API key 鉴权 转发带 remotedevice token
- 装配`internal/plugins/all.go` 注册 remotedevice
**验证**`gofmt -w` + `go build ./...` exit:0
**待办(后续 Phase**
- [] 单测注册表/WS 握手/push/cmdrun/授权
- [] webui 前端设备管理页非必选可经 REST/CLI 使用
- [] 心跳/离线自动清理的周期 goroutine当前断开即清理
### Phase 0bGUI 连接 remotedevice已实施
**需求**本机有 GUIElectron为其添加连接 remotedevice 网关的逻辑可查看/授权/控制设备
**已交付**cmd/gui/
- **连接类型**表单新增 `device`设备网关 remotedevice类型`saveConnForm` device 分支url+apiKey=ws_token连接测试加 device 探活GET /api/v1/device X-API-Key
- **normalize 修复**`main.js normalizeConnections` 允许 `type==="device"`原来会强制改回 webui
- **侧栏设备入口** + `view-devices` 容器index.html
- **renderDevices()**设备列表名称/种类/在线/授权/能力)、空态引导、"执行命令"prompt 输入POST /device/push)、"授权/取消授权"POST /device/auth)、"刷新"
- `renderAll`/`refreshAll` 挂载 renderDevicesstate.devices
**验证**`node --check renderer/app.js` `node --check main.js` 全通过`go test ./internal/plugins/... ./internal/sdk/... ./internal/agent/...` 全绿remotedevice 端到端WS 设备接入 REST 列表/online/鉴权/push验证通过
**设备模拟全链路**验证实录
- WS /api/v1/device/ws?token=... hello_ack bind_ack 服务端 push {op:cmd, command} 设备回 cmd_result
- GET /api/v1/device 返回全部设备合并授权态GET /api/v1/device/online 只返回在线 token 401
- GUI "执行命令" /device/push 已验证下发到达设备
### Phase 0c端到端验收已完成
**环境**临时 homed-data /tmp/ha-dev+ Python WS 设备模拟 + Electron GUI 冒烟
**验证结果**
- `go build ./...` exit:0`go test ./internal/plugins/... ./internal/sdk/... ./internal/agent/...` 全绿
- remotedevice 网关监听 127.0.0.1:9890config_remotedevice listen_addr/ws_token/authorized_devices 持久化
- WS 设备接入全链路hello_ack bind_ack 服务端 push {op:cmd} 设备回 cmd_result
- RESTGET /api/v1/device含授权态合并)、/online仅在线)、 token 401
- GUI`node --check renderer/app.js` + `node --check main.js` 通过Electron 冒烟--disable-gpu稳定运行无 JS 错误headless 容器需禁 GPU
- 说明webui 反代默认禁用device_gateway_enabled=falseGUI 直连 remotedevice 网关不受影响
**遗留/后续**
- [ ] remotedevice 单测registry/WS 握手/push/授权
- [ ] agent 真实工具调用验证 LLM key本环境无
- [ ] webui 设备管理页可选GUI 已覆盖
- [ ] cmdrun 二次确认流当前为已授权即下发
### Phase 0d真实 LLM 驱动工具调用测试(完成 ✅)
**环境**本机 LLM 网关 `http://127.0.0.1:8080/v1`deepseek-v4-flash-freeBearer key+ 临时 homed + Python WS 模拟设备living-light 客厅灯已授权在线)。
**配置**`core.llm.base_url/api_key/model=AUTO/adapter=openai` + `core.llm.sources.default.*`重启后 `model=AUTO base=http://127.0.0.1:8080/v1 sources=2`
**测试1devicedetect扫描设备**
> 用户指令:"请扫描一下当前有哪些设备在线用devicedetect工具"
> Agent 真实调用 devicedetect → 返回客厅智能灯living-light类型/状态/授权/能力/最近在线时间 ✓7.8s
**测试2device_ctl_cmdrun反向操控设备—— 决定性验证**
> 用户指令:"用device_ctl_cmdrun让客厅灯living-light执行 turn_on 命令打开灯"
> 设备模拟日志:`PUSHED: {command: turn_on, op: cmd, req_id: f00dd0596ffeee2d}` → 设备回 `cmd_result`
> Agent 回复:"开灯指令已成功下发执行status: ok请求 ID f00dd0596ffeee2d" ✓16.6s
**结论**HomeAgent LLM 决策 devicectl 工具 WS push 设备执行 cmd_result 回执 agent 汇报 全链路真实跑通"agent 反向操控设备"核心能力已验证
**清理**已终止临时 homed / 模拟设备 / 临时文件代码改动与 plan.md 保留
### Phase 0eGUI 作为设备接入 + 托盘驻留 + 偏好设置 + 聊天卡顿修复(已实施)
**设备桥GUI 作为设备)**
- `cmd/gui/main.js` 新增设备桥GUI `device_id: gui-<hostname>` 接入 remotedevice WShello/bind收到 `{op:cmd}` `spawn` 本机执行白名单命令 + 15s 限时 + 8KB 截断 `cmd_result`
- whenReady 时若存在 `type="device"` 连接则自动启动设备桥before-quit 停设备桥
**托盘驻留**
- `initTray()`nativeImage icon + 菜单显示主界面/退出+ 双击显示whenReady 调用
- `window-all-closed` `exitToTray` 偏好true 则隐藏驻留后台维持设备桥false quit
- `window:close` handler 改为 exitToTray hide修复了托盘块重复注册崩溃
**偏好设置prefs:get/set + 设置页 UI**
- `gui-prefs.json`userData `autoLaunch`开机自启Electron `app.setLoginItemSettings` + openAsHidden/ `silentStart`静默启动createWindow hide/ `exitToTray`退出进托盘默认 true
- preload 暴露 `homeagent.prefs`设置页客户端偏好卡片三个开关
**聊天卡顿/输入框卡死修复**
- 根因webui 连接 sendChat POST `/chat`同步等 60s 完整结果 SSE 流式agent_output 增量**双通道重复**回复长时反复全量 innerHTML 重建 + marked.parse 占满主线程 UI 卡死输入冻结
- 修复webui **触发式 POST**15s 短超时确认受理回复靠 SSE 流式增量渲染cli/device SSE 保持同步等完整结果
- `connectSSE` 跳过 device设备网关无 chat/eventssendChat device 连接提示不支持聊天
- 流式增量渲染`renderChatStreamChunk` 90ms 防抖 + 200字符/300ms 节流 parse保留输入框 DOM 不被重建
**验证**node --check main.js/preload.js/app.js 全通过Electron 冒烟--disable-gpu --in-process-gpu稳定运行无 JS 错误go build ./... exit:0go test plugins 全绿
**待后续**screenuseGUI 拉起窗口显示信息规划中cmdrun 二次确认流remotedevice 单测
### Phase 0fdeviceinfo 工具 + GUI 被控端能力声明(已实施并真实验证)
**deviceinfo 工具agent 探查设备详情+能力)**
- `internal/plugins/remotedevice/device.go`新增第五个工具 `deviceinfo`参数 device_id返回设备接入时声明的 infohostname/platform/arch/cpus/mem/版本+ caps 能力列表需已授权
- `registry.go``DeviceMeta` 新增 `Info map[string]interface{}``metaFromMsg` 解析 hello `device.info``publicDevices` 输出带 info
**GUI 被控端能力**
- `cmd/gui/main.js` 设备桥 hello 声明 `caps: ["status","cmdrun","deviceinfo","cmdresult"]` + `info`本机 hostname/platform/arch/cpus/totalmem/node/electron 版本
**真实验证(模拟 GUI 设备 + 本机 LLM 网关)**
> 用户指令:"用deviceinfo探查 gui-testhost 这台设备的信息和它支持什么能力"
> Agent 调 deviceinfo → 返回基本信息device_id/名称/类型/平台/主机名/在线授权、硬件8核/16G/Node v22/Electron 33、能力status/cmdrun/deviceinfo11.7s
**遗留**screenuseGUI 拉起窗口显示规划中cmdrun 二次确认流remotedevice 单测
### Phase 0gwaiter CLI 设备桥(已实施并端到端验证)
**实现**cmd/waiter/
- `device.go``deviceBridge` Go 标准库 WS 客户端)——握手//hello/bind/readLoop/execCommand白名单命令 + 15s 超时 + 8KB 截断cap 声明 status/cmdrun/deviceinfoinfo 上报 hostname/platform/arch/cpus/mem_mb
- `config.go`Config `device_gateway` + `device_token`waiter.yaml
- `main.go``--device` / `--device-token` 启动设备桥或读配置
**端到端验证(真实执行)**
- `waiter -device 127.0.0.1:9890 -device-token ...` `device bridge active: waiter-jianf-Station`
- remotedevice 登记waiter-jianf-Station / HomeAgent CLI (waiter) / computer / caps[status,cmdrun,deviceinfo] / info{arch:amd64,cpus:32,mem_mb:23129,hostname:jianf-Station} authorized online
- **device_ctl_cmdrun**"用device_ctl_cmdrun让 waiter-jianf-Station 执行 uname -a" agent 调工具 waiter 本机 exec 返回 `Linux jianf-Station 6.12.101+deb13-amd64 ... x86_64 GNU/Linux` ✓(9.2s
- **deviceinfo**"用deviceinfo查看 waiter-jianf-Station" 32 / 22.6GB / Linux amd64 / 三能力 ✓(11.3s
**修复的关键 Bug**9890 被残留进程占用导致新 homed remotedevice 监听失败acceptBind 用旧 token 401杀残留后恢复正常
**验证环境**临时 homedLLM 网关+ 新编译 waiter全量 go build/test 通过仅剩 2 个与改动无关的既有 system 失败)。
### Phase 0h设备默认不授权用户手动授权已实施并真实验证
**需求**GUI waiter 设备默认不授权必须用户手动授权
**修复registry.go bind**
- bind 原无条件 `SetAuthorized(id, true)`每次重连/心跳都自动授权撤销被覆盖)→ 改为**仅验证 token + 登记设备绝不自动授权**
- 授权完全由用户手动控制GUI 本机卡片/设备页授权本机按钮 REST `/api/v1/device/auth` waiter 用户手动操作
- 新增设备接入即 `authorized:false`agent device_ctl_* 默认拒绝
**配套修复cmd/gui/main.js**
- 设备桥选连接优先 `currentId`否则最后一个 device 连接避免列表里旧连接旧 token 抢先 401
- 清理 connections.json 残留的旧 device 连接
**真实验证GUI 被控端 + 本机 LLM 网关)**
- GUI 接入后 `gui-jianf-Station authorized: **False**` ✅(默认不授权
- **手动授权后** agent `device_ctl_cmdrun echo AUTH_OK` 返回 AUTH_OK
- **撤销授权后** agent `device_ctl_cmdrun` 返回 "device gui-jianf-Station 未授权无法执行命令" ❌;本地 cmd_run 兜底成功本机能力合理
- agent 智能未授权时自动改用本地 cmd_run 并提示"去设备管理页授权"
**结论**GUI/waiter 被控设备默认不可被 agent 远程操控用户手动授权后才可撤销即时生效
### Phase 0iGUI 视觉修复(圆角 / 字体方框 / 托盘图标 / 授权开关融合)
**托盘图标**cmd/gui/
- 生成 `icon-tray.png`(22x22) + `icon-tray@2x.png`(44x44)ImageMagick icon.svg 转换
- `initTray` 平台化Linux PNGico/svg Linux 托盘不受支持)、Windows icoLinux resize 22x22 兜底
**窗口圆角**cmd/gui/
- `createWindow` `transparent: true` + `roundedCorners`透明帧圆角窗口
- CSS`body` 透明 + 18px 圆角`#app` 18px 圆角 + 背景背景移入容器内裁切
**设备页字体/方框修复**renderer/style.css
- table 10px 圆角 + collapse + hoverth 加粗kv-row .val flex 对齐
- `.switch input` `-webkit-appearance:none` + 背景透明消除 checkbox 浅蓝方框
**授权开关融合**renderer/app.js
- 本机授权改为 **kv-row 信息行内嵌滑动开关**key=授权val=开关+状态圆点与设备ID/网关/状态行同构,消除割裂
- 设备列表"执行命令"按钮替换为**授权开关**checked=已授权)
- 修复 onchange `JSON.stringify(id)` 双引号冲突 单引号转义 `deviceToggleAuth('id',this.checked)`
**验证**CDP 计算样式body/app 18px 圆角可见card 14pxtable 10pxswitch input 背景透明CDP 模拟点击授权开关 authorized true 持久化agent device_ctl_cmdrun 操控 GUI 成功
### Phase 0j白屏根因定位 + 逐步安全加回(已完成)
**白屏根因(二分定位确定)**新版 main.js 顶层 `const { Tray, Menu: ElectronMenu, nativeImage } = require("electron")` asar/无托盘环境加载异常)→ 导致 renderer 合成卡死窗口全灰白无绘制设备桥/新版 renderer 本身安全混合测试证明)。
**修复**托盘改**函数内惰性 require + try/catch 安全降级**失败不动托盘不影响窗口)。
**逐步加回验证**每步实测窗口字节 70KB 正常
- F3 main 主体 + 设备桥 + handlers + 新版 renderer可用基线
- Step1安全托盘惰性 require)— 显示正常
- Step2完整 prefsGUI_PREFS_FILE 持久化 / loadGuiPrefs / applyAutoLaunch / silentStart / 开机自启)— 显示正常
- Step3生命周期window-all-closed 退出进托盘依偏好before-quit 清理托盘/设备桥)— 显示正常
**最终版已安装**/opt/HomeAgentmd5 047f2f1bbe备份 /tmp/ha-app-step3-final.asar设备桥 / 设备页+授权开关 / 惰性托盘 / prefs 持久化 / 退出进托盘 / 字体/圆角renderer)。