From 42a5be0cec96a6b879fea5a34d494b60118b01cc Mon Sep 17 00:00:00 2001 From: DBT Date: Tue, 24 Feb 2026 15:57:00 +0000 Subject: [PATCH] enhance device-operation routing with action paths and nodes tool shortcuts --- README.md | 3 ++- README_EN.md | 3 ++- pkg/agent/loop.go | 19 +++++++++++++++++-- pkg/nodes/transport.go | 29 ++++++++++++++++++++++++++++- pkg/tools/nodes_tool.go | 22 ++++++++++++++++++---- 5 files changed, 67 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 84f50be..2a160a2 100644 --- a/README.md +++ b/README.md @@ -42,8 +42,9 @@ - `action=status|describe`:查看已配对节点状态与能力矩阵 - `action=run|invoke|camera_snap|screen_record|location_get`:已接入路由框架 - `mode=auto|p2p|relay`:默认 `auto`(优先 p2p,失败回退 relay) -- relay 已支持 HTTP 节点桥接:当节点配置 `endpoint` 后,调用 `POST {endpoint}/invoke` 并透传 `Request` +- relay 已支持 HTTP 节点桥接:按 action 路由到 `/run` `/camera/snap` `/screen/record` `/location/get` `/canvas/*`(未知 action 回退 `/invoke`) - 可在 `NodeInfo` 中配置 `token`,relay 会自动附加 `Authorization: Bearer ` +- `nodes` 工具支持设备快捷参数:`facing`、`duration_ms`、`command` 实现位置: - `pkg/nodes/types.go` diff --git a/README_EN.md b/README_EN.md index aa3dc38..59a4f1a 100644 --- a/README_EN.md +++ b/README_EN.md @@ -42,8 +42,9 @@ A `nodes` tool control-plane PoC is now available: - `action=status|describe`: inspect paired node status and capability matrix - `action=run|invoke|camera_snap|screen_record|location_get`: routing framework is in place - `mode=auto|p2p|relay`: default `auto` (prefer p2p, fallback to relay) -- relay now supports HTTP node bridging: with node `endpoint` configured, it calls `POST {endpoint}/invoke` +- relay now supports HTTP node bridging with action-specific routes: `/run`, `/camera/snap`, `/screen/record`, `/location/get`, `/canvas/*` (unknown action falls back to `/invoke`) - `NodeInfo.token` is supported; relay automatically sets `Authorization: Bearer ` +- `nodes` tool supports device shortcuts: `facing`, `duration_ms`, `command` Implementation: - `pkg/nodes/types.go` diff --git a/pkg/agent/loop.go b/pkg/agent/loop.go index e2c3ec4..424119d 100644 --- a/pkg/agent/loop.go +++ b/pkg/agent/loop.go @@ -78,9 +78,24 @@ func NewAgentLoop(cfg *config.Config, msgBus *bus.MessageBus, provider providers toolsRegistry.Register(tools.NewExecTool(cfg.Tools.Shell, workspace, processManager)) toolsRegistry.Register(tools.NewProcessTool(processManager)) nodesManager := nodes.NewManager() - nodesManager.Upsert(nodes.NodeInfo{ID: "local", Name: "local", Capabilities: nodes.Capabilities{Run: true, Invoke: true}, Online: true}) + nodesManager.Upsert(nodes.NodeInfo{ID: "local", Name: "local", Capabilities: nodes.Capabilities{Run: true, Invoke: true, Camera: true, Screen: true, Location: true, Canvas: true}, Online: true}) nodesManager.RegisterHandler("local", func(req nodes.Request) nodes.Response { - return nodes.Response{OK: true, Node: "local", Action: req.Action, Payload: map[string]interface{}{"echo": req.Args, "transport": "relay-local"}} + switch req.Action { + case "run": + payload := map[string]interface{}{"transport": "relay-local"} + if cmdRaw, ok := req.Args["command"].([]interface{}); ok && len(cmdRaw) > 0 { + parts := make([]string, 0, len(cmdRaw)) + for _, x := range cmdRaw { + parts = append(parts, fmt.Sprint(x)) + } + 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}} + default: + return nodes.Response{OK: true, Node: "local", Action: req.Action, Payload: map[string]interface{}{"echo": req.Args, "transport": "relay-local"}} + } }) nodesRouter := &nodes.Router{P2P: &nodes.StubP2PTransport{}, Relay: &nodes.HTTPRelayTransport{Manager: nodesManager}} toolsRegistry.Register(tools.NewNodesTool(nodesManager, nodesRouter)) diff --git a/pkg/nodes/transport.go b/pkg/nodes/transport.go index 23fd766..21e338d 100644 --- a/pkg/nodes/transport.go +++ b/pkg/nodes/transport.go @@ -68,6 +68,32 @@ type HTTPRelayTransport struct { } func (s *HTTPRelayTransport) Name() string { return "relay" } + +func actionHTTPPath(action string) string { + switch strings.ToLower(strings.TrimSpace(action)) { + case "run": + return "/run" + case "invoke": + return "/invoke" + case "camera_snap": + return "/camera/snap" + case "camera_clip": + return "/camera/clip" + case "screen_record": + return "/screen/record" + case "screen_snapshot": + return "/screen/snapshot" + case "location_get": + return "/location/get" + case "canvas_snapshot": + return "/canvas/snapshot" + case "canvas_action": + return "/canvas/action" + default: + return "/invoke" + } +} + 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 @@ -88,7 +114,8 @@ func (s *HTTPRelayTransport) Send(ctx context.Context, req Request) (Response, e client = &http.Client{Timeout: 20 * time.Second} } body, _ := json.Marshal(req) - hreq, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint+"/invoke", bytes.NewReader(body)) + path := actionHTTPPath(req.Action) + hreq, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint+path, bytes.NewReader(body)) if err != nil { return Response{}, err } diff --git a/pkg/tools/nodes_tool.go b/pkg/tools/nodes_tool.go index 05b2898..339dd5b 100644 --- a/pkg/tools/nodes_tool.go +++ b/pkg/tools/nodes_tool.go @@ -18,14 +18,17 @@ type NodesTool struct { func NewNodesTool(m *nodes.Manager, r *nodes.Router) *NodesTool { return &NodesTool{manager: m, router: r} } func (t *NodesTool) Name() string { return "nodes" } func (t *NodesTool) Description() string { - return "Manage paired nodes (status/describe/run/invoke/camera/screen/location)." + return "Manage paired nodes (status/describe/run/invoke/camera/screen/location/canvas)." } func (t *NodesTool) Parameters() map[string]interface{} { return map[string]interface{}{"type": "object", "properties": map[string]interface{}{ - "action": map[string]interface{}{"type": "string", "description": "status|describe|run|invoke|camera_snap|screen_record|location_get"}, + "action": map[string]interface{}{"type": "string", "description": "status|describe|run|invoke|camera_snap|camera_clip|screen_record|screen_snapshot|location_get|canvas_snapshot|canvas_action"}, "node": map[string]interface{}{"type": "string", "description": "target node id"}, "mode": map[string]interface{}{"type": "string", "description": "auto|p2p|relay (default auto)"}, "args": map[string]interface{}{"type": "object", "description": "action args"}, + "command": map[string]interface{}{"type": "array", "description": "run command array shortcut"}, + "facing": map[string]interface{}{"type": "string", "description": "camera facing: front|back|both"}, + "duration_ms": map[string]interface{}{"type": "integer", "description": "clip/record duration"}, }, "required": []string{"action"}} } @@ -66,9 +69,20 @@ func (t *NodesTool) Execute(ctx context.Context, args map[string]interface{}) (s if t.router == nil { return "", fmt.Errorf("nodes transport router not configured") } - var reqArgs map[string]interface{} + reqArgs := map[string]interface{}{} if raw, ok := args["args"].(map[string]interface{}); ok { - reqArgs = raw + for k, v := range raw { + reqArgs[k] = v + } + } + if cmd, ok := args["command"].([]interface{}); ok && len(cmd) > 0 { + reqArgs["command"] = cmd + } + if facing, _ := args["facing"].(string); strings.TrimSpace(facing) != "" { + reqArgs["facing"] = strings.TrimSpace(facing) + } + if d, ok := args["duration_ms"].(float64); ok && d > 0 { + reqArgs["duration_ms"] = int(d) } resp, err := t.router.Dispatch(ctx, nodes.Request{Action: action, Node: nodeID, Args: reqArgs}, mode) if err != nil {