Files
HomeAgent/docs/PLUGIN_DEV.md

461 lines
12 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 插件开发指南
## 概述
HomeAgent 的所有外部交互能力都来自插件。插件通过 `PluginSDK`Go API与内核交互。
**SDK 仓库**:插件开发工具、模板代码和示例插件统一托管在
SDK 仓库。
```bash
git clone https://gitcode.com/JianFeeeee/homeagent-sdk.git
cd homeagent-sdk
hack/plugin-dev/scaffold.sh myplugin ./plugins/myplugin
```
每个插件实现一个三方法接口:
```go
type Plugin interface {
Name() string
Start(sdk *PluginSDK) error
Stop() error
}
```
### 三种开发方式
| 方式 | 适用场景 | 复杂度 |
|------|---------|--------|
| **动态 .so 插件(推荐)** | 独立分发的第三方插件 | 中等,使用 [SDK 仓库](https://gitcode.com/JianFeeeee/homeagent-sdk) 脚手架生成 |
| **内置插件** | 随 HomeAgent 一起发布 | 简单,需合入主仓库 |
| **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{})
// 快捷方式text → Input
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 返回结果 | 修改输出/工具列表 |
| `before_toolcall` | 工具调用前 | 审计、拒绝、改参 |
| `after_toolcall` | 工具执行后 | 脱敏、改写结果 |
| `before_output` | 输出前 | 格式适配 |
| `after_output` | 输出后 | 统计日志 |
```go
sdk.RegisterStage(sdk.StageOnInput, func(ctx *sdk.StageContext) error {
if ctx.UserID == "blocked_user" {
resp := "你已被限制使用"
ctx.Response = &resp // 短路后续阶段
return nil
}
return nil
})
```
#### 事件 — 订阅/发布系统事件
```go
unsub := sdk.Subscribe(events.EventType("tool_call"), func(evt *events.Event) {
log.Printf("工具被调用: %v", evt.Payload)
})
defer unsub()
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)
```
### 读取插件配置
每插件独立 SQLite 表 `config_<name>`
```go
val, err := s.Settings().Get("api_key")
if err != nil {
// 未配置
}
// 读取其他插件配置
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 插件
动态插件是独立于 HomeAgent 内核编译的 Go 插件,使用外部的 [Plugin SDK](https://gitcode.com/JianFeeeee/homeagent-sdk)
而非内核内部的 SDK 包。
完整的外部插件示例在 SDK 仓库的 `example/` 目录下:`qq``files``memo``web`
### 快速开始
使用 SDK 仓库的脚手架生成项目:
```bash
git clone https://gitcode.com/JianFeeeee/homeagent-sdk.git
cd homeagent-sdk
hack/plugin-dev/scaffold.sh myplugin ./plugins/myplugin
```
生成的代码:
```go
package main
import (
"gitcode.com/JianFeeeee/homeagent-sdk/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
cd <SDK_REPO_ROOT>
go build -buildmode=plugin -o plugins/myplugin/plugin.so plugins/myplugin/
```
或使用项目中的 Makefile
```bash
cd plugins/myplugin && make
```
### 部署
将插件目录(含 `plugin.json` + `plugin.so`)放入内核配置的插件目录:
```
<dataDir>/plugins/myplugin/
plugin.json — {"name": "myplugin", "version": "1.0", "description": "..."}
plugin.so — 编译产物
```
内核扫描时会自动发现并加载。无需修改 `main.go``all.go`
### 打包分发
使用 SDK 仓库的打包工具生成 `.hmap` 分发包:
```bash
hack/plugin-dev/packager.sh plugins/myplugin
# 输出: dist/myplugin-0.1.0.hmap
```
通过 WebUI 插件管理页面上传安装,或使用 `plugin_install` 工具。
### 完整示例
SDK 仓库的 `example/qq/` 目录提供了一个完整的 QQ 集成插件示例(对接 NapCat 框架),
涵盖消息收发、群管理、好友管理、文件操作、OCR 等功能,可作为开发参考。
---
## 五、最佳实践
1. `Start()` 非阻塞 — goroutine 启动长任务,不要阻塞 Start
2. `Stop()` 清理资源 — 关连接、停 goroutine、取消订阅
3. 工具名唯一 — 建议插件名前缀避免冲突
4. handler 返回 `error` 时 LLM 会收到并可能重试
5. 打断用 `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 服务器 |
### 外部插件示例
| 插件 | 位置 | 特点 |
|------|------|------|
| QQ | `example/qq/` in [homeagent-sdk](https://gitcode.com/JianFeeeee/homeagent-sdk) | NapCat 框架对接14 个工具 |
| 你的插件 | `plugins/yourplugin/` | 使用 SDK 脚手架生成 |
---
*了解项目整体目标?查看 [OVERVIEW.md](OVERVIEW.md)。*
*了解技术架构?查看 [ARCHITECTURE.md](ARCHITECTURE.md)。*
*SDK 仓库与开发工具?查看 [homeagent-sdk](https://gitcode.com/JianFeeeee/homeagent-sdk)。*