refactor(diff): 重构文件差异工具以使用独立路径验证逻辑
重构 diff 工具使其使用独立的路径验证逻辑,移除对 fileSystem 接口的依赖 将工具结果类型改为使用 fstools.ToolResult 统一处理 添加对整数参数的严格类型检查
This commit is contained in:
parent
6da38a0901
commit
2f8f0cca2c
8 changed files with 190 additions and 18 deletions
7
.picoclaw.pid
Normal file
7
.picoclaw.pid
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"pid": 16504,
|
||||
"token": "8fb35d8be88d02dd43f1d6717650f617",
|
||||
"version": "dev",
|
||||
"port": 18790,
|
||||
"host": "localhost"
|
||||
}
|
||||
58
config.json.20260418.bak
Normal file
58
config.json.20260418.bak
Normal 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
36
logs/gateway.log
Normal 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
0
logs/gateway_panic.log
Normal file
|
|
@ -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
|
||||
|
|
|
|||
22
workspace/HEARTBEAT.md
Normal file
22
workspace/HEARTBEAT.md
Normal 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
4
workspace/cron/jobs.json
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"version": 1,
|
||||
"jobs": []
|
||||
}
|
||||
1
workspace/heartbeat.log
Normal file
1
workspace/heartbeat.log
Normal file
|
|
@ -0,0 +1 @@
|
|||
[2026-04-18 01:34:02] [INFO] Created default HEARTBEAT.md template
|
||||
Loading…
Add table
Reference in a new issue