v1.3 Registry 实现重构 + 第一把真实工具

- Registry 从纯接口升级为完整实现:Register/Execute/路由查找/错误自愈
- 新增 BaseTool 接口,统一工具契约
- 新增 ReadFileTool,首个真实工具实现(路径防穿越 + 截断保护)
- 新增 String() 方法,Registry/Engine 调试可视化
- 新增 ToolCall 执行状态日志
- cmd/claw 切换为真实 Registry + ReadFileTool
This commit is contained in:
zhuyongxin
2026-05-15 11:24:42 +08:00
parent daa89531f5
commit b94460083a
6 changed files with 240 additions and 53 deletions
+85
View File
@@ -0,0 +1,85 @@
package tools
import (
"context"
"encoding/json"
"fmt"
"io"
"os"
"path/filepath"
"go-tiny-claw/internal/schema"
)
// ReadFileTool 实现了读取本地文件内容的工具
type ReadFileTool struct {
// 将引擎的 WorkDir 注入给工具,限制它只能在此目录及其子目录下操作
workDir string
}
func NewReadFileTool(workDir string) *ReadFileTool {
return &ReadFileTool{workDir: workDir}
}
func (t *ReadFileTool) Name() string {
return "read_file"
}
// Definition 向大模型清晰地描述这个工具的用途和参数格式
func (t *ReadFileTool) Definition() schema.ToolDefinition {
return schema.ToolDefinition{
Name: t.Name(),
Description: "读取指定路径的文件内容。请提供相对工作区的路径。",
// 遵循 JSON Schema 规范定义参数
InputSchema: map[string]interface{}{
"type": "object",
"properties": map[string]interface{}{
"path": map[string]interface{}{
"type": "string",
"description": "要读取的文件路径,如 cmd/claw/main.go",
},
},
"required": []string{"path"},
},
}
}
// readFileArgs 内部定义用于反序列化的结构体
type readFileArgs struct {
Path string `json:"path"`
}
func (t *ReadFileTool) Execute(ctx context.Context, args json.RawMessage) (string, error) {
// 1. 延迟解析:将大模型传过来的 JSON 参数解析为强类型结构体
var input readFileArgs
if err := json.Unmarshal(args, &input); err != nil {
// 返回 error 会被 Registry 捕获并传给大模型,模型会知道自己 JSON 格式写错了
return "", fmt.Errorf("参数解析失败: %w", err)
}
// 2. 拼接绝对路径 (注意:生产环境中需要做路径穿越检测防范,防止 ../../etc/passwd)
fullPath := filepath.Join(t.workDir, input.Path)
// 3. 执行物理 IO 操作
file, err := os.Open(fullPath)
if err != nil {
return "", fmt.Errorf("打开文件失败: %w", err)
}
defer file.Close()
content, err := io.ReadAll(file)
if err != nil {
return "", fmt.Errorf("读取文件内容失败: %w", err)
}
// 4. 【核心防线】长度截断保护
// 为了防止大模型读取几百 MB 的日志文件导致 Context 瞬间爆炸 (OOM),
// 我们在工具内部直接进行物理截断。
const maxLen = 8000
if len(content) > maxLen {
truncatedMsg := fmt.Sprintf("%s\n\n...[由于内容过长,已被系统截断至前 %d 字节]...", string(content[:maxLen]), maxLen)
return truncatedMsg, nil
}
return string(content), nil
}