Files
homeagent-sdk/README.md
JianFeeeee e256023399 docs+plugindev: README 同步子进程架构,scaffold 修 entry 与 go.sum 死路
三类问题,都会让新用户第一次上手就走错:

1. README 仍写 plugin.so
   plugin.json 示例的 entry、entry 字段说明、.hmap 包格式三处描述的都是
   已退场的 C ABI 产物。改为 plugin.bin,并补 bundle 模式下
   plugin.bin.<goos>.<goarch> 的命名与安装时挑平台的行为。

2. cmd_init.go 的 entry := "plugin.so"
   scaffold 出来的 plg.json 带着一个已退场的 entry 值。build 实际不看这个
   值(只用它区分 Lua),但跟着模板走会误以为自己在做 C ABI 插件。

3. 生成的项目第一次 build 必定失败
   go.mod 只 require 一个 gitcode 模块版本号且不生成 go.sum。gitcode 不在
   proxy.golang.org 上,于是:
     go build     → missing go.sum entry
     go mod tidy  → 去公共 proxy 拉一个不存在的条目,超时
   原来 cmdBuild 里那句 `go mod download <mod>` 走的正是这条死路,失败后
   只打一行 warn 就继续编译,紧接着死在同一个错误上——用户看到两段无关报错。

   修法分两处:
   - 生成的 go.mod 直接写指向本机 SDK 的 replace(replace 到目录时 go 不
     需要也不校验 go.sum)
   - 新增 ensureSDKResolvable:三级策略(已有本地 replace → 探测本机 SDK
     并写入 → 兜底 go mod tidy 带 -mod=mod,失败给可操作提示)。存量项目
     go.mod 无 replace 时走第二级救回。

bin/ 5 个预编译二进制不再进仓库(改为 release 附件):
5 个平台各 26-28MB,每次重编都在 git 历史里再叠一份,而它们本质是可从源码
复现的产物。README 的下载说明同步改为 release 附件 URL + 从源码编译。
2026-09-03 19:27:03 +08:00

24 KiB
Raw 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 提供插件开发全流程支持。预编译二进制作为 release 附件分发linux/darwin/windows × amd64/arm64Releases 下载后加入 PATH 即可:

# 从 release 附件下载(以 v1.0.0 / linux amd64 为例)
curl -Lo plugindev https://gitcode.com/JianFeeeee/homeagent-sdk/releases/download/v1.0.0/plugindev_linux_amd64
chmod +x plugindev

# 或从源码自己编
cd tools/plugindev && go build -o plugindev .

二进制不再随仓库分发(旧的 bin/ 目录已停用5 个平台各 26-28MB 每次重编都在 git 历史里再叠一份,而它们本质是可从源码复现的产物。

命令 说明
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.bin",
  "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.bin / main.lua。v1.0.0 起 Go 插件统一为 plugin.bin,不再区分平台后缀
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.bin — Go 编译产物(单平台构建)
  • plugin.bin.<goos>.<goarch> — 多平台 bundle 模式下每平台一份, 安装时 pluginmgr 挑当前平台那份重命名为 plugin.bin
  • main.lua — Lua 插件入口Lua 插件时)

v1.0.0 起不再使用 plugin.so/plugin.dll/plugin.dylib——进程边界即 ABI 边界, 不存在平台特定的动态库区分。旧产物新内核不会加载,会给出明确的重编提示。

插件生命周期

入口函数

插件必须导出 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 放入插件目录后重启平台。