Files
homeagent-sdk/README.md
JianFeeeee fc876c5554 feat: 单插件重载等 PluginMgr 能力导出到外部 SDK
- meta: CORE_PLUGIN_RELOAD_ONE(48) / LIST_LOADED(49) / IS_DISABLED(50)
- sdk: PluginMgrAPI 接口(ReloadOne/ListLoadedPlugins/IsPluginDisabled) +
  PluginSDK.SetPluginMgrAPI/PluginMgr() 访问器
- plugindev 模板: dispatchPluginMgr 桥接注入,走 C ABI 48/49/50
2026-08-23 19:47:55 +08:00

23 KiB
Raw Permalink Blame History

HomeAgent SDK

HomeAgent 插件开发 SDK用于构建与 HomeAgent 平台交互的智能插件。

SDK API 接口

Plugin 接口

插件需实现 Plugin 接口:

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

PluginSDK 方法

通过 Start(sdk *PluginSDK) 注入的 SDK 实例提供以下方法:

分类 方法 说明
阶段钩子 RegisterStage(stage, handler, scope...) 注册阶段回调scope 可选:StageScopeGlobal(全局,默认)或 StageScopeOwnTools(仅自己工具)
输入通道 RegisterInputChannel(name, def) 注册输入通道def 为 ChannelDefNoMemory/Cleaner
输出通道 RegisterOutputChannel(name, caps, desc, def, handler) 注册输出通道def 为 ChannelDefcaps 为能力位掩码
工具注册 RegisterTool(name, def, handler) 注册工具供 LLM 调用
插件 API RegisterPluginAPI(name) 注册插件 API 供其他插件访问
图记忆 Memory() 访问图记忆 API实体-关系存储)
文本记忆 TextMemory() 访问文本记忆 API时序事件
文档记忆 DocMemory() 访问文档记忆 API向量存储
社交图谱 Social() 访问社交图谱 API外部插件只读
知识库 Knowledge() 访问知识库 API
LLM LLM() 访问 LLM 提供商管理 API
设置 Settings() 访问设置 API
事件 Events() 访问事件订阅器(外部插件仅订阅)
注入 InjectText(source, channel, text) / InjectInterruptText(source, channel, text) / InjectTextNoMemory(source, channel, text) 向管道注入文本
自动重启 SetAutoRestart(enabled) / AutoRestart() 控制崩溃自动重启

阶段钩子

// 全局监听所有插件的阶段事件
sdk.RegisterStage(StagePreAction, func(ctx *StageContext) error { return nil })

// 仅监听自己注册的工具的 before_toolcall / after_toolcall
sdk.RegisterStage(StageBeforeToolcall, myHandler, StageScopeOwnTools)

ChannelDef

type ChannelDef struct {
    NoMemory bool              // 通道输入/输出不参与记忆计算(向量/关键词/蒸馏),原文保留
    Cleaner  func(string) string // 可选:计算层过滤函数(不改原文)
}

ChannelDef 控制通道在记忆计算层的行为,与 ToolDefNoMemory/Cleaner 语义一致。

输入通道

sdk.RegisterInputChannel("qq", ChannelDef{
    NoMemory: true,
    Cleaner:  func(text string) string { return strings.TrimSpace(text) },
})

输出通道

sdk.RegisterOutputChannel("my-channel", CapText|CapFile, "通道描述", ChannelDef{}, handler)

handler 接收三个参数:

  • payload (string) — 消息载荷。type=text 时直接填文字,type=file/image 时填 URL
  • meta (string) — 可选的 JSON 路由元数据(如 {"group_id":123,"user_id":456}
  • type (string) — 载荷类型,枚举值见下

能力标志位:

标志 说明
CapText 1 纯文本输出
CapFile 2 文件输出
CapImage 4 图片输出
CapAudio 8 音频输出
CapStructured 16 结构化数据输出

type 枚举值:

说明
text 纯文本
voice / audio 语音
image 图片
file 文件

IOInjector 通道路由

方法 说明
InjectText(source, channel, text) 注入文本,记入内存,路由到指定通道
InjectInterruptText(source, channel, text) 注入中断文本,打断当前处理,路由到指定通道
InjectTextNoMemory(source, channel, text) 注入文本,不记入内存,路由到指定通道

source 标识来源,channel 指定目标输出通道。

Triple 扩展字段

Triple 数据结构新增字段:

  • Confidence — 置信度0.0~1.0
  • SubjectType — 主体类型
  • ObjectType — 客体类型

ToolDef 字段说明

RegisterTooldef 参数类型为 sdk.ToolDef,包含以下字段:

字段 类型 说明
Name string 工具名,建议插件名前缀避免冲突
Description string 工具描述LLM 据此选择调用
Parameters map[string]interface{} JSON Schema 格式参数定义
NoMemory bool 默认为 false;设为 true 时输出不参与向量/jieba/蒸馏计算(原文保留)
Cleaner func(string) string 可选,输出进入计算层前的清洗函数(如 JSON 提取 .content

NoMemoryCleaner 的详细设计意图参见核心仓 docs/zh/PLUGIN_DEV.md

New 构造函数

New() 由内核在加载插件时调用,插件开发者无需手动构造 PluginSDK

func New(name string, sett SettingsAPI, regTool ToolRegistrar, regStage StageRegistrar, regAPI APIRegistrar, regOutput OutputChannelRegistrar) *PluginSDK

插件开发者只需实现 Plugin 接口并导出 NewPluginFactory() 入口函数。

plugindev 工具链

plugindev 提供插件开发全流程支持。仓库 bin/ 提供各平台预制二进制linux/darwin/windows × amd64/arm64下载后直接加入 PATH 即可:

curl -o plugindev https://gitcode.com/JianFeeeee/homeagent-sdk/-/raw/main/bin/plugindev_linux_amd64
chmod +x plugindev
命令 说明
plugindev init <name> [--lua] 初始化插件项目(生成 plg.json、plugin.go 或 main.lua、go.mod、README.md
plugindev build [flags] 编译并打包为 .hmap 包(支持跨平台编译和 bundle 模式)
plugindev clean 清理 build/dist/ 目录及生成文件plugin.json、z_bridge_gen.go
plugindev debug [dir] 通过 Yaegi Go 解释器加载插件源码,启动交互式 REPL 调试
plugindev sdk <command> SDK 版本管理子命令list/install/use/path/current/latest

支持 GoLua 两种插件语言。

build 命令 flags

Flag 说明
--outdir <dir> 输出目录(默认 dist,可覆盖 plg.json 中的 outdir
--target <os/arch> 构建目标(如 linux/amd64),可重复指定(追加到 plg.json 中的 targets
--bundle 强制 bundle 模式(同时编译 linux/amd64, darwin/amd64, windows/amd64
--no-bundle 关闭 bundle 模式,仅按 targets 逐个编译
--sdk-path <path> 指定 SDK 源码路径(覆盖 plg.json 中的 sdk_path
--replace <from=to> / -R Go 模块替换(追加到 plg.json 中的 replacesfrom 为模块路径,to 为本地路径

plg.json 清单格式

{
  "name": "weather",
  "name_zh": "天气查询",
  "name_en": "Weather",
  "version": "1.0.0",
  "description": "天气查询插件",
  "author": "HomeAgent",
  "entry": "plugin.so",
  "tags": ["weather", "forecast"],
  "targets": "linux/amd64,windows/amd64",
  "outdir": "dist",
  "bundle": true,
  "replaces": {
    "github.com/example/pkg": "../local/pkg"
  },
  "source_dirs": [
    "../shared-lib"
  ]
}
字段 类型 说明
name string 插件标识名
name_zh string 中文名
name_en string 英文名
version string 版本号
description string 插件描述
author string 作者
entry string 入口文件(plugin.so / plugin.dll / main.lua
tags string[] 标签
targets string 构建目标,逗号分隔(如 linux/amd64,windows/amd64Lua 插件为 lua
outdir string 输出目录(默认 dist
bundle bool 是否 bundle 模式(同时编译多平台,默认 true
sdk_path string SDK 源码路径(覆盖自动检测的 SDK 路径)
go_version string Go 版本(如 1.21,默认从 SDK 的 go.mod 读取)
replaces object Go 模块替换key=模块路径value=本地路径
source_dirs string[] 额外源码搜索路径(编译时自动导入,用于引入 thirdpart/ 外部的共享代码)

.hmap 包格式

.hmap 为 ZIP 归档,包含:

  • plugin.json — 插件元数据
  • plugin.so — Go 编译产物Linux
  • plugin.dll — Go 编译产物Windows
  • plugin.dylib — Go 编译产物macOSbundle 模式)
  • main.lua — Lua 插件入口Lua 插件时)

插件生命周期

入口函数

插件必须导出 NewPluginFactory 入口函数Gostart() 函数Lua

Go 插件 — 实现 Plugin 接口并导出工厂函数:

func NewPluginFactory(name string, config map[string]interface{}) (sdk.Plugin, error) {
    return &Plugin{name: name}, nil
}

该函数由内核在加载插件时调用,name 为插件名,configskill.json 中的配置(如有)。

Lua 插件 — 返回包含 start(sdk)stop() 方法的 table

local plugin = { name = "my-plugin" }
function plugin.start(sdk) -- 注册工具等 end
function plugin.stop() end
return plugin

启动与停止

  • Start(sdk *PluginSDK) error — 插件启动,接收 SDK 实例
  • Stop() error — 插件停止,释放资源
  • sdk.RegisterStopHandler(fn func()) — 注册停止清理回调。内核(内置插件)或 z_bridge外部插件会在调用插件 Stop() 之前统一执行已注册的 handler后注册先执行执行后清空、幂等。适合做持久化落盘、取消后台任务等清理此时插件内存状态仍然新鲜避免在 Stop() 阶段以陈旧状态写回导致数据复活。

删除清理onRemove

Stop/RegisterStopHandler 在插件停止(含重载、禁用)时执行;RegisterOnRemoveHandler 仅在插件被**卸载(删除)**时执行一次,重载/禁用不触发:

  • sdk.RegisterOnRemoveHandler(fn func()) — 注册删除清理回调。内核在 RemovePlugin 流程中、插件 Stop() 之后执行(后注册先执行,执行后清空、幂等)。用于删除插件自身创建的持久化文件(数据/缓存/状态文件)。
  • 内核卸载时一并清理:工具注册、disabled_plugins 记录、插件配置项定义(plugin.<name>.*)与插件配置表(config_<name>),卸载后插件配置区完全消失。
  • 示例:example/calendar(删 events.jsonexample/memo(删 memos.jsonexample/rss(删订阅数据目录)、example/weather(删缓存目录);plugindev 模板含 onRemove 演示。
sdk.RegisterOnRemoveHandler(func() {
    os.Remove(filepath.Join(dataDir, "events.json"))
})

自动重启

sdk.SetAutoRestart(true)
// 查询状态
enabled := sdk.AutoRestart()

插件崩溃时平台自动拉起,保障服务可用性。

受限 SDK vs 完整 SDK

外部插件(第三方分发)使用受限 SDK,仅暴露安全子集:

受限 API 允许操作
SocialAPI 只读:GetPersonGetTraitGetRelationsGetNetworkListPersons
EventSubscriber 仅订阅:Subscribe(无 Publish

内部插件(平台内置)拥有完整 SDK 访问权限,包括 SocialAPI 写操作和 EventPublisher。

示例插件

插件 类型 说明
weather Go 天气查询wttr.in演示 NoMemory/Cleaner/阶段钩子/通道/文本记忆
luademo Lua Lua 全功能示例,覆盖 v0.8.0 Lua SDK 全部 API 面
qq Go QQ 消息集成NapCat17 个工具,输入/输出通道完整对接
a2a Go Agent-to-Agent 协议通信
ai_image Go AI 图片生成
bili Go Bilibili 视频下载
browser Go 网络搜索、网页抓取、浏览器渲染
calendar Go 日历管理
editdoc Go 文档编辑
files Go 文件管理
memo Go 备忘录PreAction 注入 + 定时提醒)
music Go 音乐播放
ocr Go 光学字符识别
rss Go RSS 订阅
sanitizer Go 内容清洗/安全过滤

Remote Device SDK

用于开发远程设备接入适配器的 C 语言 SDK零外部依赖兼容嵌入式平台。

架构

┌─────────────────────────────────────────────────┐
│            ha_remotedevice (C SDK)              │
│  协议引擎  │  WS 帧  │  JSON  │  状态机  │ 传输抽象  │
└──────────┬──────────────────────────────────────┘
           │  同一份 C 代码,设备端和 App 端共用
    ┌──────┴──────────────────┐
    ▼                         ▼
┌──────────────┐    ┌──────────────────────────┐
│  ESP32 裸机   │    │  Linux 设备上的 App        │
│  纯 C 直调     │    │  (Python ctypes / Go CGo / │
│  简单命令处理   │    │   Node addon / C# P/Invoke) │
└──────────────┘    └──────────────────────────┘

声明式 API 设计

设备在代码中声明自己是什么能做什么支持哪些命令每个命令对应独立处理函数SDK 自动分发并回执结果:

#include "ha_remotedevice.h"

/* 声明能力 */
const char *caps[] = {"camera", "status", NULL};

/* 声明式命令处理表:每个命令绑定独立处理函数 */
static ha_status_t handle_camerasue(const char *req_id, const char *args,
                                    ha_cmd_result_t *result, void *userdata) {
    (void)req_id; (void)userdata;
    int duration = args[0] ? atoi(args) : 0;
    // 拍照/录像...
    result->status = 0;
    result->output = "data:image/jpeg;base64,...";  // SDK 自动回执
    return HA_OK;
}

ha_cmd_handler_def_t handlers[] = {
    {.command = "shell",      .handler = handle_shell},
    {.command = "camerasue",  .handler = handle_camerasue},
    {.command = "screensee",  .handler = handle_screensee},
    {.command = "speakeruse", .handler = handle_speakeruse},
    {.command = NULL},  /* 标记结束 */
};

ha_config_t config = {
    .transport = my_transport,     // 用户实现 4 个函数
    .server    = "192.168.1.100:9890",
    .token     = "my-token",
    .device = {
        .device_id = "esp32-cam-1",
        .name      = "门口摄像头",
        .kind      = "camera",
        .caps      = caps,
    },
    .handlers  = handlers,   // 声明式命令处理表
    .on_state  = my_state_handler,
};

ha_client_t *client = ha_client_new(&config);
ha_client_start(client);
while (1) {
    ha_client_process(client);     // 主循环处理
}

传输层抽象

用户只需实现 4 个函数,适配不同平台:

ha_transport_t my_transport = {
    .connect = my_tcp_connect,   // 建立 TCP 连接
    .send    = my_tcp_send,      // 发送数据
    .recv    = my_tcp_recv,      // 接收数据(阻塞)
    .close   = my_tcp_close,     // 关闭连接
    .ctx     = &my_platform_ctx,
};

支持的协议

功能 API
WS 连接 + 握手 ha_client_start 自动完成
设备注册 (hello/bind) 启动时自动发送
命令接收 (shell/homeagent) handlers 表声明式注册SDK 自动分发
命令回执 ha_client_send_result
二进制分块(录像等) ha_client_send_data_chunked
TTS 音频接收 on_binary 回调
事件上报 ha_client_send_event
状态上报 ha_client_send_status
心跳保持 自动 ping/pong

使用方式

通过 plugindev 工具链初始化项目:

plugindev init my-adapter --type remotedevice

生成 main.c + CMakeLists.txt,可直接编译或作为三方库引入:

add_subdirectory(path/to/ha_remotedevice)
target_link_libraries(my_app ha_remotedevice)
target_include_directories(my_app PRIVATE ${HA_REMOTEDEVICE_INCLUDE_DIR})

快速接入指南

以下是从零到设备成功接入 HomeAgent 的完整步骤。

1. 准备工作

在 HomeAgent 平台上创建接入令牌:

# 在 HomeAgent 服务端创建一个设备接入令牌
curl -X POST http://<homeagent-server>:8080/api/v1/device/token \
  -H "Content-Type: application/json" \
  -d '{"device_id":"esp32-cam-1","name":"门口摄像头","kind":"camera"}'
# 返回: {"token":"ha-dev-token-xxxxx"}

记录下返回的 token,设备端配置时使用。

2. 实现传输层4 个函数)

根据你的平台实现 ha_transport_t 的 4 个函数指针。以下是几种常见场景:

场景 A带 TCP/IP 栈的嵌入式设备(如 ESP32 + lwIP

#include "ha_remotedevice.h"
#include "lwip/sockets.h"

static int esp_connect(void *ctx, const char *host, uint16_t port) {
    struct sockaddr_in addr;
    int sock = socket(AF_INET, SOCK_STREAM, 0);
    if (sock < 0) return -1;
    addr.sin_family = AF_INET;
    addr.sin_port = htons(port);
    inet_pton(AF_INET, host, &addr.sin_addr);
    int ret = connect(sock, (struct sockaddr *)&addr, sizeof(addr));
    if (ret < 0) { closesocket(sock); return -1; }
    *(int *)ctx = sock;
    return 0;
}

static int esp_send(void *ctx, const uint8_t *data, int len) {
    int sock = *(int *)ctx;
    return send(sock, (const char *)data, len, 0);
}

static int esp_recv(void *ctx, uint8_t *buf, int len) {
    int sock = *(int *)ctx;
    return recv(sock, (char *)buf, len, 0);
}

static void esp_close(void *ctx) {
    int sock = *(int *)ctx;
    closesocket(sock);
}

int esp_ctx = -1;
ha_transport_t transport = {
    .connect = esp_connect,
    .send    = esp_send,
    .recv    = esp_recv,
    .close   = esp_close,
    .ctx     = &esp_ctx,
};

场景 B通过串口UART连接透传模块

static int uart_connect(void *ctx, const char *host, uint16_t port) {
    (void)host; (void)port;
    // 初始化 UART波特率 115200
    return uart_init((uart_ctx_t *)ctx, 115200);
}

static int uart_send(void *ctx, const uint8_t *data, int len) {
    return uart_write((uart_ctx_t *)ctx, data, len);
}

static int uart_recv(void *ctx, uint8_t *buf, int len) {
    return uart_read((uart_ctx_t *)ctx, buf, len);
}

static void uart_close(void *ctx) {
    uart_deinit((uart_ctx_t *)ctx);
}

注意UART 透传时,另一端需运行一个 TCP 桥接程序,将串口数据转发到 HomeAgent 的 WebSocket 端口。

3. 声明设备能力和命令处理

#include "ha_remotedevice.h"

/* 声明设备能力 */
const char *caps[] = {"camera", "speaker", "status", NULL};

/* 处理 camerasue 命令(拍照) */
static ha_status_t handle_camera(const char *req_id, const char *args,
                                 ha_cmd_result_t *result, void *userdata) {
    (void)req_id; (void)userdata;
    int duration = args[0] ? atoi(args) : 0;  // 参数:录像时长

    // 拍照或录像,将结果填入 result
    result->status = 0;
    result->output = "data:image/jpeg;base64,/9j/4AAQ...";  // base64 图像数据
    return HA_OK;
}

/* 处理 shell 命令 */
static ha_status_t handle_shell(const char *req_id, const char *args,
                                ha_cmd_result_t *result, void *userdata) {
    (void)req_id; (void)userdata;
    // 执行 shell 命令args 为完整命令字符串
    result->status = 0;
    result->output = "command executed";
    return HA_OK;
}

/* 声明式命令处理表 */
ha_cmd_handler_def_t handlers[] = {
    {.command = "shell",      .handler = handle_shell},
    {.command = "camerasue",  .handler = handle_camera},
    {.command = "screensee",  .handler = handle_camera},
    {.command = "speakeruse", .handler = handle_speaker},
    {.command = NULL},  /* 标记结束 */
};

4. 配置并启动客户端

ha_config_t config = {
    .transport = transport,                 // 传输层实现
    .server    = "192.168.1.100:9890",      // HomeAgent 服务端地址
    .token     = "ha-dev-token-xxxxx",      // 第 1 步获取的令牌
    .device = {
        .device_id = "esp32-cam-1",
        .name      = "门口摄像头",
        .kind      = "camera",
        .caps      = caps,
        .info_json = "{\"chip\":\"ESP32-S3\",\"firmware\":\"v1.0\"}",
    },
    .handlers  = handlers,                  // 命令处理表
    .on_binary = on_binary_data,            // 接收 TTS 音频等二进制数据
    .on_state  = on_state_change,           // 连接状态变化回调
    .ping_interval = 30,                    // 心跳间隔秒数
};

ha_client_t *client = ha_client_new(&config);
ha_status_t ret = ha_client_start(client);
if (ret != HA_OK) {
    printf("设备接入失败: %d\n", ret);
    return;
}

/* 主循环 */
while (1) {
    ha_client_process(client);  // 处理协议帧、心跳、命令分发

    /* 可选:设备主动上报事件 */
    ha_client_send_event(client, "motion_detected",
                         "{\"zone\":\"front_door\",\"confidence\":0.95}");

    /* 可选:上报设备状态 */
    ha_client_send_status(client, "online");

    vTaskDelay(100 / portTICK_PERIOD_MS);  // 嵌入式 RTOS 风格延时
}

5. 验证连接

在 HomeAgent 服务端检查设备是否在线:

# 查看已注册设备列表
curl http://<homeagent-server>:8080/api/v1/device/list
# 预期输出包含: {"device_id":"esp32-cam-1","status":"online",...}

# 向设备发送命令(测试 camerasue
curl -X POST http://<homeagent-server>:8080/api/v1/device/esp32-cam-1/cmd \
  -H "Content-Type: application/json" \
  -d '{"cmd":"camerasue","args":"3"}'
# 预期返回: {"status":"ok","result":"data:image/jpeg;base64,..."}

6. 调试技巧

问题 检查点
连接失败 确认 server 地址和端口可通;检查 token 是否正确
WS 握手失败 确认 HomeAgent 服务端已开启 WebSocket 支持
命令无响应 确认 handlers 表中注册了对应命令名;检查 on_binary 是否配置
断线重连 max_reconnect 控制重连次数,-1 为无限重连
内存不足(嵌入式) 定义 HA_NO_ALLOC 宏禁用动态内存分配

位置

  • SDK 源码: remotedevice/
  • plugindev 模板: plugindev init --type remotedevice

构建与安装

构建

plugindev build

输出 .hmap 包到 dist/ 目录(默认 bundle 多平台合集;单平台构建使用 plugindev build --no-bundle)。

安装

通过 pluginmgr HTTP API 安装(端口默认 9876仅监听 127.0.0.1,无鉴权):

# 本地路径
curl -X POST http://127.0.0.1:9876/plugins \
  -H "Content-Type: application/json" \
  -d '{"path": "/path/to/my-plugin.hmap"}'

# 直接上传二进制
curl -X POST http://127.0.0.1:9876/plugins \
  --data-binary @dist/my-plugin.hmap

或通过 WebUI 插件管理页面上传,也可手动将 .hmap 放入插件目录后重启平台。