Files
HomeAgent/docs/PLUGIN_DEV.md
root 197f932932 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 更新链接
2026-07-03 17:58:20 +08:00

10 KiB
Raw Blame History

HomeAgent 插件开发指南

概述

HomeAgent 的所有外部交互能力都来自插件。插件是独立运行的 Go 包,通过 PluginSDKGo API与内核交互。

每个插件需要实现一个非常简单的接口:

type Plugin interface {
    Name() string
    Start(sdk *PluginSDK) error
    Stop() error
}

三种开发方式

方式 适用场景 复杂度
内置插件 随 HomeAgent 一起发布 简单,需合入主仓库
动态 .so 插件 独立分发的第三方插件 中等,需编译为 .so
Lua 脚本插件 轻量快速原型 简单(预留功能)

一、快速开始:内置插件

目录结构

internal/plugins/yourplugin/
    plugin.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 中添加空白导入:

package plugins

import (
    _ "gitcode.com/JianFeeeee/HomeAgent/internal/plugins/yourplugin"
    // ... 其他插件
)

完整示例:定时器插件

internal/plugins/timer/plugin.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 — 输入输出

// 向排队通道投递输入(按序处理)
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 可调用你的能力

sdk.RegisterTool(name string, def ToolDef, handler ToolHandler) error
  • name: 工具名称LLM 通过此名称调用)
  • def: 工具定义(描述 + 参数 JSON Schema
  • handler: 调用时执行的函数

工具定义示例:

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 输出后 统计、记录
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
})

📡 事件 — 订阅/发布系统事件

// 订阅事件
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"},
})

🧠 能力访问

// 记忆
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

// 在 Start() 中
val, err := s.Settings().Get("api_key")
if err != nil {
    // 未配置
}

用户通过 WebUI 或 CLI 设置:

// 读取其他插件的配置
s.Settings().GetPlugin("other_plugin", "some_key")

// 读取核心配置
s.Settings().GetCore("llm.model")

三、插件需要外部依赖时的做法

有些插件在初始化时需要内核中的组件数据库、LLM 管理器等)。采用包级变量注入模式:

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 中:

myplugin.DataDir = filepath.Join(*dataDir, "myplugin_data")
pluginReg.Load(plgDir)  // 之后调用

四、动态 .so 插件

编译插件为 .so

// 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 }

编译:

go build -buildmode=plugin -o plugin.so ./myplugin/

部署

<dataDir>/plugins/myplugin/
    plugin.json    — {"name": "myplugin", "version": "1.0", "description": "..."}
    plugin.so      — 编译产物

内核扫描时会自动发现并加载。无需修改 main.goall.go


五、最佳实践

  1. Start() 非阻塞 — 长时间运行的任务用 goroutine 启动,不要在 Start() 中阻塞
  2. Stop() 清理资源 — 关闭网络连接、停止 goroutine、取消订阅
  3. 工具 name 唯一 — 工具名不能与其他插件冲突,建议用插件名前缀
  4. 错误处理 — 工具 handler 返回 errorLLM 会收到错误信息并可能重试
  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 了解技术架构?查看 ARCHITECTURE.md