mirror of
https://gitcode.com/JianFeeeee/homeagent-sdk.git
synced 2026-09-20 00:48:12 +00:00
## 背景:内核侧发现的真问题
在真实二进制压力测试里发现:插件只调 `RegisterOutputChannel("cli", ...)`,
却用同一个通道名 `InjectTextSync("cli", ...)` 注入输入 ⇒ 内核 inputch 登记表里
**没有**这个通道,"把 inputch 划给驻留子"直接失败(`划入 inputch cli: inputch 未注册`)。
根因是**契约没有落到插件与 SDK 面上**:inputch 是内核最基本的**输入路由单位**,
"谁会往这个通道注入输入"必须显式声明,而 SDK 文档没说清它与 RegisterOutputChannel
的分工,示例与模板工程也没有示范。
## SDK 面
- `RegisterInputChannel` / `RegisterOutputChannel` 的文档补齐**方向契约**:
入站(谁会注入)与出站(output_send__<name> 的回复发给谁)是分开登记的两件事;
凡是用 `InjectText*/InjectInput*/InjectInterrupt*(source, "<name>", ...)` 注入的
通道名都要 RegisterInputChannel。README 同步补了一段契约说明。
## 示例插件(全部补齐,之前只有 qq/weather 是对的)
`a2a`、`acp`、`browser`、`memo`:注入用 `p.name` ⇒ 登记 `p.name`;
`calendar`、`rss`:注入用字面量通道名 ⇒ 登记同名通道。
(这些插件此前是"能注入、但通道不在登记表里",与 cli 同类问题。)
## 模板工程(生成器 templates.go)
- `tmplPluginGo`:示范入站+出站两个方向(含 ChannelDef/NoMemory 说明与 `inputch 未注册` 的成因)。
- `tmplMainLua`:同样两个方向(`register_input_channel` / `register_output_channel`)。
- `tmplReadme`:新增 "Channels" 一节(方向对照表 + 兜底告警说明)。
- 实测:`hmapdev init` 生成的 Go/Lua 工程都含通道代码,Go 工程可构建打包出 `.hmap`;
`--lua` 工程同样生成通道代码。
## 生成器两处修正(都是实测踩出来的)
1. `sdk install --from <dir>`:install 原本只能从 Release 归档下载,而 SDK 开发期的新能力
(如 proc 桥要透传的 `InjectOptions.Priority`)还没发版 ⇒ 生成的工程必然编译失败
(`z_proc_gen.go: opts.Priority undefined`)。现在可用本地源码装一个版本并激活。
实测:`hmapdev sdk install --from <local sdk>` → 装成 v1.3.0 并激活 → 工程构建通过。
2. 构建前置校验 `sdkHasInjectPriority`:proc 桥模板需要 `InjectOptions.Priority`,
旧 SDK 没有时应给出**可执行**的报错(升级 SDK 或用 `--from`),
而不是把两条 `opts.Priority undefined` 编译错误甩给用户(那些错误指向生成物,
完全看不出是 SDK 版本问题)。实测:声明 sdk=1.2.0 的工程构建时正确命中该提示。
## 未决(发布期事项)
`InjectOptions.Priority` 属本特性线新增能力,**已发布的 SDK v1.2.0 不含它**;
发版时 SDK 版本需随之内含该能力(当前源码 meta 已是 1.3.0),否则外部开发者
按文档生成的工程会撞上上面那条守卫。
562 lines
20 KiB
Go
562 lines
20 KiB
Go
package main
|
||
|
||
// 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 收到 map:payload(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
|
||
}
|
||
`
|
||
|
||
const tmplSDKLua = `-- HomeAgent Lua Plugin SDK (standalone mock)
|
||
sdk = {}
|
||
function sdk.log(level, msg) print("[lua-plugin] " .. tostring(level) .. ": " .. tostring(msg)) end
|
||
function sdk.register_tool(name, def, handler) print("[lua-plugin] register_tool: " .. tostring(name)) end
|
||
function sdk.register_stage(stage, handler, scope) print("[lua-plugin] register_stage: " .. tostring(stage) .. " scope=" .. tostring(scope)) end
|
||
function sdk.register_api(name) print("[lua-plugin] register_api: " .. tostring(name)) end
|
||
function sdk.register_output_channel(name, caps, desc, def, handler) print("[lua-plugin] register_output_channel: " .. tostring(name)) end
|
||
function sdk.register_input_channel(name, def) print("[lua-plugin] register_input_channel: " .. tostring(name)) end
|
||
function sdk.get_setting(key) return nil end
|
||
function sdk.set_setting(key, value) print("[lua-plugin] set_setting: " .. tostring(key)) end
|
||
function sdk.inject_text(source, channel, text) print("[lua-plugin] inject_text: " .. tostring(source)) end
|
||
function sdk.inject_interrupt(source, channel, text) print("[lua-plugin] inject_interrupt: " .. tostring(source)) end
|
||
function sdk.inject_text_no_memory(source, channel, text) print("[lua-plugin] inject_text_no_memory: " .. tostring(source)) end
|
||
function sdk.set_auto_restart(enabled) print("[lua-plugin] set_auto_restart: " .. tostring(enabled)) end
|
||
sdk.memory = {}
|
||
function sdk.memory.recall(query, depth) return {entities={}, relations={}} end
|
||
function sdk.memory.commit(triples) return nil end
|
||
function sdk.memory.introspect() return {} end
|
||
function sdk.memory.merge(source, target) return 0 end
|
||
function sdk.memory.purge(criteria, hard) return 0 end
|
||
sdk.doc = {}
|
||
function sdk.doc.query(text, top_k) return {} end
|
||
function sdk.doc.insert(doc) return nil end
|
||
function sdk.doc.remove(id) return nil end
|
||
function sdk.doc.stats() return {} end
|
||
sdk.knowledge = {}
|
||
function sdk.knowledge.search(query, limit) return {} end
|
||
function sdk.knowledge.add(tag, content) return nil end
|
||
function sdk.knowledge.list() return {} end
|
||
sdk.text_memory = {}
|
||
function sdk.text_memory.append(evt) return nil end
|
||
sdk.llm = {}
|
||
function sdk.llm.list_sources() return {} end
|
||
function sdk.llm.set_source(name) return nil end
|
||
function sdk.llm.current_source() return nil end
|
||
sdk.social = {}
|
||
function sdk.social.get_person(name) return {} end
|
||
function sdk.social.get_network(name, depth) return {} end
|
||
function sdk.social.get_trait(name, trait) return {value=nil, found=false} end
|
||
function sdk.social.get_relations(name) return {} end
|
||
function sdk.social.list_persons() return {} end
|
||
sdk.settings = {}
|
||
function sdk.settings.get_core(key) return nil end
|
||
function sdk.settings.set_core(key, value) return nil end
|
||
function sdk.settings.list_core(prefix) return {} end
|
||
function sdk.settings.get_plugin(plugin, key) return nil end
|
||
function sdk.settings.set_plugin(plugin, key, value) return nil end
|
||
function sdk.settings.list_plugin(plugin, prefix) return {} end
|
||
function sdk.settings.list(prefix) return {} end
|
||
function sdk.settings.register_def(def) return nil end
|
||
function sdk.settings.defs(prefix) return {} end
|
||
function sdk.settings.dump() return {} end
|
||
function sdk.settings.plugins() return {} end
|
||
sdk.json = {}
|
||
function sdk.json.encode(val)
|
||
if type(val) == "string" then return '"' .. val:gsub('"', '\\"'):gsub('\n', '\\n') .. '"'
|
||
elseif type(val) == "number" or type(val) == "boolean" then return tostring(val)
|
||
elseif type(val) == "table" then local parts, i = {}, 1
|
||
for k, v in pairs(val) do parts[i] = sdk.json.encode(k) .. ":" .. sdk.json.encode(v); i = i + 1 end
|
||
return "{" .. table.concat(parts, ",") .. "}" end
|
||
return "null"
|
||
end
|
||
function sdk.json.decode(str) local ok, fn = pcall(load, "return " .. str); if ok then return fn() end; return nil end
|
||
sdk.http = {}
|
||
function sdk.http.get(url) print("[lua-plugin] http.get: " .. tostring(url)); return {status=200, body='{"mock":true}', headers={}} end
|
||
function sdk.http.post(url, body, ct) print("[lua-plugin] http.post: " .. tostring(url)); return {status=200, body='{"mock":true}', headers={}} end
|
||
return sdk
|
||
`
|
||
|
||
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 })
|
||
-- 出站 output:output_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} |
|
||
|
||
def(ChannelDef)描述该通道在记忆计算层的行为:NoMemory: true = 该通道输入不进记忆;
|
||
Cleaner = 计算层清洗后再向量化/提关键词(原文不改)。
|
||
|
||
> 只登记出站通道、却用同名通道注入输入时,内核会兜底登记同名 inputch 并在日志里告警。
|
||
> 兜底只为兼容老插件 —— 请显式登记,让「这是入站通道」成为插件的明确意图。
|
||
|
||
## Install
|
||
|
||
Upload the .hmap file through the Plugin Manager API.
|
||
`
|