standardize device operation responses with codes and richer local simulations

This commit is contained in:
DBT
2026-02-24 16:27:37 +00:00
parent 04cbb22c57
commit bbc0690eaa
5 changed files with 32 additions and 10 deletions

View File

@@ -48,6 +48,7 @@
- 可在 `gateway.token` 配置网关注册令牌;子节点注册/续租需带 `Authorization: Bearer <token>`
- 可在 `NodeInfo` 中配置 `token`relay 会自动附加 `Authorization: Bearer <token>`
- `nodes` 工具支持设备快捷参数:`facing``duration_ms``command`
- 设备动作响应统一:`ok/code/error/payload`code 示例:`ok` `unsupported_action` `transport_error`
实现位置:
- `pkg/nodes/types.go`

View File

@@ -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 <token>` for register/heartbeat
- `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`)
Implementation:
- `pkg/nodes/types.go`

View File

@@ -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>", "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}}

View File

@@ -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
}

View File

@@ -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"`