Files
homeagent-sdk/tools/hmapdev/templates.go
HomeAgent Agent efb396d7b3 feat(lua): Lua SDK 全量对齐 1.3.0 + hmapdev 单一 mock 源/版本标记/语法预检
内核侧 Lua 桥此前停在 v0.8.0 时代能力面,而 1.1/1.2/1.3 新增的
媒体、注入标志位、中断优先级、事件订阅、动态通道注销只在 Go 侧存在,
文档却宣称『能力完全对齐』——属于静默漂移。

本仓(事实源):
- 新增 sdk/lua/sdk.lua:Lua mock 的单一事实源,补齐全部新 API
  (*_opts / inject_input_sync / inject_*_media / set_tool_blocks /
  unregister_output_channel / events / plugin_mgr / insert_with_media /
  sentence_text+media_digests / attachments / context_policy)。
- scripts/sync-lua-sdk.sh:把事实源同步到 hmapdev assets、luademo、
  以及被 vendored 时的内核副本;三份 sdk.lua 不再各自漂移。
- hmapdev init --lua:优先从激活 SDK 拷权威 mock,内嵌模板降级为
  assets/sdk.lua 回退,不再内联手写副本。
- hmapdev build(Lua):plugin.json 写入 SDK 版本(能力可追溯),
  打包前用 luac -p / lua loadfile 做语法预检,失败以非零码退出。
- hmapdev debug --lua:优先用激活 SDK 的权威 mock(HMAPDEV_SDK_LUA)。
- luademo 升级为全能力示例(新增 luademo_probe_v2)。
2026-09-13 19:56:12 +08:00

504 lines
17 KiB
Go
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.

package main
import _ "embed"
// tmplPlgJSON is the plg.json template
const tmplPlgJSON = `{
"name": "{{.Plg.Name}}",
"name_zh": "{{.Plg.NameZh}}",
"name_en": "{{.Plg.NameEn}}",
"version": "{{.Plg.Version}}",
"description": "{{.Plg.Description}}",
"author": "{{.Plg.Author}}",
"entry": "{{.Plg.Entry}}",
"sdk": "{{.Plg.SDK}}",
"tags": [{{range $i, $t := .Plg.Tags}}{{if $i}}, {{end}}"{{$t}}"{{end}}],
"targets": "{{.Plg.Targets}}"
}
`
const tmplGoMod = `module {{.ModulePath}}
go {{.GoVersion}}
require {{.SDKModule}} {{.SDKVersion}}
{{if .SDKLocalPath}}
// SDK 指向本机源码。gitcode 的模块不在 proxy.golang.org 上,
// 没有这条 replace 就需要 go.sum 条目,而那个条目无处可拉。
// 若你已有可访问的私有 proxy可删掉本行。
replace {{.SDKModule}} => {{.SDKLocalPath}}
{{end}}`
const tmplPluginGo = `package main
import (
"fmt"
"gitcode.com/JianFeeeee/homeagent-sdk/sdk"
)
type Plugin struct {
name string
sdk *sdk.PluginSDK
}
func (p *Plugin) Name() string { return p.name }
func (p *Plugin) Start(s *sdk.PluginSDK) error {
p.sdk = s
s.RegisterStopHandler(func() { fmt.Printf("[%s] stop handler running\n", p.name) })
s.Settings().RegisterDef(sdk.ConfigDef{
Key: "plugin.{{.Plg.Name}}.example", Default: "hello", Type: "string",
DisplayName: "示例配置", Description: "An example configuration key",
Category: "{{.Plg.Name}}",
})
// ---- 通道channel两个方向是分开的两件事 ----
//
// 入站 inputch ——「谁会往这个通道注入输入」。
// 凡是用 s.InjectText*/InjectInput*/InjectInterrupt*(source, "<name>", ...) 注入的通道名,
// 都要在这里登记inputch 是内核最基本的**输入路由单位**,只有登记过的通道
// 才能被「划给驻留子resident sub-agent没登记就划分会失败inputch 未注册)。
// 只登记出站通道时内核会兜底登记同名 inputch **并打告警**(兼容老插件)。
chName := p.name
_ = s.RegisterInputChannel(chName, sdk.ChannelDef{NoMemory: true})
// 出站 output ——「output_send__<name> 的回复发给谁」。
// handler 收到 mappayload(string) / type(string) / meta(string|optional)。
_ = s.RegisterOutputChannel(chName, sdk.CapText, "示例通道(回复由此返回)",
sdk.ChannelDef{NoMemory: true}, func(args map[string]interface{}) (interface{}, error) {
return map[string]interface{}{"status": "ok"}, nil
})
tp := p.name + "_"
s.RegisterTool(tp+"hello", sdk.ToolDef{
Name: tp + "hello",
Description: "A hello world tool",
Parameters: map[string]interface{}{"type": "object", "properties": map[string]interface{}{}},
NoMemory: false, // 工具输出对 LLM 注意力有信号价值时为 false纯操作工具为 true
// Cleaner: func(output string) string {
// // 工具输出参与向量化/jieba/蒸馏前,在此过滤噪音
// return output
// },
}, p.handleHello)
fmt.Printf("[%s] started\n", p.name)
return nil
}
func (p *Plugin) Stop() error { fmt.Printf("[%s] stopped\n", p.name); return nil }
func (p *Plugin) handleHello(args map[string]interface{}) (interface{}, error) {
return map[string]interface{}{"content": "Hello from {{.Plg.Name}} plugin!"}, nil
}
func NewPluginFactory(name string, config map[string]interface{}) (sdk.Plugin, error) {
return &Plugin{name: name}, nil
}
`
// fallbackLuaSDK 是 sdk.lua 的内嵌回退副本(单一事实源为 SDK 仓 sdk/lua/sdk.lua
//
// 为什么不再内联一份手写 mock三份 sdk.lua内核内嵌 / 工具链模板 / 项目副本)
// 各自漂移过一次结果就是“mock 有、内核没有”或反过来。改为从 assets/sdk.lua
// 内嵌 + 由 SDK 仓同步脚本搬运,并配契约测试守住。
//
//go:embed assets/sdk.lua
var fallbackLuaSDK string
const tmplMainLua = `-- {{.Plg.Name}} plugin
local plugin = { name = "{{.Plg.Name}}" }
function plugin.start(sdk)
sdk.log("info", "{{.Plg.Name}} starting...")
-- 通道:入站与出站分开登记。
-- 入站 inputch凡是用 sdk.inject_text/sdk.inject_interrupt(source, "<name>", ...) 注入的通道名
-- 都要登记;只有登记过的通道才能被「划给驻留子」(没登记会报 inputch 未注册)。
sdk.register_input_channel("{{.Plg.Name}}", { no_memory = true })
-- 出站 outputoutput_send__<name> 的回复由 handler 处理
sdk.register_output_channel("{{.Plg.Name}}", 1, "示例通道(回复由此返回)", { no_memory = true },
function(args) return { status = "ok" } end)
sdk.register_tool("{{.Plg.Name}}_hello", {
description = "A hello world tool",
parameters = { type = "object", properties = {} }
}, function(args) return { content = "Hello from {{.Plg.Name}} plugin!" } end)
sdk.log("info", "{{.Plg.Name}} started")
end
function plugin.stop() sdk.log("info", "{{.Plg.Name}} stopped") end
return plugin
`
// ============================================================
// Remote Device Adapter Templates
// ============================================================
const tmplRemoteDeviceMain = `#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include "ha_remotedevice.h"
/* ============================================================
* {{.Plg.Name}} — Remote Device Adapter
*
* 声明式远程设备接入示例。
* 用户只需实现:
* 1. ha_transport_t 的 4 个函数
* 2. 声明 handlers 表(设备支持哪些命令 + 对应的处理函数)
* 其余协议细节WS 握手、hello/bind、心跳、重连、命令分发、结果回执由 SDK 自动处理。
* ============================================================ */
/* ====================== 传输层实现 ======================
*
* 请为你的平台实现以下 4 个函数:
* connect(ctx, host, port) — 建立 TCP 连接
* send(ctx, data, len) — 发送数据
* recv(ctx, buf, len) — 接收数据(阻塞,返回实际接收字节数)
* close(ctx) — 关闭连接
*
* 示例POSIX socket 实现
*/
#if defined(_WIN32) || defined(_WIN64)
/* Windows 平台需包含 winsock2.h */
#error "Please implement transport for your platform (see example below)"
#else
/* POSIX (Linux, macOS, ESP-IDF, Zephyr, etc.) */
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netdb.h>
#include <unistd.h>
struct transport_ctx {
int sock;
};
static int transport_connect(void *ctx, const char *host, uint16_t port) {
struct transport_ctx *tc = (struct transport_ctx *)ctx;
struct hostent *he = gethostbyname(host);
if (!he) return -1;
tc->sock = socket(AF_INET, SOCK_STREAM, 0);
if (tc->sock < 0) return -1;
struct sockaddr_in addr;
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_port = htons(port);
memcpy(&addr.sin_addr, he->h_addr_list[0], he->h_length);
if (connect(tc->sock, (struct sockaddr *)&addr, sizeof(addr)) < 0) {
close(tc->sock);
tc->sock = -1;
return -1;
}
return 0;
}
static int transport_send(void *ctx, const uint8_t *data, int len) {
struct transport_ctx *tc = (struct transport_ctx *)ctx;
int sent = 0;
while (sent < len) {
int n = (int)send(tc->sock, data + sent, len - sent, 0);
if (n <= 0) return -1;
sent += n;
}
return sent;
}
static int transport_recv(void *ctx, uint8_t *buf, int len) {
struct transport_ctx *tc = (struct transport_ctx *)ctx;
int n = (int)recv(tc->sock, buf, len, 0);
return n;
}
static void transport_close(void *ctx) {
struct transport_ctx *tc = (struct transport_ctx *)ctx;
if (tc->sock >= 0) {
close(tc->sock);
tc->sock = -1;
}
}
#endif
/* ====================== 声明式命令处理 ======================
*
* 每个命令对应一个处理函数,通过填写 ha_cmd_result_t 返回数据。
* SDK 自动回执结果,无需手动调用 send_result。
*
* 返回方式:
* 1. 文本输出:填写 result->output
* 2. 二进制数据:设置 result->has_binary=1 并填写 binary_data/len/mime
* 3. 错误:设置 result->status=1 并填写 result->error
* 4. 返回 HA_OK 表示处理成功,其他值表示处理失败
*/
/* ESP32-CAM 摄像头处理 */
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 = 0;
if (args && args[0]) duration = atoi(args);
printf("[camera] %s (duration=%ds)\n", duration ? "record" : "snapshot", duration);
/* 返回文本结果base64 图片) */
result->status = 0;
result->output = "data:image/jpeg;base64,/9j/4AAQ...";
return HA_OK;
}
/* 屏幕截图处理 */
static ha_status_t handle_screensee(const char *req_id, const char *args,
ha_cmd_result_t *result, void *userdata) {
(void)req_id; (void)args; (void)userdata;
printf("[screen] screenshot\n");
result->status = 0;
result->output = "data:image/png;base64,iVBORw0KGgo...";
return HA_OK;
}
/* 语音播报处理 */
static ha_status_t handle_speakeruse(const char *req_id, const char *args,
ha_cmd_result_t *result, void *userdata) {
(void)req_id; (void)userdata;
printf("[speaker] TTS: %s\n", args ? args : "");
result->status = 0;
result->output = "speakeruse done";
return HA_OK;
}
/* 远程操控处理computeruse */
static ha_status_t handle_computeruse(const char *req_id, const char *args,
ha_cmd_result_t *result, void *userdata) {
(void)req_id; (void)userdata;
const char *action = NULL;
const char *json_str = NULL;
ha_cmd_parse_json(args, &action, &json_str);
printf("[computeruse] action=%s\n", action ? action : "unknown");
result->status = 0;
result->output = "computeruse done";
return HA_OK;
}
/* 剪贴板读取 */
static ha_status_t handle_clipboardsee(const char *req_id, const char *args,
ha_cmd_result_t *result, void *userdata) {
(void)req_id; (void)args; (void)userdata;
result->status = 0;
result->output = "clipboard content";
return HA_OK;
}
/* 剪贴板写入 */
static ha_status_t handle_clipboardsue(const char *req_id, const char *args,
ha_cmd_result_t *result, void *userdata) {
(void)req_id; (void)userdata;
printf("[clipboard] write: %s\n", args ? args : "");
result->status = 0;
result->output = "clipboard written";
return HA_OK;
}
/* 屏幕显示 */
static ha_status_t handle_screensue(const char *req_id, const char *args,
ha_cmd_result_t *result, void *userdata) {
(void)req_id; (void)userdata;
printf("[screensue] show: %s\n", args ? args : "");
result->status = 0;
result->output = "screensue shown";
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;
printf("[shell] cmd: %s\n", args ? args : "");
result->status = 0;
result->output = "shell output";
return HA_OK;
}
/* 设备信息查询 */
static ha_status_t handle_deviceinfo(const char *req_id, const char *args,
ha_cmd_result_t *result, void *userdata) {
(void)req_id; (void)args; (void)userdata;
result->status = 0;
result->output = "{\"platform\":\"linux\",\"arch\":\"x86_64\"}";
return HA_OK;
}
/* ====================== 连接状态回调 ====================== */
static void on_state(int connected, void *userdata) {
(void)userdata;
printf("[devicelink] state: %s\n", connected ? "connected" : "disconnected");
}
/* ====================== 主函数 ====================== */
int main(int argc, char *argv[]) {
/* 传输层上下文 */
struct transport_ctx tctx;
tctx.sock = -1;
ha_transport_t transport = {
.connect = transport_connect,
.send = transport_send,
.recv = transport_recv,
.close = transport_close,
.ctx = &tctx,
};
/* ===== 声明式设备配置 ===== */
/* 声明设备能力 */
const char *caps[] = {
"status", "cmdrun", "deviceinfo",
"camerasue", "screensee", "speakeruse",
"computeruse", "clipboardsee", "clipboardsue",
"screensue",
NULL
};
/* 声明命令处理表:设备支持哪些命令,以及对应的处理函数 */
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 = "computeruse", .handler = handle_computeruse},
{.command = "clipboardsee", .handler = handle_clipboardsee},
{.command = "clipboardsue", .handler = handle_clipboardsue},
{.command = "screensue", .handler = handle_screensue},
{.command = "deviceinfo", .handler = handle_deviceinfo},
{.command = NULL}, /* 标记结束 */
};
ha_config_t config = {
.transport = transport,
.server = "127.0.0.1:9890",
.token = "your-token-here",
.device = {
.device_id = "{{.Plg.Name}}",
.name = "{{.Plg.NameEn}}",
.kind = "computer",
.caps = caps,
.info_json = "{\"platform\":\"linux\",\"arch\":\"x86_64\"}",
},
.handlers = handlers, /* 声明式命令处理表 */
.on_state = on_state,
.ping_interval = 30,
};
ha_client_t *client = ha_client_new(&config);
if (!client) {
fprintf(stderr, "Failed to create client\n");
return 1;
}
printf("Starting remote device adapter: {{.Plg.Name}}\n");
printf(" Server: %s\n", config.server);
printf(" Device ID: %s\n", config.device.device_id);
printf(" Kind: %s\n", config.device.kind);
printf(" Caps: ");
for (const char **p = caps; *p; p++) printf("%s ", *p);
printf("\n");
ha_status_t st = ha_client_start(client);
if (st != HA_OK) {
fprintf(stderr, "Failed to connect: %d\n", st);
ha_client_destroy(client);
return 1;
}
printf("Connected! Entering main loop...\n");
/* 主循环 */
while (1) {
ha_status_t st = ha_client_process(client);
if (st == HA_ERR_DISCONNECTED) {
printf("Disconnected, exiting.\n");
break;
}
#if defined(_WIN32) || defined(_WIN64)
Sleep(10);
#else
usleep(10000);
#endif
}
ha_client_stop(client);
ha_client_destroy(client);
return 0;
}
`
const tmplRemoteDeviceCMake = `cmake_minimum_required(VERSION 3.10)
project({{.Plg.Name}} VERSION 0.1.0 LANGUAGES C)
# ============================================================
# {{.Plg.Name}} — Remote Device Adapter
# ============================================================
# 设置 SDK 路径(默认使用内置 SDK也可通过 -DSDK_PATH=... 指定)
set(SDK_PATH "${CMAKE_CURRENT_SOURCE_DIR}/ha_remotedevice"
CACHE PATH "Path to ha_remotedevice SDK")
# 添加 SDK 子目录
if(EXISTS "${SDK_PATH}/CMakeLists.txt")
add_subdirectory(${SDK_PATH} ha_remotedevice)
else()
message(FATAL_ERROR "ha_remotedevice SDK not found at ${SDK_PATH}")
endif()
# 创建设备适配器可执行文件
add_executable(${PROJECT_NAME}
main.c
)
# 链接 SDK
target_link_libraries(${PROJECT_NAME} PRIVATE ha_remotedevice)
# 包含 SDK 头文件
target_include_directories(${PROJECT_NAME} PRIVATE
${HA_REMOTEDEVICE_INCLUDE_DIR}
)
# 编译选项
if(CMAKE_C_COMPILER_ID MATCHES "GNU|Clang")
target_compile_options(${PROJECT_NAME} PRIVATE
-Wall -Wextra -Wpedantic
-Wno-unused-parameter
)
endif()
# 安装
install(TARGETS ${PROJECT_NAME} RUNTIME DESTINATION bin)
`
const tmplReadme = `# {{.Plg.Name}}
{{.Plg.Description}}
## Build
` + "```bash" + `
hmapdev build
` + "```" + `
## Channels
入站与出站是分开登记的两件事:
| 方向 | API | 用途 |
|---|---|---|
| 入站 inputch | RegisterInputChannel(name, def) | 声明「谁会往这个通道注入输入」。**凡是用 InjectText*/InjectInput*/InjectInterrupt*(source, "<name>", ...) 注入的通道名都要登记** |
| 出站 output | RegisterOutputChannel(name, caps, desc, def, handler) | 声明 output_send__<name> 的回复发给谁handler 收到 {payload,type,meta} |
defChannelDef描述该通道在记忆计算层的行为NoMemory: true = 该通道输入不进记忆;
Cleaner = 计算层清洗后再向量化/提关键词(原文不改)。
> 只登记出站通道、却用同名通道注入输入时,内核会兜底登记同名 inputch 并在日志里告警。
> 兜底只为兼容老插件 —— 请显式登记,让「这是入站通道」成为插件的明确意图。
## Install
Upload the .hmap file through the Plugin Manager API.
`