From 2f8f0cca2c67318581dc98e729f6106d86db1604 Mon Sep 17 00:00:00 2001 From: keep <1603421097@qq.com> Date: Sat, 18 Apr 2026 01:36:24 +0800 Subject: [PATCH] =?UTF-8?q?refactor(diff):=20=E9=87=8D=E6=9E=84=E6=96=87?= =?UTF-8?q?=E4=BB=B6=E5=B7=AE=E5=BC=82=E5=B7=A5=E5=85=B7=E4=BB=A5=E4=BD=BF?= =?UTF-8?q?=E7=94=A8=E7=8B=AC=E7=AB=8B=E8=B7=AF=E5=BE=84=E9=AA=8C=E8=AF=81?= =?UTF-8?q?=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 重构 diff 工具使其使用独立的路径验证逻辑,移除对 fileSystem 接口的依赖 将工具结果类型改为使用 fstools.ToolResult 统一处理 添加对整数参数的严格类型检查 --- .picoclaw.pid | 7 ++++ config.json.20260418.bak | 58 +++++++++++++++++++++++++++++ logs/gateway.log | 36 ++++++++++++++++++ logs/gateway_panic.log | 0 pkg/tools/diff.go | 80 +++++++++++++++++++++++++++++++--------- workspace/HEARTBEAT.md | 22 +++++++++++ workspace/cron/jobs.json | 4 ++ workspace/heartbeat.log | 1 + 8 files changed, 190 insertions(+), 18 deletions(-) create mode 100644 .picoclaw.pid create mode 100644 config.json.20260418.bak create mode 100644 logs/gateway.log create mode 100644 logs/gateway_panic.log create mode 100644 workspace/HEARTBEAT.md create mode 100644 workspace/cron/jobs.json create mode 100644 workspace/heartbeat.log diff --git a/.picoclaw.pid b/.picoclaw.pid new file mode 100644 index 000000000..87f740876 --- /dev/null +++ b/.picoclaw.pid @@ -0,0 +1,7 @@ +{ + "pid": 16504, + "token": "8fb35d8be88d02dd43f1d6717650f617", + "version": "dev", + "port": 18790, + "host": "localhost" +} \ No newline at end of file diff --git a/config.json.20260418.bak b/config.json.20260418.bak new file mode 100644 index 000000000..84c276ea2 --- /dev/null +++ b/config.json.20260418.bak @@ -0,0 +1,58 @@ +{ + "agents": { + "defaults": { + "workspace": "./workspace", + "restrict_to_workspace": true, + "model_name": "gpt-5.4", + "max_tokens": 8192, + "context_window": 131072, + "temperature": 0.7, + "max_tool_iterations": 20, + "summarize_message_threshold": 20, + "summarize_token_percent": 75, + "split_on_marker": false + } + }, + "model_list": [ + { + "model_name": "gpt-5.4", + "model": "openai/gpt-5.4", + "api_key": "sk-NlyZWKIMG7NYg1jXwRNqul6ejmVtAvkFLBwPTXx8zgM6UuD8", + "api_base": "https://mydamoxing.cn/v1" + } + ], + "channels": {}, + "tools": { + "web": { + "enabled": true + }, + "exec": { + "enabled": true + }, + "read_file": { + "enabled": true, + "mode": "lines" + }, + "write_file": { + "enabled": true + }, + "edit_file": { + "enabled": true + }, + "list_dir": { + "enabled": true + }, + "append_file": { + "enabled": true + }, + "diff_files": { + "enabled": true + } + }, + "gateway": { + "host": "localhost", + "port": 18790, + "hot_reload": false, + "log_level": "info" + } +} diff --git a/logs/gateway.log b/logs/gateway.log new file mode 100644 index 000000000..01616faa8 --- /dev/null +++ b/logs/gateway.log @@ -0,0 +1,36 @@ +{"level":"info","component":"config","to":3,"from":0,"time":"2026-04-18T01:34:01+08:00","caller":"D:/Desktop/一面千石/picoclaw/pkg/config/config.go:1002","message":"config migrate start"} +{"level":"debug","component":"pid","time":"2026-04-18T01:34:01+08:00","caller":"D:/Desktop/一面千石/picoclaw/pkg/pid/pidfile.go:100","message":"wrote pid file: d:\\Desktop\\一面千石\\picoclaw\\.picoclaw.pid success"} +{"level":"debug","component":"tools","name":"read_file","time":"2026-04-18T01:34:01+08:00","caller":"D:/Desktop/一面千石/picoclaw/pkg/tools/registry.go:56","message":"Registered core tool"} +{"level":"debug","component":"tools","name":"write_file","time":"2026-04-18T01:34:01+08:00","caller":"D:/Desktop/一面千石/picoclaw/pkg/tools/registry.go:56","message":"Registered core tool"} +{"level":"debug","component":"tools","name":"list_dir","time":"2026-04-18T01:34:01+08:00","caller":"D:/Desktop/一面千石/picoclaw/pkg/tools/registry.go:56","message":"Registered core tool"} +{"level":"debug","component":"tools","name":"exec","time":"2026-04-18T01:34:01+08:00","caller":"D:/Desktop/一面千石/picoclaw/pkg/tools/registry.go:56","message":"Registered core tool"} +{"level":"debug","component":"tools","name":"edit_file","time":"2026-04-18T01:34:01+08:00","caller":"D:/Desktop/一面千石/picoclaw/pkg/tools/registry.go:56","message":"Registered core tool"} +{"level":"debug","component":"tools","name":"append_file","time":"2026-04-18T01:34:01+08:00","caller":"D:/Desktop/一面千石/picoclaw/pkg/tools/registry.go:56","message":"Registered core tool"} +{"level":"debug","component":"tools","name":"diff_files","time":"2026-04-18T01:34:01+08:00","caller":"D:/Desktop/一面千石/picoclaw/pkg/tools/registry.go:56","message":"Registered core tool"} +{"level":"info","component":"agent","time":"2026-04-18T01:34:01+08:00","caller":"D:/Desktop/一面千石/picoclaw/pkg/agent/registry.go:39","message":"Created implicit main agent (no agents.list configured)"} +{"level":"debug","component":"tools","name":"web_fetch","time":"2026-04-18T01:34:01+08:00","caller":"D:/Desktop/一面千石/picoclaw/pkg/tools/registry.go:56","message":"Registered core tool"} +{"level":"debug","component":"tools","name":"message","time":"2026-04-18T01:34:01+08:00","caller":"D:/Desktop/一面千石/picoclaw/pkg/tools/registry.go:56","message":"Registered core tool"} +{"level":"debug","component":"tools","name":"reaction","time":"2026-04-18T01:34:01+08:00","caller":"D:/Desktop/一面千石/picoclaw/pkg/tools/registry.go:56","message":"Registered core tool"} +{"level":"debug","component":"tools","name":"send_file","time":"2026-04-18T01:34:01+08:00","caller":"D:/Desktop/一面千石/picoclaw/pkg/tools/registry.go:56","message":"Registered core tool"} +{"level":"debug","component":"tools","name":"load_image","time":"2026-04-18T01:34:01+08:00","caller":"D:/Desktop/一面千石/picoclaw/pkg/tools/registry.go:56","message":"Registered core tool"} +{"level":"debug","component":"tools","name":"find_skills","time":"2026-04-18T01:34:01+08:00","caller":"D:/Desktop/一面千石/picoclaw/pkg/tools/registry.go:56","message":"Registered core tool"} +{"level":"debug","component":"tools","name":"install_skill","time":"2026-04-18T01:34:01+08:00","caller":"D:/Desktop/一面千石/picoclaw/pkg/tools/registry.go:56","message":"Registered core tool"} +{"level":"debug","component":"tools","name":"spawn","time":"2026-04-18T01:34:01+08:00","caller":"D:/Desktop/一面千石/picoclaw/pkg/tools/registry.go:56","message":"Registered core tool"} +{"level":"debug","component":"tools","name":"subagent","time":"2026-04-18T01:34:01+08:00","caller":"D:/Desktop/一面千石/picoclaw/pkg/tools/registry.go:56","message":"Registered core tool"} +{"level":"info","component":"agent","tools_count":16,"skills_total":7,"skills_available":7,"time":"2026-04-18T01:34:01+08:00","caller":"D:/Desktop/一面千石/picoclaw/pkg/gateway/gateway.go:209","message":"Agent initialized"} +{"level":"debug","component":"tools","name":"cron","time":"2026-04-18T01:34:01+08:00","caller":"D:/Desktop/一面千石/picoclaw/pkg/tools/registry.go:56","message":"Registered core tool"} +{"level":"info","component":"heartbeat","interval_minutes":30,"time":"2026-04-18T01:34:01+08:00","caller":"D:/Desktop/一面千石/picoclaw/pkg/heartbeat/service.go:100","message":"Heartbeat service started"} +{"level":"info","component":"media","max_age":"30m0s","interval":"5m0s","time":"2026-04-18T01:34:01+08:00","caller":"D:/Desktop/一面千石/picoclaw/pkg/media/store.go:322","message":"cleanup enabled"} +{"level":"info","component":"channels","time":"2026-04-18T01:34:01+08:00","caller":"D:/Desktop/一面千石/picoclaw/pkg/channels/manager.go:451","message":"Initializing channel manager"} +{"level":"info","component":"channels","enabled_channels":0,"time":"2026-04-18T01:34:01+08:00","caller":"D:/Desktop/一面千石/picoclaw/pkg/channels/manager.go:468","message":"Channel initialization completed"} +{"level":"warn","component":"channels","time":"2026-04-18T01:34:01+08:00","caller":"D:/Desktop/一面千石/picoclaw/pkg/channels/manager.go:556","message":"No channels enabled"} +{"level":"info","component":"channels","time":"2026-04-18T01:34:01+08:00","caller":"D:/Desktop/一面千石/picoclaw/pkg/channels/manager.go:559","message":"Starting all channels"} +{"level":"info","component":"channels","started":0,"failed":0,"total":0,"time":"2026-04-18T01:34:01+08:00","caller":"D:/Desktop/一面千石/picoclaw/pkg/channels/manager.go:660","message":"Channel startup completed"} +{"level":"info","component":"channels","addr":"[::1]:18790","time":"2026-04-18T01:34:01+08:00","caller":"D:/Desktop/一面千石/picoclaw/pkg/channels/manager.go:635","message":"Shared HTTP server listening"} +{"level":"info","component":"channels","addr":"127.0.0.1:18790","time":"2026-04-18T01:34:01+08:00","caller":"D:/Desktop/一面千石/picoclaw/pkg/channels/manager.go:635","message":"Shared HTTP server listening"} +{"level":"info","component":"channels","time":"2026-04-18T01:34:01+08:00","caller":"D:/Desktop/一面千石/picoclaw/pkg/channels/manager.go:884","message":"Outbound dispatcher started"} +{"level":"info","component":"channels","time":"2026-04-18T01:34:01+08:00","caller":"D:/Desktop/一面千石/picoclaw/pkg/channels/manager.go:884","message":"Outbound media dispatcher started"} +{"level":"info","component":"devices","time":"2026-04-18T01:34:01+08:00","caller":"D:/Desktop/一面千石/picoclaw/pkg/devices/service.go:58","message":"Device event service disabled or no sources"} +{"level":"debug","component":"heartbeat","time":"2026-04-18T01:34:02+08:00","caller":"D:/Desktop/一面千石/picoclaw/pkg/heartbeat/service.go:163","message":"Executing heartbeat"} +{"level":"info","component":"heartbeat","time":"2026-04-18T01:34:02+08:00","caller":"D:/Desktop/一面千石/picoclaw/pkg/heartbeat/service.go:167","message":"No heartbeat prompt (HEARTBEAT.md empty or missing)"} +{"level":"error","component":"gateway","debug":true,"config_path":"d:\\Desktop\\一面千石\\picoclaw\\config.json","error":"error opening gateway listeners: failed to open adaptive localhost listener on port 18790","home_path":"d:\\Desktop\\一面千石\\picoclaw","allow_empty":false,"time":"2026-04-18T01:35:30+08:00","caller":"D:/Desktop/一面千石/picoclaw/pkg/gateway/gateway.go:137","message":"Gateway startup failed"} diff --git a/logs/gateway_panic.log b/logs/gateway_panic.log new file mode 100644 index 000000000..e69de29bb diff --git a/pkg/tools/diff.go b/pkg/tools/diff.go index 407fe88d8..252117c39 100644 --- a/pkg/tools/diff.go +++ b/pkg/tools/diff.go @@ -5,20 +5,25 @@ import ( "bytes" "context" "fmt" + "math" + "os" "path/filepath" "regexp" "strings" "github.com/pmezard/go-difflib/difflib" + fstools "github.com/sipeed/picoclaw/pkg/tools/fs" ) const ( - DiffFormatUnified = "unified" - DiffFormatSideBySide = "side_by_side" + DiffFormatUnified = "unified" + DiffFormatSideBySide = "side_by_side" ) type DiffTool struct { - fs fileSystem + workspace string + restrict bool + allowPaths []*regexp.Regexp maxReadFileSize int64 } @@ -35,11 +40,13 @@ func NewDiffTool( maxSize := int64(maxReadFileSize) if maxSize <= 0 { - maxSize = MaxReadFileSize + maxSize = fstools.MaxReadFileSize } return &DiffTool{ - fs: buildFs(workspace, restrict, patterns), + workspace: workspace, + restrict: restrict, + allowPaths: patterns, maxReadFileSize: maxSize, } } @@ -85,15 +92,40 @@ func (t *DiffTool) Parameters() map[string]any { } } -func (t *DiffTool) Execute(ctx context.Context, args map[string]any) *ToolResult { +func getInt64Arg(args map[string]any, key string, defaultVal int64) (int64, error) { + raw, exists := args[key] + if !exists { + return defaultVal, nil + } + + switch v := raw.(type) { + case float64: + if v != math.Trunc(v) { + return 0, fmt.Errorf("%s must be an integer, got float %v", key, v) + } + return int64(v), nil + case int: + return int64(v), nil + case int64: + return v, nil + case uint: + return int64(v), nil + case uint64: + return int64(v), nil + default: + return 0, fmt.Errorf("%s must be an integer, got %T", key, v) + } +} + +func (t *DiffTool) Execute(ctx context.Context, args map[string]any) *fstools.ToolResult { fileA, ok := args["file_a"].(string) if !ok { - return ErrorResult("file_a is required") + return fstools.ErrorResult("file_a is required") } fileB, ok := args["file_b"].(string) if !ok { - return ErrorResult("file_b is required") + return fstools.ErrorResult("file_b is required") } format := DiffFormatUnified @@ -120,7 +152,7 @@ func (t *DiffTool) Execute(ctx context.Context, args map[string]any) *ToolResult var err error contextLines, err = getInt64Arg(args, "context_lines", 3) if err != nil { - return ErrorResult(err.Error()) + return fstools.ErrorResult(err.Error()) } if contextLines < 0 { contextLines = 0 @@ -129,12 +161,12 @@ func (t *DiffTool) Execute(ctx context.Context, args map[string]any) *ToolResult contentA, err := t.readFile(fileA) if err != nil { - return ErrorResult(fmt.Sprintf("failed to read file_a: %v", err)) + return fstools.ErrorResult(fmt.Sprintf("failed to read file_a: %v", err)) } contentB, err := t.readFile(fileB) if err != nil { - return ErrorResult(fmt.Sprintf("failed to read file_b: %v", err)) + return fstools.ErrorResult(fmt.Sprintf("failed to read file_b: %v", err)) } linesA := strings.SplitAfter(contentA, "\n") @@ -159,24 +191,36 @@ func (t *DiffTool) Execute(ctx context.Context, args map[string]any) *ToolResult default: result, err = t.formatUnifiedDiff(linesA, linesB, fileA, fileB, int(contextLines)) if err != nil { - return ErrorResult(fmt.Sprintf("failed to generate diff: %v", err)) + return fstools.ErrorResult(fmt.Sprintf("failed to generate diff: %v", err)) } } - return NewToolResult(result) + return fstools.NewToolResult(result) +} + +func (t *DiffTool) validatePath(path string) (string, error) { + return fstools.ValidatePathWithAllowPaths(path, t.workspace, t.restrict, t.allowPaths) } func (t *DiffTool) readFile(path string) (string, error) { - file, err := t.fs.Open(path) + validatedPath, err := t.validatePath(path) + if err != nil { + return "", err + } + + file, err := os.Open(validatedPath) if err != nil { return "", err } defer file.Close() - if info, statErr := file.Stat(); statErr == nil { - if info.Size() > t.maxReadFileSize { - return "", fmt.Errorf("file too large (max %d bytes)", t.maxReadFileSize) - } + info, err := file.Stat() + if err != nil { + return "", err + } + + if info.Size() > t.maxReadFileSize { + return "", fmt.Errorf("file too large (max %d bytes)", t.maxReadFileSize) } var buf bytes.Buffer diff --git a/workspace/HEARTBEAT.md b/workspace/HEARTBEAT.md new file mode 100644 index 000000000..9a4e3ca80 --- /dev/null +++ b/workspace/HEARTBEAT.md @@ -0,0 +1,22 @@ +# Heartbeat Check List + +This file contains tasks for the heartbeat service to check periodically. + +## Examples + +- Check for unread messages +- Review upcoming calendar events +- Check device status (e.g., MaixCam) + +## Instructions + +- Execute ALL tasks listed below. Do NOT skip any task. +- For simple tasks (e.g., report current time), respond directly. +- For complex tasks that may take time, use the spawn tool to create a subagent. +- The spawn tool is async - subagent results will be sent to the user automatically. +- After spawning a subagent, CONTINUE to process remaining tasks. +- Only respond with HEARTBEAT_OK when ALL tasks are done AND nothing needs attention. + +--- + +Add your heartbeat tasks below this line: diff --git a/workspace/cron/jobs.json b/workspace/cron/jobs.json new file mode 100644 index 000000000..b8cdc503b --- /dev/null +++ b/workspace/cron/jobs.json @@ -0,0 +1,4 @@ +{ + "version": 1, + "jobs": [] +} \ No newline at end of file diff --git a/workspace/heartbeat.log b/workspace/heartbeat.log new file mode 100644 index 000000000..af123e328 --- /dev/null +++ b/workspace/heartbeat.log @@ -0,0 +1 @@ +[2026-04-18 01:34:02] [INFO] Created default HEARTBEAT.md template