mirror of
https://gitcode.com/JianFeeeee/homeagent-sdk.git
synced 2026-10-31 14:27:30 +00:00
* feat(commondev): 主管式多仓库开发流水线 + sidecar 后端抽象(pi/zcode)
## 是什么
agent 统筹开发流程的插件:登记项目 → 开工程线(每条线一个隔离工作目录 +
一个 sidecar 子进程 + 一对通道)→ 投递任务 → 同会话续投 → 评审/合入。
38 个工具:project_*/branch_*/workdir_*/job_*/review_*/test_*/
endpoint_*/agent_*/pi_*/conflict_check。
## sidecar 后端抽象(本轮核心)
传输层(local/ssh)与后端(pi/zcode/自定义)**正交**,四个自由组合:
driver.json 描述一个后端:script / cliPath / env / args / llmConfig
三级查找:插件数据目录 → **随包 rawfile** → 内置 pi 描述
占位符:{workdir} {dataDir} {pluginDir} {rawfile} {sessionId}
★ 内置 pi 回退**与默认后端解耦**(2026-10-17 修):原实现写
`if cli == defaultAgentCLI { return builtinPiDriver() }`,把"给谁兜底"
和"默认选谁"耦合到同一常量 —— 一旦有人改默认,pi 当场失去兜底,
显式 cli="pi" 开始报「驱动不存在」。现在判断字面 "pi"。
默认后端保持 **pi**(我们本地主用);交付方用 zcode 走两种局部开关:
· 单线:commondev_branch_open(cli="zcode")
· 按机器:commondev_endpoint_set(host=..., cli="zcode")
## zcode 接入(三条真机判据,全部要求真改工程文件)
· TestZcodeFullPipeline(29-34s):随包 driver + 真实 ZCode CLI
· TestZcodeOverSSH(39s):真 ssh + scp,远端 node 跑 zcode
· TestZcodeOverSSHWithPerHostEndpoints(68s):两个 host 别名配不同
cli_path,走端点表而不是全局 settings
★ 会话库必须隔离到工作区(sidecar.mjs 的 ensureWorkspaceSessionStore):
实测 ZCode 的会话库路径**硬编码 homedir()**,`ZCODE_DATA_BASE_DIR` 对它
完全无效(26 个会话全落全局库)。但 `<workdir>/.zcode/config.json` 的
`storage.sessionDbPath` **确实生效**。所以启动前写一份工作区配置,
一举解决:多线不共用历史 / 不污染全局 ~/.zcode / 删线不留孤儿会话。
只在不存在时写 —— 用户手工放的要尊重。
## 其他
· rawfile:项目根 rawfile/ 默认全收,解到 <插件目录>/rawfile/,绝对路径拒绝
· 凭证:命名引用 + ${ENV_VAR},独立 0600 文件,绝不进 config.db(0644)
· secretshell:给不受信 agent 用的安全 shell(ackey + safeshell)
· 冲突检查 / 评审判据 / 镜像 / 每条线独立 workdir(hash 命名,合并后删除)
判据 30+ 条,含 4 条「判据本身的判据」(防止判据假绿)。
* fix(sdk): 协议通道纪律 + hmapdev 模板升级 + 示例插件 stdout 污染
## 核心缺陷:插件往 stdout 打日志 = 污染 JSON-RPC 协议通道
子进程插件与内核用 stdout 传 NDJSON 帧,stderr 才是日志通道。
脚手架模板(及 4 个示例插件)用 `fmt.Printf` 打日志 ⇒ 内核收到非 JSON 文本:
[proc] weather 收到非法 JSON 帧(21 字节): invalid character 'i'
**生产实测 1038 条**(huawei_smarthome 720 / weather 113 / rss 103 / calendar 102)。
后果不只是日志噪音:那一行日志被**丢弃**,插件看起来"没启动"。
修法:
- hmapdev 模板 `fmt.Printf` → `log.Printf`(9 处)
- 示例插件 4 个 14 处:ai_image(2) calendar(5) rss(4) weather(3)
+ 补 "log" import
- 内核侧(主仓)同步让报错可定位:内容摘要 + 可能原因 + 修法
判别法:`package main` = 子进程(协议通道,禁用 stdout 打日志);
普通库包 = 进程内插件(编译进内核,写真实终端,**不是**缺陷)——
`internal/plugins/cli` 的 `fmt.Println` 属后者。
## hmapdev
- `cmd_build.go` +136:平台产物精确匹配(`--target` 是**append** 语义,
原脚本 `ls dist/*.hmap | head -1` 会按字典序抓到 amd64,
**导致 arm64 包里装的是 x86-64 二进制**,且退出码 0、校验和通过)
- `cmd_init.go` +46:脚手架 stdio 纪律
- `templates/proc_main.go.tmpl`:补齐 mgrStop/mgrStart/mgrIsRuntimeStopped
三态能力(插件管理器运行态停启)
- `templates/plg.json.tmpl` 删除(合并进 cmd_init)
## 判据
- `scaffold_stdout_test.go`(3 条):扫模板生成的代码不得写 stdout。
★ 判据自身先假红过一次:**模板的注释里就写了 `fmt.Printf`** ⇒
必须 `stripTemplateComments()` 之后才判
- `example_stdout_test.go`(2 条):扫全部 20 个子进程示例插件,
报错给**文件:行号**;变异咬住并给出准确行号
- `package_examples_test.go`(2 条):**执行真实打包脚本**
(第一版测的是 hmapdev 而不是脚本 ⇒ 变异不咬)
- `cmd_build` / `cmd_init` 新判据
## 其他
- sdk/settings.go +15(三态能力透传)
- `docs/versions.md` +§示例插件协议通道纪律(含生产 1038 条实测表格)
- `skills/homeagent-plugin-dev/SKILL.md` +§协议通道纪律(正误代码对比)
* fix(examples): 恢复 go.mod 相对路径 + 移除 plg.json 的 sdk 固定(修 CI 红)
CI 上 `Examples (linux/amd64)` 与 `(darwin/arm64)` 双红,只有两个失败:
✗ commondev 构建失败
✗ editdoc 构建失败
error: 项目声明 sdk=1.4.0,但本地 SDK 存储里没有这个版本
其余 20 个示例全部成功 —— 说明不是通用问题,而是这两个被改坏了。
## 根因(两个叠加)
**① go.mod 被本机绝对路径污染**
我在上一个提交里用 `git add -A`,把本机 `go build` **自动改写**过的
go.mod 一并提交了:
require github.com/JianFeeeee/homeagentsdk v1.4.0
replace github.com/JianFeeeee/homeagentsdk => /root/.homeagent/hmapdev/sdk/v1.4.0
基线(origin/main)是:
require github.com/JianFeeeee/homeagentsdk v0.0.0
replace github.com/JianFeeeee/homeagentsdk => ../../
本机那个目录真的存在 ⇒ 本地全绿;CI 没有 ⇒ 必红。
**② plg.json 声明了 `"sdk": "1.4.0"`**
一声明版本,hmapdev 就去**本地 SDK store** 找它,找不到直接拒绝构建。
全仓 20+ 示例**都不声明**,只有我改过的这两个声明了。
示例的正确依赖表达是 go.mod 的 `v0.0.0` + `replace ../../`。
## 修法
两个文件都恢复到与其余示例一致的写法。
## 判据(3 条,都是新加的)
· `TestExampleGoModHasNoAbsoluteReplace`:任何示例 go.mod 不得含
`/root/`、`/home/`、`/Users/`、`C:\`
· `TestExampleGoModUsesRelativeSDKReplace`:replace 必须是相对路径 `../../`
· `TestExamplePlgJsonDoesNotPinSDKVersion`:plg.json 不得声明 sdk
变异 2/2 咬住(重新引入绝对路径 / 重新声明 sdk,都准确报出文件路径)。
## 本地验证(模拟 CI 干净环境)
用干净 HOME 跑 `package/build-examples.sh`:
· 修复前:成功 18 / 失败 4(commondev, editdoc, browser, rss)
· 修复后:成功 20 / 失败 2(browser, rss —— **本机代理不通**,
`proxy.golang.org` 超时;用本地 modcache 验证这两个可正常构建)
即 22/22 源码都能构建,CI 有网络不受影响。
## 教训
`git add -A` 会把「构建工具自动改写的文件」一起提交。
提交前必须 `git status` 逐个看,尤其 go.mod / go.sum。
---------
Co-authored-by: JianFeeeee <JianFeeeee@users.noreply.gitcode.com>
523 lines
18 KiB
Go
523 lines
18 KiB
Go
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}}",
|
||
"rawfiles": []
|
||
}
|
||
`
|
||
|
||
const tmplGoMod = `module {{.ModulePath}}
|
||
|
||
go {{.GoVersion}}
|
||
|
||
require {{.SDKModule}} {{.SDKVersion}}
|
||
{{if .SDKLocalPath}}
|
||
// SDK 指向本机源码。
|
||
//
|
||
// ★ 为何默认走 replace 而不是直接拉模块(2026-10-08 更新):
|
||
// SDK 已从 gitcode 迁到 GitHub(module 路径 github.com/JianFeeeee/homeagentsdk)。
|
||
// 仓库虽在 GitHub,但本工程模板仍默认指向**本机源码**,原因与托管地无关:
|
||
// (1)hmapdev 装的是本机 SDK(~/.homeagent/hmapdev/sdk),离线也能构建;
|
||
// (2)插件必须与内核的 SDK 版本严格一致,走网络拉会引入版本漂移;
|
||
// (3)内网/受限出口环境拉不到公共 proxy。
|
||
// 若你已有可访问的私有 proxy 或愿意走公网,可删掉本行。
|
||
replace {{.SDKModule}} => {{.SDKLocalPath}}
|
||
{{end}}`
|
||
|
||
const tmplPluginGo = `package main
|
||
|
||
import (
|
||
"log"
|
||
|
||
"github.com/JianFeeeee/homeagentsdk/sdk"
|
||
)
|
||
|
||
// ★★ 插件里**绝不能往 stdout 打印任何东西**(本注释是踩坑记录,2026-10-16)。
|
||
//
|
||
// 子进程插件的 **stdout 是 JSON-RPC 协议通道**(与内核的帧都走它),
|
||
// stderr 才是日志通道(SDK 侧 log.SetOutput(os.Stderr))。
|
||
//
|
||
// 用 fmt.Printf 打一行日志 ⇒ 内核 readLoop 收到一段非 JSON 文本,
|
||
// 报「收到非法 JSON 帧(N 字节)」并**丢弃**。日志本身丢了,还刷了一条
|
||
// 看起来像协议故障的报错(极难定位到「是我自己 printf 的」)。
|
||
//
|
||
// ⇒ 一律用 log.Printf(默认走 stderr)。
|
||
|
||
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() { log.Printf("[%s] stop handler running", 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)
|
||
log.Printf("[%s] started", p.name)
|
||
return nil
|
||
}
|
||
|
||
func (p *Plugin) Stop() error { log.Printf("[%s] stopped", 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 })
|
||
-- 出站 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.
|
||
`
|