diff --git a/README.md b/README.md index b33396e..af686f9 100644 --- a/README.md +++ b/README.md @@ -48,6 +48,7 @@ - 可在 `gateway.token` 配置网关注册令牌;子节点注册/续租需带 `Authorization: Bearer ` - 可在 `NodeInfo` 中配置 `token`,relay 会自动附加 `Authorization: Bearer ` - `nodes` 工具支持设备快捷参数:`facing`、`duration_ms`、`command` +- 设备动作响应统一:`ok/code/error/payload`(code 示例:`ok` `unsupported_action` `transport_error`) 实现位置: - `pkg/nodes/types.go` diff --git a/README_EN.md b/README_EN.md index ec29f44..88eeea5 100644 --- a/README_EN.md +++ b/README_EN.md @@ -48,6 +48,7 @@ A `nodes` tool control-plane PoC is now available: - configure `gateway.token` as registration token; child nodes must send `Authorization: Bearer ` for register/heartbeat - `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`) Implementation: - `pkg/nodes/types.go` diff --git a/pkg/agent/loop.go b/pkg/agent/loop.go index c5d5171..322626f 100644 --- a/pkg/agent/loop.go +++ b/pkg/agent/loop.go @@ -82,7 +82,7 @@ func NewAgentLoop(cfg *config.Config, msgBus *bus.MessageBus, provider providers nodesManager.RegisterHandler("local", func(req nodes.Request) nodes.Response { switch req.Action { case "run": - payload := map[string]interface{}{"transport": "relay-local"} + payload := map[string]interface{}{"transport": "relay-local", "simulated": true} if cmdRaw, ok := req.Args["command"].([]interface{}); ok && len(cmdRaw) > 0 { parts := make([]string, 0, len(cmdRaw)) for _, x := range cmdRaw { @@ -90,11 +90,23 @@ func NewAgentLoop(cfg *config.Config, msgBus *bus.MessageBus, provider providers } payload["command"] = parts } - return nodes.Response{OK: true, Node: "local", Action: req.Action, Payload: payload} - case "camera_snap", "camera_clip", "screen_record", "screen_snapshot", "location_get", "canvas_snapshot", "canvas_action": - return nodes.Response{OK: true, Node: "local", Action: req.Action, Payload: map[string]interface{}{"transport": "relay-local", "simulated": true, "args": req.Args}} + 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}} + 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}} + 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}} + 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}} + 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"}} + 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}} + 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: - return nodes.Response{OK: true, Node: "local", Action: req.Action, Payload: map[string]interface{}{"echo": req.Args, "transport": "relay-local"}} + return nodes.Response{OK: false, Code: "unsupported_action", Node: "local", Action: req.Action, Error: "unsupported local simulated action"} } }) nodesRouter := &nodes.Router{P2P: &nodes.StubP2PTransport{}, Relay: &nodes.HTTPRelayTransport{Manager: nodesManager}} diff --git a/pkg/nodes/transport.go b/pkg/nodes/transport.go index 21e338d..8fe0ba0 100644 --- a/pkg/nodes/transport.go +++ b/pkg/nodes/transport.go @@ -96,18 +96,18 @@ func actionHTTPPath(action string) string { func (s *HTTPRelayTransport) Send(ctx context.Context, req Request) (Response, error) { if s.Manager == nil { - return Response{OK: false, Node: req.Node, Action: req.Action, Error: "relay manager not configured"}, nil + return Response{OK: false, Code: "relay_unavailable", Node: req.Node, Action: req.Action, Error: "relay manager not configured"}, nil } if resp, ok := s.Manager.Invoke(req); ok { return resp, nil } n, ok := s.Manager.Get(req.Node) if !ok { - return Response{OK: false, Node: req.Node, Action: req.Action, Error: "node not found"}, nil + return Response{OK: false, Code: "node_not_found", Node: req.Node, Action: req.Action, Error: "node not found"}, nil } endpoint := strings.TrimRight(strings.TrimSpace(n.Endpoint), "/") if endpoint == "" { - return Response{OK: false, Node: req.Node, Action: req.Action, Error: "node endpoint not configured"}, nil + return Response{OK: false, Code: "endpoint_missing", Node: req.Node, Action: req.Action, Error: "node endpoint not configured"}, nil } client := s.Client if client == nil { @@ -125,13 +125,13 @@ func (s *HTTPRelayTransport) Send(ctx context.Context, req Request) (Response, e } hresp, err := client.Do(hreq) if err != nil { - return Response{OK: false, Node: req.Node, Action: req.Action, Error: err.Error()}, nil + return Response{OK: false, Code: "transport_error", Node: req.Node, Action: req.Action, Error: err.Error()}, nil } defer hresp.Body.Close() payload, _ := io.ReadAll(io.LimitReader(hresp.Body, 1<<20)) var resp Response if err := json.Unmarshal(payload, &resp); err != nil { - return Response{OK: false, Node: req.Node, Action: req.Action, Error: fmt.Sprintf("invalid node response: %s", strings.TrimSpace(string(payload)))}, nil + return Response{OK: false, Code: "invalid_response", Node: req.Node, Action: req.Action, Error: fmt.Sprintf("invalid node response: %s", strings.TrimSpace(string(payload)))}, nil } if strings.TrimSpace(resp.Node) == "" { resp.Node = req.Node @@ -139,5 +139,12 @@ func (s *HTTPRelayTransport) Send(ctx context.Context, req Request) (Response, e if strings.TrimSpace(resp.Action) == "" { resp.Action = req.Action } + if strings.TrimSpace(resp.Code) == "" { + if resp.OK { + resp.Code = "ok" + } else { + resp.Code = "remote_error" + } + } return resp, nil } diff --git a/pkg/nodes/types.go b/pkg/nodes/types.go index 0a9ba24..20611e2 100644 --- a/pkg/nodes/types.go +++ b/pkg/nodes/types.go @@ -36,6 +36,7 @@ type Request struct { // Envelope for node responses. type Response struct { OK bool `json:"ok"` + Code string `json:"code,omitempty"` Error string `json:"error,omitempty"` Node string `json:"node,omitempty"` Action string `json:"action,omitempty"`