From a21237c1e43a51a296e850154f996f5c3c7f4822 Mon Sep 17 00:00:00 2001 From: DBT Date: Wed, 25 Feb 2026 01:23:25 +0000 Subject: [PATCH] tighten device action args validation and normalize media payload schema --- README.md | 1 + README_EN.md | 1 + pkg/agent/loop.go | 12 ++++++------ pkg/nodes/transport.go | 31 +++++++++++++++++++++++++++++++ pkg/tools/nodes_tool.go | 5 +++++ 5 files changed, 44 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index af686f9..69681d4 100644 --- a/README.md +++ b/README.md @@ -49,6 +49,7 @@ - 可在 `NodeInfo` 中配置 `token`,relay 会自动附加 `Authorization: Bearer ` - `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` diff --git a/README_EN.md b/README_EN.md index 88eeea5..e40ac07 100644 --- a/README_EN.md +++ b/README_EN.md @@ -49,6 +49,7 @@ A `nodes` tool control-plane PoC is now available: - `NodeInfo.token` is supported; relay automatically sets `Authorization: Bearer ` - `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` diff --git a/pkg/agent/loop.go b/pkg/agent/loop.go index 4416337..617eaaf 100644 --- a/pkg/agent/loop.go +++ b/pkg/agent/loop.go @@ -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": 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,", "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: diff --git a/pkg/nodes/transport.go b/pkg/nodes/transport.go index 8fe0ba0..8c4ceb3 100644 --- a/pkg/nodes/transport.go +++ b/pkg/nodes/transport.go @@ -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 +} diff --git a/pkg/tools/nodes_tool.go b/pkg/tools/nodes_tool.go index a35595b..b8f2a7e 100644 --- a/pkg/tools/nodes_tool.go +++ b/pkg/tools/nodes_tool.go @@ -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