diff --git a/README.md b/README.md index 7db7b87..df67b60 100644 --- a/README.md +++ b/README.md @@ -51,6 +51,7 @@ - 设备动作响应统一:`ok/code/error/payload`(code 示例:`ok` `unsupported_action` `transport_error`) - 设备 `payload` 规范字段:`media_type` `storage` `url|path|image` `meta` - 支持 `agent_task`:主节点可向具备 `model` 能力的子节点下发任务,子节点返回执行结果 +- 节点分发审计写入:`memory/nodes-dispatch-audit.jsonl` 实现位置: - `pkg/nodes/types.go` diff --git a/README_EN.md b/README_EN.md index a55802f..91901fa 100644 --- a/README_EN.md +++ b/README_EN.md @@ -51,6 +51,7 @@ A `nodes` tool control-plane PoC is now available: - 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` - supports `agent_task`: parent node can dispatch tasks to child nodes with `model` capability and receive execution results +- node dispatch audit is persisted to `memory/nodes-dispatch-audit.jsonl` Implementation: - `pkg/nodes/types.go` diff --git a/pkg/agent/loop.go b/pkg/agent/loop.go index aae2b7a..0b552da 100644 --- a/pkg/agent/loop.go +++ b/pkg/agent/loop.go @@ -113,7 +113,7 @@ func NewAgentLoop(cfg *config.Config, msgBus *bus.MessageBus, provider providers } }) nodesRouter := &nodes.Router{P2P: &nodes.StubP2PTransport{}, Relay: &nodes.HTTPRelayTransport{Manager: nodesManager}} - toolsRegistry.Register(tools.NewNodesTool(nodesManager, nodesRouter)) + toolsRegistry.Register(tools.NewNodesTool(nodesManager, nodesRouter, filepath.Join(workspace, "memory", "nodes-dispatch-audit.jsonl"))) if cs != nil { toolsRegistry.Register(tools.NewRemindTool(cs)) diff --git a/pkg/tools/nodes_tool.go b/pkg/tools/nodes_tool.go index 90cc2b7..6525c68 100644 --- a/pkg/tools/nodes_tool.go +++ b/pkg/tools/nodes_tool.go @@ -4,18 +4,24 @@ import ( "context" "encoding/json" "fmt" + "os" + "path/filepath" "strings" + "time" "clawgo/pkg/nodes" ) // NodesTool provides an OpenClaw-style control surface for paired nodes. type NodesTool struct { - manager *nodes.Manager - router *nodes.Router + manager *nodes.Manager + router *nodes.Router + auditPath string } -func NewNodesTool(m *nodes.Manager, r *nodes.Router) *NodesTool { return &NodesTool{manager: m, router: r} } +func NewNodesTool(m *nodes.Manager, r *nodes.Router, auditPath string) *NodesTool { + return &NodesTool{manager: m, router: r, auditPath: strings.TrimSpace(auditPath)} +} func (t *NodesTool) Name() string { return "nodes" } func (t *NodesTool) Description() string { return "Manage paired nodes (status/describe/run/invoke/camera/screen/location/canvas)." @@ -107,11 +113,39 @@ func (t *NodesTool) Execute(ctx context.Context, args map[string]interface{}) (s return "", fmt.Errorf("invalid_args: canvas_action requires args.action") } } - resp, err := t.router.Dispatch(ctx, nodes.Request{Action: action, Node: nodeID, Task: strings.TrimSpace(task), Model: strings.TrimSpace(model), Args: reqArgs}, mode) + req := nodes.Request{Action: action, Node: nodeID, Task: strings.TrimSpace(task), Model: strings.TrimSpace(model), Args: reqArgs} + resp, err := t.router.Dispatch(ctx, req, mode) if err != nil { + t.writeAudit(req, nodes.Response{OK: false, Code: "transport_error", Error: err.Error(), Node: nodeID, Action: action}, mode) return "", err } + t.writeAudit(req, resp, mode) b, _ := json.Marshal(resp) return string(b), nil } } + +func (t *NodesTool) writeAudit(req nodes.Request, resp nodes.Response, mode string) { + if strings.TrimSpace(t.auditPath) == "" { + return + } + _ = os.MkdirAll(filepath.Dir(t.auditPath), 0755) + row := map[string]interface{}{ + "time": time.Now().UTC().Format(time.RFC3339), + "mode": strings.TrimSpace(mode), + "action": req.Action, + "node": req.Node, + "task": req.Task, + "model": req.Model, + "ok": resp.OK, + "code": resp.Code, + "error": resp.Error, + } + b, _ := json.Marshal(row) + f, err := os.OpenFile(t.auditPath, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644) + if err != nil { + return + } + defer f.Close() + _, _ = f.Write(append(b, '\n')) +}