feat: 设备能力矩阵校验 + 设备主动上报事件通道

回应架构讨论: 外接设备各自声明能力, 且补齐设备→agent 单向推送缺口。

1. 能力矩阵 (registry.go):
   - capabilityTools 映射: caps 声明 → 可用工具
     screen/screensue/screensee, computeruse, clipboard(see/sue),
     camera/camerasue, speaker/speakeruse
   - SupportsTool 校验: screensee/computeruse/clipboard* 执行前检查
     目标设备是否声明对应能力, 未声明直接报错(不再下发到设备端才失败)
   - 兼容规则: 声明 cmd/cmdrun 等历史值 → 全能力;
     未声明任何已知能力 → 全能力(旧设备兼容);
     有已知能力声明则严格匹配

2. 设备主动上报事件 (WS op=event):
   - 设备可推 {op:event, type, detail/payload} 无需回执
   - 插件层 SetEventHandler 回调: 格式化为人类可读文本,
     经 SDK InjectInput 异步注入 agent(source=device/{id}, 回复路由回同通道)
   - 同设备同类型事件 10s 节流防传感器风暴
   - 场景: 摄像头识别未知人员驻留主动告警, agent 收到后自主处置

测试: 能力矩阵8场景 + 摄像头调screensee被拒 + 事件上报回调(含device_id回退)
This commit is contained in:
JianFeeeee
2026-08-21 19:58:37 +08:00
parent 9a39273d5c
commit 8d193f0af9
4 changed files with 262 additions and 4 deletions

View File

@ -391,6 +391,9 @@ func (d *devicectlDevice) screensee(args map[string]interface{}) (interface{}, e
if !m.Online {
return nil, fmt.Errorf("device %s 不在线", id)
}
if !d.reg.SupportsTool(id, "screensee") {
return nil, fmt.Errorf("device %s 未声明 screensee 能力(caps=%v),无法截屏", id, m.Caps)
}
reqID := newReqID()
if err := d.reg.PushCmd(id, reqID, "screensee", "homeagent"); err != nil {
return nil, fmt.Errorf("下发截屏命令失败: %w", err)
@ -439,6 +442,9 @@ func (d *devicectlDevice) computeruse(args map[string]interface{}) (interface{},
if !m.Online {
return nil, fmt.Errorf("device %s 不在线", id)
}
if !d.reg.SupportsTool(id, "computeruse") {
return nil, fmt.Errorf("device %s 未声明 computeruse 能力(caps=%v),无法操控鼠标键盘", id, m.Caps)
}
// 构造 GUI 端约定的 JSON 参数(坐标相对 screensueDisplay 所选屏)
params := map[string]interface{}{"action": action}
@ -492,8 +498,9 @@ func (d *devicectlDevice) computeruse(args map[string]interface{}) (interface{},
return out, nil
}
// clipboardCheck 检查设备可操作性(存在/已授权/在线),返回错误或 nil。
func (d *devicectlDevice) clipboardCheck(id, verb string) error {
// clipboardCheck 检查设备可操作性(存在/已授权/在线/能力声明),返回错误或 nil。
// tool 为空时跳过能力校验(如 device_ctl_cmdrun 由自身逻辑处理)。
func (d *devicectlDevice) clipboardCheck(id, verb, tool string) error {
if id == "" {
return fmt.Errorf("device_id required")
}
@ -507,6 +514,9 @@ func (d *devicectlDevice) clipboardCheck(id, verb string) error {
if !m.Online {
return fmt.Errorf("device %s 不在线", id)
}
if tool != "" && !d.reg.SupportsTool(id, tool) {
return fmt.Errorf("device %s 未声明 %s 能力(caps=%v),无法执行此操作", id, tool, m.Caps)
}
return nil
}
@ -514,7 +524,7 @@ func (d *devicectlDevice) clipboardCheck(id, verb string) error {
// 协议(GUI 配套):homeagent-clipboardsee → 回执 output 字段为剪切板文字。
func (d *devicectlDevice) clipboardsee(args map[string]interface{}) (interface{}, error) {
id, _ := args["device_id"].(string)
if err := d.clipboardCheck(id, "读取"); err != nil {
if err := d.clipboardCheck(id, "读取", "clipboardsee"); err != nil {
return nil, err
}
reqID := newReqID()
@ -551,7 +561,7 @@ func (d *devicectlDevice) clipboardsue(args map[string]interface{}) (interface{}
if text == "" {
return nil, fmt.Errorf("text required(要写入剪切板的内容)")
}
if err := d.clipboardCheck(id, "写入"); err != nil {
if err := d.clipboardCheck(id, "写入", "clipboardsue"); err != nil {
return nil, err
}
reqID := newReqID()