mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-09-22 09:58:06 +00:00
回应 GUI c29abe9 需服务端配套项([阻塞]两项 + [中]一项):
1. readFrame 支持 0x2 二进制帧(之前遇 0x2 直接断连):
- 返回 opcode, 二进制帧上限放宽至 8MB
2. handleWS 二进制聚合协议(设备→网关, 录像回传):
- cmd_data_start 开启按 req_id 聚合 → 0x2 帧追加 → cmd_data_end 聚合完成
- 结果存 cmdresult(data_base64 字段), device_ctl_cmdresult 可取回
- 超限防护(声明 total×2 或硬上限 64MB)
3. PushData 下发协议(网关→设备, 音频/TTS):
- cmd_speech_start → 0x2 分块(8KB) → cmd_speech_end
- GUI 端已实现接收侧(speakeruse 播放链路打通)
4. device_ctl_cmdrun 工具描述补齐:
- screensue 带参示例(<内容>/<秒> <内容>)
- camerasue 录像说明(cmdresult 含 data_base64)
- speakeruse 文字朗读
测试: 最小 WS 客户端端到端验证二进制分块上传/PushData 下发/离线报错
286 lines
11 KiB
Go
286 lines
11 KiB
Go
package remotedevice
|
||
|
||
import (
|
||
"fmt"
|
||
"sort"
|
||
"strings"
|
||
"time"
|
||
|
||
agentIO "gitcode.com/JianFeeeee/HomeAgent/internal/agent/io"
|
||
)
|
||
|
||
// devicectlDevice 把设备网关暴露为 IOManager 的一个 Device:
|
||
// Tools() 提供 devicedetect / device_ctl_status / device_ctl_cmdrun / device_ctl_cmdresult,
|
||
// Execute() 检查授权并路由到 WS 在线设备。
|
||
type devicectlDevice struct {
|
||
reg *Registry
|
||
persist func() // 授权变更后持久化
|
||
}
|
||
|
||
func (d *devicectlDevice) Name() string { return "devicectl" }
|
||
func (d *devicectlDevice) Type() agentIO.DeviceType { return agentIO.DeviceIO }
|
||
func (d *devicectlDevice) OutputCapabilities() agentIO.OutputCapability { return agentIO.CapStructured }
|
||
func (d *devicectlDevice) Description() string {
|
||
return "远程设备控制网关:查看已接入/已授权的设备并发送控制指令(经用户授权的设备)"
|
||
}
|
||
func (d *devicectlDevice) ChannelDef() agentIO.ChannelDef { return agentIO.ChannelDef{} }
|
||
func (d *devicectlDevice) Start() error { return nil }
|
||
func (d *devicectlDevice) Stop() error { return nil }
|
||
|
||
func (d *devicectlDevice) Tools() []agentIO.ToolDef {
|
||
return []agentIO.ToolDef{
|
||
{
|
||
Name: "devicedetect",
|
||
Description: "扫描并列出已接入设备网关的设备(含在线/离线状态与授权状态)。" +
|
||
"用途:查看当前有哪些设备连接了 HomeAgent、是否在线、是否已授权。" +
|
||
"参数 kind 可选,只返回该种类的设备。返回值 device_id 用于后续 device_ctl_* 工具。",
|
||
Parameters: map[string]interface{}{
|
||
"type": "object",
|
||
"properties": map[string]interface{}{
|
||
"kind": map[string]interface{}{"type": "string", "description": "按设备种类过滤(light/camera/computer/phone/...),可省略"},
|
||
},
|
||
},
|
||
},
|
||
{
|
||
Name: "device_ctl_status",
|
||
Description: "查询一台已在线设备的实时状态。" +
|
||
"仅返回设备上报的状态信息(如电量/温度/运行状态)。" +
|
||
"需要 device_id(来自 devicedetect)。设备必须已授权且在线。",
|
||
Parameters: map[string]interface{}{
|
||
"type": "object",
|
||
"properties": map[string]interface{}{
|
||
"device_id": map[string]interface{}{"type": "string", "description": "目标设备 ID"},
|
||
},
|
||
"required": []interface{}{"device_id"},
|
||
},
|
||
},
|
||
{
|
||
Name: "device_ctl_cmdrun",
|
||
Description: "向设备下发命令/操作(异步,accepted=true 后用 device_ctl_cmdresult 轮询结果)。" +
|
||
"command 支持两类(前缀区分):\n" +
|
||
"- shell-cmd: 在设备上执行原生 shell 命令,如 shell-cmd ls -la /tmp\n" +
|
||
"- homeagent-cmd: 调用设备端 HomeAgent 内置能力:\n" +
|
||
" · homeagent-screensue <显示内容/HTML> — 用户侧屏幕弹窗显示自定义内容(带参显示,如 homeagent-screensue 会议提醒:三点开会)\n" +
|
||
" · homeagent-screensue <秒> <内容> — 显示指定时长后自动关闭;不带参数则常驻\n" +
|
||
" · homeagent-camerasue — 抓拍单张 jpeg(结果为 base64 data URL)\n" +
|
||
" · homeagent-camerasue <N秒> — 录像 N 秒 mp4(二进制分块回传,cmdresult 含 data_base64 字段)\n" +
|
||
" · homeagent-speakeruse <文字> — 设备端 TTS 语音朗读文字\n" +
|
||
"⚡ 高危:设备必须已授权,且该操作会改变设备行为。" +
|
||
"返回 accepted=true 表示已下发并等待设备执行,之后可用 device_ctl_cmdresult 查询结果。" +
|
||
"若设备未授权或离线,返回错误信息。",
|
||
Parameters: map[string]interface{}{
|
||
"type": "object",
|
||
"properties": map[string]interface{}{
|
||
"device_id": map[string]interface{}{"type": "string", "description": "目标设备 ID"},
|
||
"command": map[string]interface{}{"type": "string", "description": "以 shell-cmd 或 homeagent-cmd 前缀开头。如 shell-cmd pwd、homeagent-screensue 三点开会、homeagent-camerasue 5(录5秒)、homeagent-speakeruse 你好"},
|
||
},
|
||
"required": []interface{}{"device_id", "command"},
|
||
},
|
||
},
|
||
{
|
||
Name: "device_ctl_cmdresult",
|
||
Description: "查询之前 device_ctl_cmdrun 下发命令的执行结果(按 req_id 或 device_id 最近一次)。" +
|
||
"若非阻塞或已超时,用此工具取回设备执行输出。",
|
||
Parameters: map[string]interface{}{
|
||
"type": "object",
|
||
"properties": map[string]interface{}{
|
||
"req_id": map[string]interface{}{"type": "string", "description": "命令请求 ID(device_ctl_cmdrun 返回)"},
|
||
"device_id": map[string]interface{}{"type": "string", "description": "设备 ID(查询最近一次结果)"},
|
||
},
|
||
},
|
||
},
|
||
{
|
||
Name: "deviceinfo",
|
||
Description: "探查一台设备接入网关时声明的详细信息与支持能力。" +
|
||
"返回设备的 OS/架构/CPU/内存/能力 caps 等(设备接入时上报,非实时)。" +
|
||
"需要 device_id(来自 devicedetect)。设备必须已授权。",
|
||
Parameters: map[string]interface{}{
|
||
"type": "object",
|
||
"properties": map[string]interface{}{
|
||
"device_id": map[string]interface{}{"type": "string", "description": "目标设备 ID"},
|
||
},
|
||
"required": []interface{}{"device_id"},
|
||
},
|
||
},
|
||
}
|
||
}
|
||
|
||
func (d *devicectlDevice) Execute(tool string, args map[string]interface{}) (interface{}, error) {
|
||
switch tool {
|
||
case "devicedetect":
|
||
return d.detect(args)
|
||
case "device_ctl_status":
|
||
return d.status(args)
|
||
case "device_ctl_cmdrun":
|
||
return d.cmdrun(args)
|
||
case "device_ctl_cmdresult":
|
||
return d.cmdresult(args)
|
||
case "deviceinfo":
|
||
return d.info(args)
|
||
default:
|
||
return nil, fmt.Errorf("unknown device tool %s", tool)
|
||
}
|
||
}
|
||
|
||
// publicDevices 把设备列表转为简洁 JSON(无内部字段)。
|
||
func publicDevices(in []DeviceMeta) []map[string]interface{} {
|
||
out := make([]map[string]interface{}, 0, len(in))
|
||
for _, m := range in {
|
||
entry := map[string]interface{}{
|
||
"device_id": m.DeviceID,
|
||
"name": m.Name,
|
||
"kind": m.Kind,
|
||
"caps": m.Caps,
|
||
"authorized": m.Authorized,
|
||
"online": m.Online,
|
||
"last_seen": m.LastSeen,
|
||
}
|
||
if len(m.Info) > 0 {
|
||
entry["info"] = m.Info
|
||
}
|
||
out = append(out, entry)
|
||
}
|
||
return out
|
||
}
|
||
|
||
// detect 实现 devicedetect。
|
||
func (d *devicectlDevice) detect(args map[string]interface{}) (interface{}, error) {
|
||
kind, _ := args["kind"].(string)
|
||
devs := d.reg.List()
|
||
if kind != "" {
|
||
var filtered []DeviceMeta
|
||
for _, m := range devs {
|
||
if strings.EqualFold(m.Kind, kind) {
|
||
filtered = append(filtered, m)
|
||
}
|
||
}
|
||
devs = filtered
|
||
}
|
||
sort.Slice(devs, func(i, j int) bool { return devs[i].DeviceID < devs[j].DeviceID })
|
||
if len(devs) == 0 {
|
||
return map[string]interface{}{"devices": []interface{}{}, "message": "暂无设备接入"}, nil
|
||
}
|
||
return map[string]interface{}{"devices": publicDevices(devs)}, nil
|
||
}
|
||
|
||
// status 实现 device_ctl_status(只读,查询设备最后一次上报状态)。
|
||
func (d *devicectlDevice) status(args map[string]interface{}) (interface{}, error) {
|
||
id, _ := args["device_id"].(string)
|
||
if id == "" {
|
||
return nil, fmt.Errorf("device_id required")
|
||
}
|
||
m, ok := d.reg.Get(id)
|
||
if !ok {
|
||
return nil, fmt.Errorf("device %s 不存在", id)
|
||
}
|
||
if !m.Authorized {
|
||
return nil, fmt.Errorf("device %s 未授权,无法查询状态(需先在设备管理页或经 bind 授权)", id)
|
||
}
|
||
if !m.Online {
|
||
return publicDevices([]DeviceMeta{m}), nil // 带 offline=true
|
||
}
|
||
return publicDevices([]DeviceMeta{m}), nil
|
||
}
|
||
|
||
// cmdrun 实现 device_ctl_cmdrun:检查授权+在线,push 命令,异步等待结果。
|
||
func (d *devicectlDevice) cmdrun(args map[string]interface{}) (interface{}, error) {
|
||
id, _ := args["device_id"].(string)
|
||
cmd, _ := args["command"].(string)
|
||
if id == "" || cmd == "" {
|
||
return nil, fmt.Errorf("device_id and command required")
|
||
}
|
||
m, ok := d.reg.Get(id)
|
||
if !ok {
|
||
return nil, fmt.Errorf("device %s 不存在", id)
|
||
}
|
||
if !m.Authorized {
|
||
return nil, fmt.Errorf("device %s 未授权,无法执行命令(请先在设备管理页授权)", id)
|
||
}
|
||
if !m.Online {
|
||
return nil, fmt.Errorf("device %s 不在线,无法执行命令", id)
|
||
}
|
||
// 命令类型:shell-cmd / homeagent-* 前缀区分;无前缀按 shell 处理(兼容旧格式)
|
||
cmdType := "shell"
|
||
switch {
|
||
case strings.HasPrefix(cmd, "shell-cmd"):
|
||
cmdType = "shell"
|
||
cmd = strings.TrimSpace(strings.TrimPrefix(cmd, "shell-cmd"))
|
||
case strings.HasPrefix(cmd, "homeagent-cmd"):
|
||
cmdType = "homeagent"
|
||
cmd = strings.TrimSpace(strings.TrimPrefix(cmd, "homeagent-cmd"))
|
||
case strings.HasPrefix(cmd, "homeagent-"):
|
||
cmdType = "homeagent"
|
||
cmd = strings.TrimSpace(strings.TrimPrefix(cmd, "homeagent-"))
|
||
default:
|
||
cmdType = "shell"
|
||
}
|
||
reqID := newReqID()
|
||
if err := d.reg.PushCmd(id, reqID, cmd, cmdType); err != nil {
|
||
return nil, fmt.Errorf("下发命令失败: %w", err)
|
||
}
|
||
// 阻塞等待设备结果(带超时);结果同时由 registry 留档。
|
||
res, err := d.reg.AwaitResult(reqID, 30*time.Second)
|
||
if err != nil {
|
||
d.reg.SaveResult(reqID, map[string]interface{}{"accepted": true, "error": err.Error(), "pending": true})
|
||
return map[string]interface{}{"accepted": true, "req_id": reqID, "pending": true, "note": "命令已下发但设备未在超时内回执,可用 device_ctl_cmdresult 再查"}, nil
|
||
}
|
||
out := res
|
||
out["req_id"] = reqID
|
||
d.reg.SaveResult(reqID, out)
|
||
return out, nil
|
||
}
|
||
|
||
// cmdresult 实现 device_ctl_cmdresult。
|
||
func (d *devicectlDevice) cmdresult(args map[string]interface{}) (interface{}, error) {
|
||
reqID, _ := args["req_id"].(string)
|
||
deviceID, _ := args["device_id"].(string)
|
||
if reqID != "" {
|
||
res, ok := d.reg.GetResult(reqID)
|
||
if !ok {
|
||
return nil, fmt.Errorf("no result for req %s(设备可能尚未回执)", reqID)
|
||
}
|
||
return res, nil
|
||
}
|
||
if deviceID != "" {
|
||
// 返回该设备最近一次结果(简化:遍历 results 找 device_id 匹配的最近一条)
|
||
// 说明:当前只按 req_id 查询;device_id 查询留给后续迭代。
|
||
m, ok := d.reg.Get(deviceID)
|
||
if !ok {
|
||
return nil, fmt.Errorf("device %s 不存在", deviceID)
|
||
}
|
||
if !m.Authorized {
|
||
return nil, fmt.Errorf("device %s 未授权", deviceID)
|
||
}
|
||
return map[string]interface{}{"device_id": deviceID, "note": "请用 device_ctl_cmdrun 返回的 req_id 查询命令结果"}, nil
|
||
}
|
||
return nil, fmt.Errorf("req_id 或 device_id 至少提供一个")
|
||
}
|
||
|
||
// info 实现 deviceinfo:返回设备详情 + 支持能力。
|
||
func (d *devicectlDevice) info(args map[string]interface{}) (interface{}, error) {
|
||
id, _ := args["device_id"].(string)
|
||
if id == "" {
|
||
return nil, fmt.Errorf("device_id required")
|
||
}
|
||
m, ok := d.reg.Get(id)
|
||
if !ok {
|
||
return nil, fmt.Errorf("device %s 不存在", id)
|
||
}
|
||
if !m.Authorized {
|
||
return nil, fmt.Errorf("device %s 未授权,无法探查信息(请先在设备管理页授权)", id)
|
||
}
|
||
out := map[string]interface{}{
|
||
"device_id": m.DeviceID,
|
||
"name": m.Name,
|
||
"kind": m.Kind,
|
||
"caps": m.Caps,
|
||
"authorized": m.Authorized,
|
||
"online": m.Online,
|
||
"last_seen": m.LastSeen,
|
||
}
|
||
if len(m.Info) > 0 {
|
||
out["info"] = m.Info
|
||
}
|
||
return out, nil
|
||
}
|