docs: 项目概览(OVERVIEW.md) + 插件开发指南(PLUGIN_DEV.md) + MCP 适配器插件

- OVERVIEW.md: 非技术用户友好的项目介绍和目标
- PLUGIN_DEV.md: 完整插件开发指南(含三种开发方式、API 参考、最佳实践)
- internal/plugins/mcp/: MCP 协议适配器插件(JSON-RPC over stdio/SSE)
  - client.go: MCP 客户端(ListTools / CallTool)
  - stdio.go: 子进程 stdin/stdout 传输
  - sse.go: HTTP POST 传输
  - plugin.go: init() 自注册 + Settings 配置读取
- README.md 更新链接
This commit is contained in:
root
2026-07-03 17:58:20 +08:00
parent 2d314b3e9c
commit 197f932932
8 changed files with 987 additions and 27 deletions

86
docs/OVERVIEW.md Normal file
View File

@ -0,0 +1,86 @@
# HomeAgent — 你的 24/7 智能管家
## 这是做什么的?
HomeAgent 是一个**持续运行的个人智能管家**。它像一个随时在线的大脑,你可以通过聊天跟它交流,让它帮你记住事情、查询知识、设置提醒、执行任务。
## 核心目标
| 目标 | 说明 |
|------|------|
| **永远在线** | 启动后持续运行,不像普通聊天软件需要每次打开 |
| **真正记住你** | 它不会每次对话都"失忆"——它会积累对你的了解,记住你的喜好、关系网和重要信息 |
| **隐私可控** | 所有数据存储在你自己的设备上(本地数据库),你也可以选择使用自己的 API 密钥 |
| **能力可扩展** | 通过"插件"添加新能力——就像手机装 App 一样 |
## 谁需要它?
- **想有个私人助理** — 帮你记待办、定时提醒、管理联系人
- **重视隐私的用户** — 数据全在本地,不经过第三方云服务
- **开发者和技术爱好者** — 可以自己编写插件来扩展功能
- **想探索 AI Agent 的人** — 一个真实可运行的 Agent 系统,不只是 API 调用
## 它能做什么?
### 🧠 记忆
- **记住你是谁** — 你的名字、喜好、重要日期
- **记住人际关系** — "张三是我同事,李四是我的朋友"
- **长期积累** — 聊得越多,它越了解你
### 📚 知识
- 你可以主动教它知识("公司的休假制度是……"
- 它会在需要时检索相关知识
### ⏰ 定时提醒
- "5分钟后提醒我喝水"
- 倒计时结束后它会主动通知你
### 🔌 可扩展(插件)
- **Web 控制台** — 在浏览器中管理和配置
- **命令行** — 通过终端快速交互
- **更多能力** — 开发者可以写插件接入任何服务
## 它是如何工作的?(简述)
```
你(通过聊天软件/终端/网页)
HomeAgent 内核 ←→ 插件(能力扩展)
本地存储(你的数据只在你这里)
```
- **内核** 是"大脑"——负责理解你说什么、调用什么能力、记住什么
- **插件** 是"手脚"——负责收发消息、设置定时器、连接外部服务等
- **所有数据存本地** — 你的对话、记忆、配置都保存在你自己的设备上
## 和普通 AI 聊天有什么区别?
| | 普通 AI 聊天 | HomeAgent |
|---|---|---|
| 记忆 | 每次对话独立,不记得你 | 长期记忆,越来越了解你 |
| 持续运行 | 关掉就没了 | 7×24 在线 |
| 主动能力 | 只能回复问题 | 能设定时器、主动提醒 |
| 可扩展 | 固定能力 | 插件系统,可无限扩展 |
| 数据隐私 | 上传到云服务 | 本地存储,完全可控 |
## 快速体验
```bash
# 启动(需要 DeepSeek API 密钥)
DEEPSEEK_API_KEY="sk-xxx" ./homed -data /tmp/ha
# 在另一个终端聊天
echo "你好,请记住我喜欢喝咖啡" | ./waiter
```
## 项目状态
HomeAgent 正在积极开发中。核心功能已可运行,插件系统和开发者 API 已就绪。
---
*想参与开发?查看 [PLUGIN_DEV.md](PLUGIN_DEV.md) 插件开发指南。*
*了解技术架构?查看 [ARCHITECTURE.md](ARCHITECTURE.md)。*

415
docs/PLUGIN_DEV.md Normal file
View File

@ -0,0 +1,415 @@
# HomeAgent 插件开发指南
## 概述
HomeAgent 的所有外部交互能力都来自插件。插件是独立运行的 Go 包,通过 `PluginSDK`Go API与内核交互。
每个插件需要实现一个非常简单的接口:
```go
type Plugin interface {
Name() string
Start(sdk *PluginSDK) error
Stop() error
}
```
### 三种开发方式
| 方式 | 适用场景 | 复杂度 |
|------|---------|--------|
| **内置插件** | 随 HomeAgent 一起发布 | 简单,需合入主仓库 |
| **动态 .so 插件** | 独立分发的第三方插件 | 中等,需编译为 .so |
| **Lua 脚本插件** | 轻量快速原型 | 简单(预留功能) |
---
## 一、快速开始:内置插件
### 目录结构
```
internal/plugins/yourplugin/
plugin.go — 插件主文件
```
### 最小插件示例
```go
package yourplugin
import (
"gitcode.com/JianFeeeee/HomeAgent/internal/plugin"
sdk "gitcode.com/JianFeeeee/HomeAgent/internal/sdk"
)
// init() 将插件注册到全局工厂表,内核启动时自动发现并加载。
func init() {
plugin.RegisterFactory("yourplugin", func(name string, config map[string]interface{}) (sdk.Plugin, error) {
return New(name), nil
})
}
type Plugin struct {
name string
}
func New(name string) *Plugin {
return &Plugin{name: name}
}
func (p *Plugin) Name() string { return p.name }
func (p *Plugin) Start(s *sdk.PluginSDK) error {
// 在这里初始化插件:启动 goroutine、注册工具、订阅事件等
return nil
}
func (p *Plugin) Stop() error {
// 清理资源
return nil
}
```
### 注册到内核
`internal/plugins/all.go` 中添加空白导入:
```go
package plugins
import (
_ "gitcode.com/JianFeeeee/HomeAgent/internal/plugins/yourplugin"
// ... 其他插件
)
```
### 完整示例:定时器插件
`internal/plugins/timer/plugin.go` 是一个完整的内置插件示例:
```go
package timer
import (
"fmt"
"log"
"sync"
"time"
"gitcode.com/JianFeeeee/HomeAgent/internal/plugin"
sdk "gitcode.com/JianFeeeee/HomeAgent/internal/sdk"
)
func init() {
plugin.RegisterFactory("timer", func(name string, config map[string]interface{}) (sdk.Plugin, error) {
return New(name), nil
})
}
type Plugin struct {
name string
mu sync.Mutex
wg sync.WaitGroup
}
func New(name string) *Plugin {
return &Plugin{name: name}
}
func (p *Plugin) Name() string { return p.name }
func (p *Plugin) Start(s *sdk.PluginSDK) error {
// 注册一个工具给 LLM 调用
return s.RegisterTool("timer_set", sdk.ToolDef{
Name: "timer_set",
Description: "设置一个定时提醒。倒计时结束后通过中断通道通知 agent。",
Parameters: map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"duration": map[string]interface{}{
"type": "string",
"description": "持续时间,例如 5s, 2m, 1h",
},
"message": map[string]interface{}{
"type": "string",
"description": "提醒内容",
},
},
"required": []string{"duration", "message"},
},
}, func(args map[string]interface{}) (interface{}, error) {
durStr, _ := args["duration"].(string)
message, _ := args["message"].(string)
dur, _ := time.ParseDuration(durStr)
go func() {
time.Sleep(dur)
// 通过中断通道通知 agent
s.InjectInterruptText("timer", "timer",
fmt.Sprintf("timer: %s", message))
}()
return map[string]interface{}{
"status": "timer_set",
"duration": durStr,
"message": message,
}, nil
})
}
func (p *Plugin) Stop() error {
p.wg.Wait()
return nil
}
```
---
## 二、插件开发详解
### PluginSDK 核心 API
#### 📤 IO — 输入输出
```go
// 向排队通道投递输入(按序处理)
sdk.InjectInput(source, channel string, payload map[string]interface{})
// 向中断通道投递输入(可打断当前 LLM 处理)
sdk.InjectInterrupt(source, channel string, payload map[string]interface{})
// 快捷方式:投递文本到排队通道
sdk.InjectText(source, channel, text string)
// 快捷方式:投递文本到中断通道
sdk.InjectInterruptText(source, channel, text string)
// 同步请求-响应发送文本并等待回复CLI 插件使用)
sdk.InjectTextSync(source, channel, text string) *OutputEvent
// 注册一个输出通道LLM 可通过 output_send 工具选择发送到此通道)
sdk.RegisterChannel(name string, dev Device) error
sdk.UnregisterChannel(name string)
sdk.ListChannels() []ChannelInfo
```
#### 🛠️ 工具 — 让 LLM 可调用你的能力
```go
sdk.RegisterTool(name string, def ToolDef, handler ToolHandler) error
```
- `name`: 工具名称LLM 通过此名称调用)
- `def`: 工具定义(描述 + 参数 JSON Schema
- `handler`: 调用时执行的函数
工具定义示例:
```go
sdk.RegisterTool("weather_query", sdk.ToolDef{
Name: "weather_query",
Description: "查询指定城市的天气",
Parameters: map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"city": map[string]interface{}{
"type": "string",
"description": "城市名称,如 北京",
},
},
"required": []string{"city"},
},
}, func(args map[string]interface{}) (interface{}, error) {
city, _ := args["city"].(string)
// 查询天气并返回
return map[string]interface{}{
"city": city,
"temp": 25,
"weather": "晴",
}, nil
})
```
#### 🔌 阶段钩子 — 干预消息处理流
7 个阶段, 按执行顺序:
| 阶段 | 时机 | 用途 |
|------|------|------|
| `on_input` | 消息刚到达 Agent | 黑名单、限流、短路回复 |
| `pre_action` | 即将调用 LLM | 注入额外上下文 |
| `post_action` | LLM 返回结果 | 修改 LLM 输出 |
| `before_toolcall` | 工具调用前 | 审计、拒绝、改参 |
| `after_toolcall` | 工具执行后 | 脱敏、改写结果 |
| `before_output` | 输出前 | 调整格式 |
| `after_output` | 输出后 | 统计、记录 |
```go
sdk.RegisterStage(sdk.StageOnInput, func(ctx *sdk.StageContext) error {
input := ctx.RawMessage
// 检查是否是黑名单用户
if ctx.UserID == "blocked_user" {
resp := "你已被限制使用"
ctx.Response = &resp // 设置 Response 会短路后续阶段
return nil
}
return nil
})
```
#### 📡 事件 — 订阅/发布系统事件
```go
// 订阅事件
unsub := sdk.Subscribe(events.EventType("tool_call"), func(evt *events.Event) {
log.Printf("工具被调用: %v", evt.Payload)
})
defer unsub() // 插件 Stop 时取消订阅
// 发布事件
sdk.Publish(&events.Event{
Type: "my_event",
Payload: map[string]interface{}{"key": "value"},
})
```
#### 🧠 能力访问
```go
// 记忆
sdk.Memory().Recall(query string) ([]MemItem, error)
sdk.Memory().Commit(triples []Triple) error
// 知识
sdk.Knowledge().Search(query string) ([]string, error)
// LLM 源管理
sdk.LLM().ListSources() []SourceInfo
sdk.LLM().SetSource(name string) error
// 配置(插件自身的配置表 config_<plugin_name>
sdk.Settings().Get(key string) (interface{}, error)
sdk.Settings().Set(key string, value interface{}) error
sdk.Settings().List(prefix string) ([]string, error)
```
### 读取插件配置
插件有自己的配置表 `config_<插件名>`,例如 `config_mcp`
```go
// 在 Start() 中
val, err := s.Settings().Get("api_key")
if err != nil {
// 未配置
}
```
用户通过 WebUI 或 CLI 设置:
```go
// 读取其他插件的配置
s.Settings().GetPlugin("other_plugin", "some_key")
// 读取核心配置
s.Settings().GetCore("llm.model")
```
---
## 三、插件需要外部依赖时的做法
有些插件在初始化时需要内核中的组件数据库、LLM 管理器等)。采用**包级变量注入**模式:
```go
package myplugin
var DataDir string // 由 main.go 在 Load() 前设置
func init() {
plugin.RegisterFactory("myplugin", func(name string, config map[string]interface{}) (sdk.Plugin, error) {
return New(name, DataDir), nil
})
}
```
`cmd/homed/main.go` 中:
```go
myplugin.DataDir = filepath.Join(*dataDir, "myplugin_data")
pluginReg.Load(plgDir) // 之后调用
```
---
## 四、动态 .so 插件
### 编译插件为 .so
```go
// myplugin/plugin.go
package main
import (
sdk "gitcode.com/JianFeeeee/HomeAgent/internal/sdk"
)
func NewPlugin(name string, config map[string]interface{}) (sdk.Plugin, error) {
return &myPlugin{name: name}, nil
}
type myPlugin struct {
name string
}
func (p *myPlugin) Name() string { return p.name }
func (p *myPlugin) Start(s *sdk.PluginSDK) error {
// 注册工具...
return nil
}
func (p *myPlugin) Stop() error { return nil }
```
编译:
```bash
go build -buildmode=plugin -o plugin.so ./myplugin/
```
### 部署
```
<dataDir>/plugins/myplugin/
plugin.json — {"name": "myplugin", "version": "1.0", "description": "..."}
plugin.so — 编译产物
```
内核扫描时会自动发现并加载。无需修改 `main.go``all.go`
---
## 五、最佳实践
1. **Start() 非阻塞** — 长时间运行的任务用 goroutine 启动,不要在 Start() 中阻塞
2. **Stop() 清理资源** — 关闭网络连接、停止 goroutine、取消订阅
3. **工具 name 唯一** — 工具名不能与其他插件冲突,建议用插件名前缀
4. **错误处理** — 工具 handler 返回 `error`LLM 会收到错误信息并可能重试
5. **中断 vs 排队** — 需要打断当前 LLM 处理的用 `InjectInterruptText`,普通的用 `InjectText`
6. **配置优先** — 不要硬编码配置,用 `Settings().Get/Set` 读写插件配置
---
## 六、现有插件参考
| 插件 | 位置 | 特点 |
|------|------|------|
| Timer | `internal/plugins/timer/` | 最简单的完整示例,注册一个工具 + 中断反馈 |
| CLI | `internal/plugins/cli/` | Unix socket 监听 + 同步请求响应 |
| OpenClaw | `internal/plugins/openclaw/` | 解析 SKILL.md 文件注册工具 |
| WebUI | `internal/plugins/webui/` | HTTP 服务 + 依赖注入Configure 模式) |
| MCP | `internal/plugins/mcp/` | JSON-RPC over stdio/SSE连接 MCP 服务器 |
---
*了解项目整体目标?查看 [OVERVIEW.md](OVERVIEW.md)。*
*了解技术架构?查看 [ARCHITECTURE.md](ARCHITECTURE.md)。*