Files
HomeAgent/docs/ARCHITECTURE.md
root 2d314b3e9c 重构: 插件自注册 + .so 动态加载 + 中断打断机制
- 所有内置插件 init() 自注册 (plugin.RegisterFactory), 移除 main.go 硬编码
- 新增 .so 动态加载器 (internal/plugin/dynamic.go), 插件可编译为 plugin.so
- 新增 plugin.json 元数据 (internal/plugin/manifest.go)
- 新增 interceptLoop 独立 goroutine:
  (a) cancelLLM() 取消进行中的 HTTP 请求
  (b) interceptCh → drainInterrupt() 注入 [打断消息] 到 LLM 上下文
  (c) InjectInput 空闲时触发新处理循环
- 新增 internal/plugins/all.go 空白导入触发所有内置插件 init()
- internal/sdk/ 作为 PluginSDK 正式 Go API
- internal/api/ → internal/plugins/webui/ 迁移
- 删除旧 cmd/cli/, 使用 cmd/waiter/ 替代
- 更新 PLAN.md / ARCHITECTURE.md / README.md 文档
2026-07-03 16:53:34 +08:00

774 lines
36 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 架构设计 v4
## 一、核心理念
24 小时陪伴用户的智能管家。**单会话·单 Agent**,身份不漂移。
### 设计原则
- **核心零 IO** — Core 没有任何硬编码 IO 能力,所有 IO 来自插件
- **所有输出是工具调用** — Agent 必须显式调用 `output_send` 才能通信,推理不自动路由
- **所有 LLM 调用走 Provider 接口** — 不直连 API
- **DeepSeek v4 flash** 为默认 LLMthinking 模式关闭
- **人格固定**personal.md记忆分层管理防止性格突变
- **知识独立于记忆**agent 主动学习
- **插件 = 三通道**:工具、阶段钩子、事件订阅
---
## 二、核心域 vs 插件域
```
┌──────────────────────────────────────────────────────────────────┐
│ 核心域 (Core Domain) │
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌───────────────────┐ │
│ │ Provider │ │ Memory │ │Knowledge │ │ Pipeline Stages │ │
│ │ (LLM) │ │ (T/D/G) │ │ (TF-IDF) │ │ 编排器 │ │
│ └──────────┘ └──────────┘ └──────────┘ └───────────────────┘ │
│ ┌──────────┐ ┌──────────┐ ┌──────────────────────────────────┐ │
│ │ Relevance│ │ Context │ │ Plugin Host │ │
│ │ Tf-Idf │ │ Persist │ │ (调用钩子 + 路由工具) │ │
│ └──────────┘ └──────────┘ └──────────────────────────────────┘ │
│ 核心无任何 IO 能力 │
├──────────────────────────────────────────────────────────────────┤
│ 边界 (Plugin API) │
│ ┌──────────────────────────────────────────────────────────────┐ │
│ │ RegisterTool(name, handler) ← 插件注册工具给 LLM │ │
│ │ RegisterStage(stage, handler) ← 插件挂入消息处理阶段 │ │
│ │ Subscribe(eventType, handler) ← 插件订阅系统事件 │ │
│ │ Publish(event) → 插件发布事件 │ │
│ │ Settings().Get/Set/List ← 读写核心/插件配置 │ │
│ │ Memory().Recall/Commit ← 图记忆访问 │ │
│ │ Knowledge().Search/Create ← 知识库访问 │ │
│ └──────────────────────────────────────────────────────────────┘ │
├──────────────────────────────────────────────────────────────────┤
│ 插件域 (Plugin Domain) │
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────────────┐ │
│ │ WebUI │ │ QQ │ │OutputBus │ │ 未来插件: …… │ │
│ │ HTTP/WS │ │ OneBot │ │通道管理 │ │ │ │
│ └──────────┘ └──────────┘ └──────────┘ └──────────────────┘ │
│ 所有 IO 都在这里 │
└──────────────────────────────────────────────────────────────────┘
```
### 核心职责
- LLM 调用编排Provider → Agent 工具循环)
- 三层记忆管理Context → Document → Graph
- 知识库维护Knowledge Store
- 阶段管道编排Stage Pipeline
- Context 相关性管理TF-IDF 余弦相似度)
- 心跳蒸馏 + 图重整
### 插件职责
- 接收外部输入WebSocket、HTTP、硬件等
- 提供输出能力(文本发送、文件传输等)
- 干预消息处理流(阶段钩子)
- 观察系统状态(事件订阅)
---
## 三、消息处理阶段管道
```
┌──────────────────────────────────────────┐
│ on_input │
│ 消息到达Agent 未做任何处理 │
│ └→ 插件可鉴权/拉黑/改写/短路回复 │
└──────────┬───────────────────────────────┘
│ 通过
┌──────────▼───────────────────────────────┐
│ 内部Context Append + Memory Recall │
│ + Context 组装 │
└──────────┬───────────────────────────────┘
│ 就绪
┌──────────▼───────────────────────────────┐
│ pre_action │
│ 上下文已就绪,即将调用 LLM │
│ └→ 插件可注入 system 消息 / 修改 context │
└──────────┬───────────────────────────────┘
│ LLM 调用
┌──────────▼───────────────────────────────┐
│ post_action │
│ LLM 返回文本 + 工具调用列表 │
│ └→ 插件可审查/修改文本、增删工具调用 │
└──────────┬───────────────────────────────┘
│ 判断有无工具调用
╱─────────────┴─────────────╲
有工具调用 无工具调用
│ │
┌──────────▼──────────────┐ │
│ before_toolcall │ │
│ 即将执行某个工具调用 │ │
│ └→ 插件可拒绝/放行/ │ │
│ 修改参数/审计 │ │
└──────────┬──────────────┘ │
│ 执行工具 │
┌──────────▼──────────────┐ │
│ after_toolcall │ │
│ 工具执行完毕,准备喂回 │ │
│ └→ 插件可脱敏/改写结果 │ │
└──────────┬──────────────┘ │
│ 回到 post_action 继续循环 │
└──────────────────────────────┘
┌──────────────────────────┘
┌──────────────────────────────────────────┐
│ before_output │
│ 最终文本就绪,即将调用 output_send │
│ └→ 插件可改写回复/添加格式/适配渠道 │
└──────────────────┬───────────────────────┘
│ output_send 调用
┌──────────────────▼───────────────────────┐
│ after_output │
│ 输出完成 │
│ └→ 记录/统计/清理资源 │
└──────────────────────────────────────────┘
```
### 7 个阶段总表
| 阶段 | 触发时机 | 插件读写权限 | 典型用途 |
|---|---|---|---|
| `on_input` | 消息到 Agent零处理 | 可读写 `raw_message`,可设置 `response` 短路 | 黑名单、限流、自定义指令前缀 |
| `pre_action` | Memory+Context 就绪LLM 调用前 | 可读写 `context_messages`(追加/修改) | 注入 RAG 结果、插入时政 context |
| `post_action` | LLM 返回文本 + 工具调用列表 | 可读写 `llm_text``tool_calls``context_messages` | 敏感词过滤、强制 redirect 工具 |
| `before_toolcall` | 单个工具调用执行前 | 可读写 `tool_call.name``tool_call.args`,设置 `deny=true` 拒绝 | 审计高危操作、OS 命令白名单 |
| `after_toolcall` | 单个工具执行完毕 | 可读写 `tool_result` | 脱敏数据库结果、排序搜索结果 |
| `before_output` | 最终文本就绪output_send 前 | 可读写 `final_text`,可设置 `skip_output=false` | 添加表情/at 前缀、多平台格式适配 |
| `after_output` | output_send 已调用 | 只读 `final_text` | 统计日志、触发后续流程 |
### 循环规则
`post_action → before_toolcall → after_toolcall → 回到 post_action` 构成**内循环**。Agent 在以下条件退出循环进入 `before_output`
- LLM 返回纯文本(无工具调用)
- `before_toolcall` 拒绝所有剩余工具且 LLM 无可执行工具
- 循环超过 `max_tool_rounds` 上限
### 短路规则
每个阶段插件都可设置 `ctx.Response`,一旦设置管道立即短路到 `after_output`
```
on_input → ctx.Response = "hello" → 跳过后面的所有阶段 → after_output
```
---
## 四、记忆体系(三层递进)
```
输入消息
┌─────────────────────────────────────────────────────┐
│ Layer 1: Context (RelevanceContext) │
│ 内存中维护最近 topK 条事件TF-IDF 评分JSON 持久化 │
│ 每次 Append/Prune → save() 防崩溃丢数据 │
│ keep=30 条活跃,多余 → 归档到 Document │
└────────────────────┬────────────────────────────────┘
│ Prune 时
┌─────────────────────────────────────────────────────┐
│ Layer 2: Document (document.Store) │
│ 文件系统 JSON + TF-IDF 向量索引 │
│ 冷文档72h 未访问 + access ≤ 2→ 蒸馏到 Graph │
│ 也可以由用户主动 commitdoc_commit 工具) │
└────────────────────┬────────────────────────────────┘
│ reorg 心跳
┌─────────────────────────────────────────────────────┐
│ Layer 3: Graph (GraphDB + Indexer) │
│ SQLite: entities + relations │
│ Entity: name, type, mention_count │
│ Relation: source → target, relation_type, confidence│
│ 搜索: 关键词 → 向量搜索实体 → BFS 遍历邻居 │
│ 蒸馏: 原始记录 → Distiller → 三元组提交 │
└─────────────────────────────────────────────────────┘
```
### 数据流关系
```
Context 修剪 → Document 归档 → reorg 心跳 → Graph 消化
Distiller (原始记录 → 三元组)
```
### 工具入口Agent 暴露给 LLM
- `memory_recall(query)` → 从 Graph 召回
- `memory_commit(triples)` → 写入 Graph
- `memory_introspect()` → 查看统计
- `doc_query(query)` → 从 Document 搜索
- `doc_commit(title, content)` → 写入 Document
---
## 五、知识体系
独立于记忆agent 主动学习。
```
knowledge/<name>/
content.md
knowledge.Store
└─ TF-IDF 向量索引 (character bigram)
└─ 独立于 memory 的 vector.Store 实例
└─ Start() 时扫描目录训练索引
└─ Add(name, content) 时增量更新
```
### 工具入口
- `knowledge_search(query)` → 向量搜索
- `knowledge_create(name, content)` → 新增
- `knowledge_list()` → 列出所有
### 为什么独立于 memory
- Memory 是 LLM 的"对话记忆"——谁说过什么、上下文
- Knowledge 是 LLM 的"知识库"——外部注入的固定知识
- 两者 TF-IDF 索引实例隔离,不互相污染
---
## 六、三通道插件交互
```
插件 ──→ 核心 核心 ──→ 插件
──────────────────────────────────────────────────
RegisterTool(name, fn) ──→ buildToolDefs()
executeToolCall() → fn
Tracker 自动包裹 Pre/PostAction
RegisterStage(stage, fn) ──→ runStage() 在对应阶段调用 fn(ctx)
返回后检查 ctx.Response 决定是否短路
Subscribe(eventType, fn) ──→ Publish(event)
所有订阅者收到(观察型)
```
### 通道对比
| 通道 | 方向 | 用途 | 可否拦截 |
|---|---|---|---|
| **工具** (RegisterTool) | 插件→核心→LLM | LLM 主动调用插件功能 | 否 |
| **阶段** (RegisterStage) | 核心→插件 | 核心触发插件干预消息流 | 是response 短路) |
| **事件** (Subscribe/Publish) | 双方向 | 审计/日志/状态通知 | 否 |
---
## 七、Agent 内部完整流程
```
processTextInput(input)
├── on_input stage ────────────── 插件可拦截/改写
├── context.Append(input)
├── context.Prune(input) → 归档到 Document
├── buildMemoryContext() → Indexer.BuildContext → Graph Recall
├── pre_action stage ──────────── 插件可注入 context
├── [循环] process(input)
│ ├── buildSystemPrompt (人格+记忆+技能+上下文)
│ ├── buildToolDefs (内置工具 + 插件工具)
│ ├── provider.Chat() → LLM
│ │
│ ├── post_action stage ─────── 插件可见 LLM 输出 + 工具列表
│ │
│ ├── 有工具调用?
│ │ ├── 每个工具:
│ │ │ ├── before_toolcall stage ── 插件可拒绝/改参
│ │ │ ├── Tracker.PreAction
│ │ │ ├── executeToolCall() ──── 路由到插件或内置
│ │ │ ├── Tracker.PostAction
│ │ │ └── after_toolcall stage ── 插件可改结果
│ │ └── → 回到 post_action (继续循环)
│ │
│ └── 无工具调用 → 退出循环
├── context.Append(response)
├── before_output stage ───────── 插件可改写最终文本
├── Publish(agent_output event)
├── output_send (调用插件注册的 output 工具)
└── after_output stage ────────── 插件只读,做统计/日志
```
---
## 八、记忆整理(心跳 LLM 驱动消歧)
图数据库在长期运行中会积累**同义实体**(如「张三」与「张先生」指同一人)和**矛盾关系**。心跳流程如下:
### 流程
```
心跳 tick (30min)
├── distillContext() — 蒸馏上下文
├── syncGraphToDocs() — 图→文档
└── reorgGraph()
├── Indexer.Sync() — 图→向量(自动)
├── DocStore.Reindex() — 文档重建索引(自动)
├── 冷文档→图归化 — 将冷文档归档为图三元组(自动)
└── 实体冲突检测 → 发现相似实体对
│ 如:「张三」(person, 5次) vs 「张先生」(person, 3次) 相似度 0.75
enqueueConsolidationTask()
│ 通过 IO 层注入 Agent 输入队列
│ channel = "_consolidation_"(内部通道,不对外输出)
Agent 处理 (processConsolidation)
│ 如同普通用户消息,走完整 LLM 工具循环
│ 但输出仅写记忆,不发外部通道
LLM 决策:
├─ 判断为同一实体 → 调用 memory_merge 合并
│ → "已将「张先生」合并到「张三」3 条关系已重定向"
├─ 判断为不同实体 → 回复"跳过"
└─ 不确定 → 回复"待定,需更多上下文"
```
### 关键设计
| 特性 | 说明 |
|---|---|
| **启发式检测LLM 决策** | bigram Jaccard 仅做候选筛选(低门槛 0.5LLM 做最终判断 |
| **走 IO 输入队列** | 不阻塞心跳,不抢占用户输入,享受完整 Agent 上下文 |
| **`_consolidation_` 通道** | 内部专用通道,输出只写记忆层,不被外部插件路由 |
| **`memory_merge` 工具** | LLM 通过此工具执行合并,自动重定向关系 + 累积 mention_count |
| **异步非阻塞** | 整理任务排队在 inputCh 尾部Agent 按序处理,不影响用户体验 |
### 类比
类似人类睡眠时大脑的海马体回放——白天经历的记忆在休息时被自发整理、关联、去重。HomeAgent 的心跳就是它的"睡眠周期",而 LLM 的参与相当于前额叶皮层执行语义判断。类比:
```diff
- 人类: 白天经历 → 海马体暂存 → 睡眠 → 前额叶整理 → 长期记忆
+ Agent: 用户交互 → Context缓存 → 心跳 → LLM 消歧 → GraphDB 存储
```
---
## 九、Child Agent
不走阶段管道,独立轻量 Agent
```
spawn_child(task) → 新建轻量 Agent
├── 独立 system prompt仅有任务描述
├── 仅 output_send 工具
├── 无 persistent memory
├── 无 Graph/Document 访问
├── 上限 5 轮工具循环
└── 销毁时返回结果文本
```
---
## 九、LLM Provider 与 Lua 适配层
LLM 调用全部通过 `Provider` 接口,核心实现是 `LuaAdaptedProvider`
```
Agent
Provider 接口 (Name / Chat / ChatStream)
LuaAdaptedProvider
├── 1. 序列化 CompletionRequest → raw JSON
├── 2. adapter.transform_request(rawJSON) → 协议特定请求体
├── 3. 读取 adapter.endpoint + adapter.headers 发 HTTP
├── 4. adapter.transform_response(rawHTTPBody) → 统一响应格式
└── 5. 反序列化为 CompletionResponse
```
### Lua 适配器契约
每个适配器是一个返回 table 的 Lua 脚本,位于 `data/adapters/*.lua`
```lua
adapter.name = "deepseek"
adapter.version = "2.0.0"
adapter.endpoint = "/chat/completions"
adapter.headers = {} -- 静态头Go 自动加 Authorization
-- 请求变换raw JSON → 协议格式
function adapter.transform_request(raw_body) return transformed end
-- 响应变换HTTP body → 统一格式 {content, reasoning_content, finish_reason, token_usage, tool_calls}
function adapter.transform_response(raw_body) return unified end
-- 流变换可选SSE data line → {content, done}
function adapter.transform_stream_chunk(raw_line) return chunk end
```
### Lua VM 能力
- `json.encode(table)` → 使用 Go `json.Marshal` 的 JSON 序列化
- `json.decode(string)` → 使用 Go `json.Unmarshal` 的 JSON 反序列化
- 全局函数 `log(level, msg)` / `http_get(url)` / `http_post(url, body)`
- 适配器内置 3 个:`openai.lua``deepseek.lua``ollama.lua`
## 十、配置中心 (ConfigRegistry)
配置不再分散在各处——通过 `ConfigRegistry` 统一管理:
```
Plugin (通过 SDK)
├─ Settings().Get("core.llm.model") → 读核心配置
├─ Settings().Set("plugin.qq.token", x) → 写插件配置
├─ Settings().List("plugin.") → 列出所有插件键
ConfigRegistry (线程安全 KV 存储)
├── 持久化到 data/settings.json
├── 键命名空间: core.* / plugin.<name>.*
├── Register(key, default) ← 注册默认值(不标记 dirty
├── Get/Set/List/Delete ← 运行时读写
└── Flush() ← 写回磁盘
```
### WebUI 配置编辑
```
┌──────────────────────────────────┐
│ 侧边栏 编辑区 │
│ ┌──────┐ ┌──────────────────┐ │
│ │ core │ │ core.llm.model │ │
│ │plugin│ │ [input field] │ │
│ │.qq │ │ [保存] │ │
│ │plugin│ ├──────────────────┤ │
│ │.webui│ │ core.llm.base_url│ │
│ └──────┘ │ [input field] │ │
│ │ [保存] │ │
│ └──────────────────┘ │
└──────────────────────────────────┘
```
- `GET /api/v1/settings?prefix=core.` → 列出配置键值 + 插件列表
- `PUT /api/v1/settings``{key, value}` 写入配置
## 十一、SDK API 定义
### PluginAPI (`internal/plugin/sdk/api.go`)
```go
type PluginAPI struct {
Name string
Version string
}
func NewPluginAPI(name, version string, bus EventBus, mem MemoryAPI, know KnowledgeAPI) *PluginAPI
// 三通道
func (p *PluginAPI) RegisterTool(name string, handler ToolHandler) error
func (p *PluginAPI) RegisterStage(stage Stage, handler StageHandler)
func (p *PluginAPI) Subscribe(eventType EventType, handler EventHandler)
func (p *PluginAPI) Publish(evt *Event)
// 访问子系统的快捷方式
func (p *PluginAPI) Memory() MemoryAPI
func (p *PluginAPI) Knowledge() KnowledgeAPI
```
### 阶段上下文 (`StageContext`)
```go
type StageContext struct {
RawMessage string // 当前输入(可改写 on_input
UserID string
GroupID string
ContextMsgs []map[string]interface{} // 可注入的消息
LLMText string // LLM 返回文本(可改写 post_action
ToolCalls []ToolCall // 工具调用列表(可增删 post_action/before_toolcall
ToolResults []ToolResult // 工具执行结果(可改写 after_toolcall
FinalText string // 最终输出文本(可改写 before_output
Response *string // 设置后短路管道
Phase Stage // 当前阶段
Memory []MemItem // 召回的记忆
Extra map[string]interface{} // 扩展字段
}
```
---
## 十二、事件系统
### 事件类型
| 类型 | 发布时机 | 用途 |
|---|---|---|
| `raw_input` | 消息到达 Agent | 记录输入日志 |
| `agent_output` | 最终输出发送后 | 记录输出日志 |
| `tool_call` | 每个工具调用完成 | 审计工具调用 |
| `reasoning` | LLM 推理文本 | 展示推理过程 |
| `system` | 系统状态变更 | 健康检查、插件变更 |
### Event Bus (`internal/events/bus.go`)
```go
type Bus struct{}
func NewBus() *Bus
func (b *Bus) Publish(event *Event)
func (b *Bus) Subscribe(eventType EventType, handler Handler) func()
```
---
## 十三、自循环输入通道
核心维护一个独立的 `selfInputCh (chan string)`,用于内部任务(记忆消歧、系统维护),不经过 IO 层:
```
心跳检测 → entitySimilarity() → enqueueConsolidationTask()
injectSelf(msg)
selfInputCh ─→ eventLoop ─→ processTextInput
不经过 IOManager不经过任何插件
```
区别于 IOManager.InputChan外部输入selfInputCh 是核心自有的纯 Go channel
确保即使没有 IO 插件,记忆整理等维护任务也能正常执行。
## 十四、数据流全景
```
外部 (QQ/HTTP/硬件)
│ 通过插件
IOManager.InjectInput() → inputCh
Agent.eventLoop() → handleInput → processTextInput
├── 1. on_input stage插件可拦截
├── 2. Context.Append
├── 3. Memory Recall (Indexer → Graph)
├── 4. pre_action stage插件可注入
├── 5. 工具循环 (最多 10 轮)
│ LLM → post_action → [before_toolcall → 执行 → after_toolcall] → LLM ...
├── 6. Context.Append(response)
├── 7. Prune不相关 → Document
├── 8. before_output stage插件可改写
├── 9. EmitOutput (通过 output_send 到对应通道)
├── 10. after_output stage插件只读
└── 11. memory_candidate → TextMemory + Distiller → GraphDB
心跳30min:
├── distillContext()
├── syncGraphToDocs()
└── reorgGraph()
├── Indexer.Sync() — 图→向量(自动)
├── DocStore.Reindex() — 文档向量重建(自动)
├── 冷文档→图归化(自动)
└── 实体冲突检测 → 走 LLM 消歧(详见第八章)
```
---
## 十五、代码结构
```
cmd/homed/main.go — 入口:组装所有子系统, 零 IO
cmd/waiter/main.go — CLI 客户端 (Unix socket)
internal/
├── agent/
│ ├── core/
│ │ ├── agent.go — Agent: eventLoop/interceptLoop/process/distillLoop
│ │ ├── context.go — RelevanceContextTF-IDF 上下文管理
│ │ └── stages.go — StageHost阶段管道编排 (并行执行)
│ ├── api/
│ │ └── provider.go — Provider 接口 + DeepSeek/Ollama/LuaAdaptedProvider
│ ├── io/
│ │ └── channel.go — IOManager (排队/中断/输出三通道)
│ └── personal.go — 人格加载
├── sdk/ ★ PluginSDK: 核心 Go API
│ ├── plugin.go — Plugin 接口 + PluginSDK 结构体
│ ├── memory.go — MemoryAPI (图/文本/文档)
│ ├── knowledge.go — KnowledgeAPI
│ ├── settings.go — SettingsAPI (配置)
│ └── llm.go — LLMAPI (源管理)
├── config/
│ └── registry.go — ConfigRegistry统一配置中心 (SQLite)
├── events/
│ └── bus.go — 系统事件总线 (Publish/Subscribe)
├── plugin/
│ ├── registry.go — 注册表:生命周期 Load/StopAll/Reload, RegisterFactory
│ ├── manifest.go — PluginManifest (plugin.json 元数据)
│ ├── dynamic.go — .so 动态加载器 (Go plugin.Open)
│ └── plugin.go — SKILL 插件解析 (OpenClaw 兼容)
├── plugins/
│ ├── all.go — 空白导入触发所有内置插件 init()
│ ├── timer/plugin.go — 定时器 (timer_set 工具 + 中断反馈)
│ ├── cli/plugin.go — CLI 插件 (Unix socket, InjectTextSync)
│ ├── openclaw/plugin.go — OpenClaw 兼容 (SKILL.md → SDK 工具注册)
│ └── webui/ — WebUI 插件 (HTTP 服务器 + 仪表盘)
│ ├── plugin.go
│ └── handler.go
├── memory/
│ ├── graph.go — SQLite 图数据库
│ ├── indexer.go — 图索引器
│ ├── vector/store.go — TF-IDF 向量存储
│ ├── document/doc.go — 文档记忆
│ ├── text/text.go — 文本记忆 (JSONL)
│ └── pipeline/ — 蒸馏器
├── knowledge/
│ └── knowledge.go — 知识系统
├── onebot/ — OneBot V11 QQ 协议实现
├── tracker/ — 变更追踪 (overlayfs)
├── supervisor/ — 守护进程
├── skill/ — 技能管理器
├── lua/
│ ├── vm.go — Lua VM (json.encode/decode, transform)
│ └── adapters/ — LLM 协议适配器脚本
├── network/ — 网络监控
├── container/ — 容器管理
├── snapshot/ — 快照
├── embed/ — 嵌入
└── tokenizer/ — 分词器
config/ — 顶层配置加载
├── config.go — Config 结构
└── config.yaml
pkg/types/ — 类型定义
docs/
├── ARCHITECTURE.md — 本架构文档
├── ADAPTER.md — Lua 适配器文档
└── PLAN.md — 实施计划/概览
```
---
## 十六、与旧设计 (v3) 的关键区别
| 维度 | v3 | v4 |
|------|-----|-----|
| 插件交互 | Device 接口 + IOManager 路由 | 三通道Tool/Stage/Event/Settings |
| 消息流编辑 | 无(纯事件推送) | 阶段管道 7 个 hook 点 |
| Event Bus | 无 | `internal/events/bus.go` |
| SDK | 无 | `internal/plugin/sdk/` (含 SettingsAPI) |
| LLM 适配 | 硬编码 Provider | Lua 脚本 raw JSON 变换 |
| 配置管理 | 分散在各处 | ConfigRegistry 统一 KV 存储 |
| 核心 IO | IOManager `EmitOutput` 直出 | 全部走 `output_send` 工具 |
| 插件工具路由 | IOManager `ExecuteTool` 链 | StageHost + Registry 双层路由 |
| 内部任务 | 无 | selfInputCh 自循环通道(不经过 IO |
---
## 十七、插件自注册与动态加载
### 自注册机制
内置插件通过 `init()` 自注册,无需 `main.go` 硬编码:
```go
// internal/plugins/timer/plugin.go
func init() {
plugin.RegisterFactory("timer", func(name string, config map[string]interface{}) (sdk.Plugin, error) {
return New(name), nil
})
}
```
空白导入文件 `internal/plugins/all.go` 触发所有内置插件的 `init()`
```go
package plugins
import (
_ "gitcode.com/JianFeeeee/HomeAgent/internal/plugins/cli"
_ "gitcode.com/JianFeeeee/HomeAgent/internal/plugins/openclaw"
_ "gitcode.com/JianFeeeee/HomeAgent/internal/plugins/timer"
_ "gitcode.com/JianFeeeee/HomeAgent/internal/plugins/webui"
)
```
`main.go` 不再需要 `RegisterNative` 调用,只需设置包级变量注入运行时依赖:
```go
cli.DefaultSocket = *cliSocket
openclaw.SkillsDir = filepath.Join(*dataDir, "skills")
webui.Configure(httpAddr, sup, memDB, ...)
pluginReg.Load(plgDir) // 自动扫描目录 + 使用已注册的工厂
```
`Load()` 分两步执行:
1. 扫描 `plugins/` 下已有子目录,匹配已注册工厂加载
2. 对已注册工厂但尚无目录的,自动创建目录并加载
### 动态 .so 加载
第三方插件编译为 `.so` 文件,放入 `<data>/plugins/<name>/`
```
<data>/plugins/myplugin/
plugin.json { "name": "myplugin", "version": "1.0", "entry": "plugin.so" }
plugin.so (Go -buildmode=plugin, 导出 NewPlugin 函数)
```
加载器 (`internal/plugin/dynamic.go`) 流程:
```go
tryLoadSO(dir, name, config):
1. plugin.Open("plugin.so")
2. Lookup("NewPlugin") 签名 func(name string, config map[string]interface{}) (sdk.Plugin, error)
3. 调用 factory, 包装为 dynamicPlugin
```
内置插件保持 init() 自注册编译进内核,第三方插件以 .so 形式热加载。
---
## 十八、中断打断机制
### 架构
```
interceptLoop (独立 goroutine)
├── InputInterruptChan() ← 定时器/消息通知等
├── (a) cancelLLM() → Provider HTTP 请求取消
├── (b) interceptCh < text → process() turn 前 drainInterrupt()
└── (c) InjectInput("interrupt", "text", ...) → 空闲时触发新处理
```
### 三种投递路径
| 路径 | 目标 | 触发时机 |
|------|------|---------|
| **(a) cancelLLM** | 取消进行中的 Provider HTTP 请求 | 拦截到 `context.Canceled` |
| **(b) interceptCh** | process() 工具循环中注入 `[打断消息]` | 每个 LLM call 前 `drainInterrupt()` |
| **(c) InjectInput** | eventLoop 空闲时启动新处理循环 | 无进行中请求时 |
### process() 内中断注入
```go
for turn := 0; turn < maxTurns; turn++ {
if text := a.drainInterrupt(); text != "" {
msgs = append(msgs, agentAPI.Message{
Role: "system",
Content: fmt.Sprintf("[打断消息] %s", text),
})
}
// LLM call with cancellable context
reqCtx, cancel := context.WithCancel(a.ctx)
a.cancelLLM = cancel // interceptLoop 可调用
resp, err := provider.Chat(reqCtx, req)
a.cancelLLM = nil
cancel()
// ... tool call loop ...
}
```