refactor(diff): 重构文件差异工具以使用独立路径验证逻辑

重构 diff 工具使其使用独立的路径验证逻辑,移除对 fileSystem 接口的依赖
将工具结果类型改为使用 fstools.ToolResult 统一处理
添加对整数参数的严格类型检查
This commit is contained in:
keep 2026-04-18 01:36:24 +08:00
parent 6da38a0901
commit 2f8f0cca2c
8 changed files with 190 additions and 18 deletions

7
.picoclaw.pid Normal file
View file

@ -0,0 +1,7 @@
{
"pid": 16504,
"token": "8fb35d8be88d02dd43f1d6717650f617",
"version": "dev",
"port": 18790,
"host": "localhost"
}

58
config.json.20260418.bak Normal file
View file

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

36
logs/gateway.log Normal file
View file

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

0
logs/gateway_panic.log Normal file
View file

View file

@ -5,11 +5,14 @@ import (
"bytes" "bytes"
"context" "context"
"fmt" "fmt"
"math"
"os"
"path/filepath" "path/filepath"
"regexp" "regexp"
"strings" "strings"
"github.com/pmezard/go-difflib/difflib" "github.com/pmezard/go-difflib/difflib"
fstools "github.com/sipeed/picoclaw/pkg/tools/fs"
) )
const ( const (
@ -18,7 +21,9 @@ const (
) )
type DiffTool struct { type DiffTool struct {
fs fileSystem workspace string
restrict bool
allowPaths []*regexp.Regexp
maxReadFileSize int64 maxReadFileSize int64
} }
@ -35,11 +40,13 @@ func NewDiffTool(
maxSize := int64(maxReadFileSize) maxSize := int64(maxReadFileSize)
if maxSize <= 0 { if maxSize <= 0 {
maxSize = MaxReadFileSize maxSize = fstools.MaxReadFileSize
} }
return &DiffTool{ return &DiffTool{
fs: buildFs(workspace, restrict, patterns), workspace: workspace,
restrict: restrict,
allowPaths: patterns,
maxReadFileSize: maxSize, 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) fileA, ok := args["file_a"].(string)
if !ok { if !ok {
return ErrorResult("file_a is required") return fstools.ErrorResult("file_a is required")
} }
fileB, ok := args["file_b"].(string) fileB, ok := args["file_b"].(string)
if !ok { if !ok {
return ErrorResult("file_b is required") return fstools.ErrorResult("file_b is required")
} }
format := DiffFormatUnified format := DiffFormatUnified
@ -120,7 +152,7 @@ func (t *DiffTool) Execute(ctx context.Context, args map[string]any) *ToolResult
var err error var err error
contextLines, err = getInt64Arg(args, "context_lines", 3) contextLines, err = getInt64Arg(args, "context_lines", 3)
if err != nil { if err != nil {
return ErrorResult(err.Error()) return fstools.ErrorResult(err.Error())
} }
if contextLines < 0 { if contextLines < 0 {
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) contentA, err := t.readFile(fileA)
if err != nil { 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) contentB, err := t.readFile(fileB)
if err != nil { 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") linesA := strings.SplitAfter(contentA, "\n")
@ -159,25 +191,37 @@ func (t *DiffTool) Execute(ctx context.Context, args map[string]any) *ToolResult
default: default:
result, err = t.formatUnifiedDiff(linesA, linesB, fileA, fileB, int(contextLines)) result, err = t.formatUnifiedDiff(linesA, linesB, fileA, fileB, int(contextLines))
if err != nil { 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) { 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 { if err != nil {
return "", err return "", err
} }
defer file.Close() defer file.Close()
if info, statErr := file.Stat(); statErr == nil { info, err := file.Stat()
if err != nil {
return "", err
}
if info.Size() > t.maxReadFileSize { if info.Size() > t.maxReadFileSize {
return "", fmt.Errorf("file too large (max %d bytes)", t.maxReadFileSize) return "", fmt.Errorf("file too large (max %d bytes)", t.maxReadFileSize)
} }
}
var buf bytes.Buffer var buf bytes.Buffer
scanner := bufio.NewScanner(file) scanner := bufio.NewScanner(file)

22
workspace/HEARTBEAT.md Normal file
View file

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

4
workspace/cron/jobs.json Normal file
View file

@ -0,0 +1,4 @@
{
"version": 1,
"jobs": []
}

1
workspace/heartbeat.log Normal file
View file

@ -0,0 +1 @@
[2026-04-18 01:34:02] [INFO] Created default HEARTBEAT.md template