tighten device action args validation and normalize media payload schema

This commit is contained in:
DBT
2026-02-25 01:23:25 +00:00
parent 68145f8185
commit a21237c1e4
5 changed files with 44 additions and 6 deletions

View File

@@ -49,6 +49,7 @@
- 可在 `NodeInfo` 中配置 `token`relay 会自动附加 `Authorization: Bearer <token>`
- `nodes` 工具支持设备快捷参数:`facing``duration_ms``command`
- 设备动作响应统一:`ok/code/error/payload`code 示例:`ok` `unsupported_action` `transport_error`
- 设备 `payload` 规范字段:`media_type` `storage` `url|path|image` `meta`
实现位置:
- `pkg/nodes/types.go`

View File

@@ -49,6 +49,7 @@ A `nodes` tool control-plane PoC is now available:
- `NodeInfo.token` is supported; relay automatically sets `Authorization: Bearer <token>`
- `nodes` tool supports device shortcuts: `facing`, `duration_ms`, `command`
- unified device response envelope: `ok/code/error/payload` (code examples: `ok`, `unsupported_action`, `transport_error`)
- device `payload` normalized fields: `media_type` `storage` `url|path|image` `meta`
Implementation:
- `pkg/nodes/types.go`

View File

@@ -93,17 +93,17 @@ func NewAgentLoop(cfg *config.Config, msgBus *bus.MessageBus, provider providers
}
return nodes.Response{OK: true, Code: "ok", Node: "local", Action: req.Action, Payload: payload}
case "camera_snap":
return nodes.Response{OK: true, Code: "ok", Node: "local", Action: req.Action, Payload: map[string]interface{}{"transport": "relay-local", "media_type": "image", "facing": req.Args["facing"], "simulated": true}}
return nodes.Response{OK: true, Code: "ok", Node: "local", Action: req.Action, Payload: map[string]interface{}{"transport": "relay-local", "media_type": "image", "storage": "inline", "facing": req.Args["facing"], "simulated": true, "meta": map[string]interface{}{"width": 1280, "height": 720}}}
case "camera_clip":
return nodes.Response{OK: true, Code: "ok", Node: "local", Action: req.Action, Payload: map[string]interface{}{"transport": "relay-local", "media_type": "video", "duration_ms": req.Args["duration_ms"], "simulated": true}}
return nodes.Response{OK: true, Code: "ok", Node: "local", Action: req.Action, Payload: map[string]interface{}{"transport": "relay-local", "media_type": "video", "storage": "path", "path": "/tmp/camera_clip.mp4", "duration_ms": req.Args["duration_ms"], "simulated": true, "meta": map[string]interface{}{"fps": 30}}}
case "screen_snapshot":
return nodes.Response{OK: true, Code: "ok", Node: "local", Action: req.Action, Payload: map[string]interface{}{"transport": "relay-local", "media_type": "image", "simulated": true}}
return nodes.Response{OK: true, Code: "ok", Node: "local", Action: req.Action, Payload: map[string]interface{}{"transport": "relay-local", "media_type": "image", "storage": "inline", "simulated": true, "meta": map[string]interface{}{"width": 1920, "height": 1080}}}
case "screen_record":
return nodes.Response{OK: true, Code: "ok", Node: "local", Action: req.Action, Payload: map[string]interface{}{"transport": "relay-local", "media_type": "video", "duration_ms": req.Args["duration_ms"], "simulated": true}}
return nodes.Response{OK: true, Code: "ok", Node: "local", Action: req.Action, Payload: map[string]interface{}{"transport": "relay-local", "media_type": "video", "storage": "path", "path": "/tmp/screen_record.mp4", "duration_ms": req.Args["duration_ms"], "simulated": true, "meta": map[string]interface{}{"fps": 30}}}
case "location_get":
return nodes.Response{OK: true, Code: "ok", Node: "local", Action: req.Action, Payload: map[string]interface{}{"transport": "relay-local", "lat": 0.0, "lng": 0.0, "accuracy": "simulated"}}
return nodes.Response{OK: true, Code: "ok", Node: "local", Action: req.Action, Payload: map[string]interface{}{"transport": "relay-local", "lat": 0.0, "lng": 0.0, "accuracy": "simulated", "meta": map[string]interface{}{"provider": "simulated"}}}
case "canvas_snapshot":
return nodes.Response{OK: true, Code: "ok", Node: "local", Action: req.Action, Payload: map[string]interface{}{"transport": "relay-local", "image": "data:image/png;base64,<simulated>", "simulated": true}}
return nodes.Response{OK: true, Code: "ok", Node: "local", Action: req.Action, Payload: map[string]interface{}{"transport": "relay-local", "image": "data:image/png;base64,<simulated>", "media_type": "image", "storage": "inline", "simulated": true, "meta": map[string]interface{}{"width": 1280, "height": 720}}}
case "canvas_action":
return nodes.Response{OK: true, Code: "ok", Node: "local", Action: req.Action, Payload: map[string]interface{}{"transport": "relay-local", "applied": true, "simulated": true, "args": req.Args}}
default:

View File

@@ -146,5 +146,36 @@ func (s *HTTPRelayTransport) Send(ctx context.Context, req Request) (Response, e
resp.Code = "remote_error"
}
}
resp.Payload = normalizeDevicePayload(resp.Action, resp.Payload)
return resp, nil
}
func normalizeDevicePayload(action string, payload map[string]interface{}) map[string]interface{} {
if payload == nil {
payload = map[string]interface{}{}
}
a := strings.ToLower(strings.TrimSpace(action))
switch a {
case "camera_snap", "screen_snapshot", "canvas_snapshot":
if _, ok := payload["media_type"]; !ok {
payload["media_type"] = "image"
}
case "camera_clip", "screen_record":
if _, ok := payload["media_type"]; !ok {
payload["media_type"] = "video"
}
}
if _, ok := payload["storage"]; !ok {
if _, hasURL := payload["url"]; hasURL {
payload["storage"] = "url"
} else if _, hasPath := payload["path"]; hasPath {
payload["storage"] = "path"
} else if _, hasInline := payload["image"]; hasInline {
payload["storage"] = "inline"
}
}
if _, ok := payload["meta"]; !ok {
payload["meta"] = map[string]interface{}{}
}
return payload
}

View File

@@ -95,6 +95,11 @@ func (t *NodesTool) Execute(ctx context.Context, args map[string]interface{}) (s
}
reqArgs["duration_ms"] = di
}
if action == "canvas_action" {
if act, _ := reqArgs["action"].(string); strings.TrimSpace(act) == "" {
return "", fmt.Errorf("invalid_args: canvas_action requires args.action")
}
}
resp, err := t.router.Dispatch(ctx, nodes.Request{Action: action, Node: nodeID, Args: reqArgs}, mode)
if err != nil {
return "", err