# Agent 文件操作能力增强实施计划 > **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. **Goal:** 为 Agent Team 实现类似 opencode 的文件操作工具(glob, grep, read_file, edit_file, write_file, git 等),支持智能搜索、大文件处理、精细编辑 **Architecture:** 在 Room 层为 Agent 注册文件操作工具,通过 Tool Calling 让 LLM 自主调用工具执行文件操作 **Tech Stack:** Go + go-openai (Tool Calling) --- ## Task 1: 创建文件操作工具定义 **Files:** - Create: `internal/room/tools/definitions.go` **Step 1: 创建工具定义文件** ```go package tools import "github.com/sashabaranov/go-openai" var FileTools = []openai.Tool{ { Type: openai.ToolTypeFunction, Function: openai.FunctionDefinition{ Name: "glob", Description: "模式匹配查找文件,支持 * ** ? 等通配符", Parameters: `{ "type": "object", "properties": { "pattern": { "type": "string", "description": "glob 模式,如 **/*.go, *.ts" } }, "required": ["pattern"] }`, }, }, { Type: openai.ToolTypeFunction, Function: openai.FunctionDefinition{ Name: "grep", Description: "在文件中搜索内容,支持正则表达式", Parameters: `{ "type": "object", "properties": { "pattern": { "type": "string", "description": "正则表达式搜索模式" }, "path": { "type": "string", "description": "搜索目录路径" }, "include": { "type": "string", "description": "文件过滤,如 *.go, *.ts" } }, "required": ["pattern"] }`, }, }, { Type: openai.ToolTypeFunction, Function: openai.FunctionDefinition{ Name: "read_file", Description: "读取文件内容,支持大文件分段读取", Parameters: `{ "type": "object", "properties": { "filename": { "type": "string", "description": "文件名(相对于 workspace)" }, "offset": { "type": "integer", "description": "起始行号(从 0 开始)" }, "limit": { "type": "integer", "description": "读取行数,默认 100" } }, "required": ["filename"] }`, }, }, { Type: openai.ToolTypeFunction, Function: openai.FunctionDefinition{ Name: "edit_file", Description: "编辑文件内容,使用字符串替换", Parameters: `{ "type": "object", "properties": { "filename": { "type": "string", "description": "文件名(相对于 workspace)" }, "old_string": { "type": "string", "description": "要替换的原始内容" }, "new_string": { "type": "string", "description": "替换后的新内容" } }, "required": ["filename", "old_string", "new_string"] }`, }, }, { Type: openai.ToolTypeFunction, Function: openai.FunctionDefinition{ Name: "write_file", Description: "写入或创建文件", Parameters: `{ "type": "object", "properties": { "filename": { "type": "string", "description": "文件名(相对于 workspace)" }, "content": { "type": "string", "description": "文件内容" } }, "required": ["filename", "content"] }`, }, }, { Type: openai.ToolTypeFunction, Function: openai.FunctionDefinition{ Name: "list_workspace", Description: "列出工作区的所有文件", Parameters: `{ "type": "object", "properties": {} }`, }, }, } ``` **Step 2: 提交** ```bash git add internal/room/tools/definitions.go git commit -m "feat: 添加文件操作工具定义" ``` --- ## Task 2: 实现工具执行器 **Files:** - Create: `internal/room/tools/executor.go` **Step 1: 实现工具执行器** ```go package tools import ( "encoding/json" "os" "os/exec" "path/filepath" "regexp" "strings" "github.com/sdaduanbilei/agent-team/internal/llm" ) type Executor struct { workspaceDir string } func NewExecutor(workspaceDir string) *Executor { return &Executor{workspaceDir: workspaceDir} } func (e *Executor) Execute(toolCall llm.ToolCall) (string, error) { switch toolCall.Function.Name { case "glob": return e.glob(toolCall.Function.Arguments) case "grep": return e.grep(toolCall.Function.Arguments) case "read_file": return e.readFile(toolCall.Function.Arguments) case "edit_file": return e.editFile(toolCall.Function.Arguments) case "write_file": return e.writeFile(toolCall.Function.Arguments) case "list_workspace": return e.listWorkspace() case "git_status": return e.gitStatus() case "git_diff": return e.gitDiff(toolCall.Function.Arguments) case "git_commit": return e.gitCommit(toolCall.Function.Arguments) default: return "", nil } } type GlobArgs struct { Pattern string `json:"pattern"` } func (e *Executor) glob(args string) (string, error) { var a GlobArgs if err := json.Unmarshal([]byte(args), &a); err != nil { return "", err } pattern := filepath.Join(e.workspaceDir, a.Pattern) files, err := filepath.Glob(pattern) if err != nil { return "", err } var result []string for _, f := range files { rel, _ := filepath.Rel(e.workspaceDir, f) result = append(result, rel) } if len(result) == 0 { return "未找到匹配的文件", nil } return strings.Join(result, "\n"), nil } type GrepArgs struct { Pattern string `json:"pattern"` Path string `json:"path"` Include string `json:"include"` } func (e *Executor) grep(args string) (string, error) { var a GrepArgs if err := json.Unmarshal([]byte(args), &a); err != nil { return "", err } searchDir := e.workspaceDir if a.Path != "" { searchDir = filepath.Join(e.workspaceDir, a.Path) } re, err := regexp.Compile(a.Pattern) if err != nil { return "", err } var results []string err = filepath.Walk(searchDir, func(path string, info os.FileInfo, err error) error { if err != nil { return nil } if info.IsDir() { return nil } if a.Include != "" { matched, _ := filepath.Match(a.Include, info.Name()) if !matched { return nil } } content, err := os.ReadFile(path) if err != nil { return nil } lines := strings.Split(string(content), "\n") for i, line := range lines { if re.MatchString(line) { rel, _ := filepath.Rel(e.workspaceDir, path) results = append(results, fmt.Sprintf("%s:%d: %s", rel, i+1, line)) } } return nil }) if len(results) == 0 { return "未找到匹配的内容", nil } // 限制结果数量 if len(results) > 100 { results = results[:100] results = append(results, "... (还有更多结果)") } return strings.Join(results, "\n"), nil } type ReadFileArgs struct { Filename string `json:"filename"` Offset int `json:"offset"` Limit int `json:"limit"` } func (e *Executor) readFile(args string) (string, error) { var a ReadFileArgs if err := json.Unmarshal([]byte(args), &a); err != nil { return "", err } if a.Limit == 0 { a.Limit = 100 } fpath := filepath.Join(e.workspaceDir, a.Filename) content, err := os.ReadFile(fpath) if err != nil { return "", err } lines := strings.Split(string(content), "\n") if a.Offset >= len(lines) { return "起始位置超出文件行数", nil } end := a.Offset + a.Limit if end > len(lines) { end = len(lines) } result := strings.Join(lines[a.Offset:end], "\n") if end < len(lines) { result += fmt.Sprintf("\n\n... (共 %d 行,当前显示 %d-%d)", len(lines), a.Offset+1, end) } return result, nil } type EditFileArgs struct { Filename string `json:"filename"` OldString string `json:"old_string"` NewString string `json:"new_string"` } func (e *Executor) editFile(args string) (string, error) { var a EditFileArgs if err := json.Unmarshal([]byte(args), &a); err != nil { return "", err } fpath := filepath.Join(e.workspaceDir, a.Filename) original, err := os.ReadFile(fpath) if err != nil { return "", err } if !strings.Contains(string(original), a.OldString) { return "", fmt.Errorf("文件中未找到要替换的内容") } updated := strings.Replace(string(original), a.OldString, a.NewString, 1) if err := os.WriteFile(fpath, []byte(updated), 0644); err != nil { return "", err } return fmt.Sprintf("已更新文件: %s", a.Filename), nil } type WriteFileArgs struct { Filename string `json:"filename"` Content string `json:"content"` } func (e *Executor) writeFile(args string) (string, error) { var a WriteFileArgs if err := json.Unmarshal([]byte(args), &a); err != nil { return "", err } fpath := filepath.Join(e.workspaceDir, a.Filename) os.MkdirAll(filepath.Dir(fpath), 0755) exists := "" if _, err := os.Stat(fpath); err == nil { exists = " (已存在,已覆盖)" } if err := os.WriteFile(fpath, []byte(a.Content), 0644); err != nil { return "", err } return fmt.Sprintf("已写入文件: %s%s", a.Filename, exists), nil } func (e *Executor) listWorkspace() (string, error) { var files []string err := filepath.Walk(e.workspaceDir, func(path string, info os.FileInfo, err error) error { if err != nil { return nil } if info.IsDir() { return nil } rel, _ := filepath.Rel(e.workspaceDir, path) if !strings.HasPrefix(rel, ".") { files = append(files, rel) } return nil }) if err != nil { return "", err } if len(files) == 0 { return "工作区为空", nil } return strings.Join(files, "\n"), nil } func (e *Executor) gitStatus() (string, error) { cmd := exec.Command("git", "status", "--porcelain") cmd.Dir = e.workspaceDir out, err := cmd.Output() if err != nil { return "", err } if len(out) == 0 { return "工作区干净,无待提交更改", nil } return string(out), nil } type GitDiffArgs struct { Filename string `json:"filename"` } func (e *Executor) gitDiff(args string) (string, error) { var a GitDiffArgs if err := json.Unmarshal([]byte(args), &a); err != nil { return "", err } var cmd *exec.Cmd if a.Filename != "" { cmd = exec.Command("git", "diff", a.Filename) } else { cmd = exec.Command("git", "diff") } cmd.Dir = e.workspaceDir out, err := cmd.Output() if err != nil { return "", err } if len(out) == 0 { return "无更改", nil } return string(out), nil } type GitCommitArgs struct { Message string `json:"message"` } func (e *Executor) gitCommit(args string) (string, error) { var a GitCommitArgs if err := json.Unmarshal([]byte(args), &a); err != nil { return "", err } // git add -A cmd := exec.Command("git", "add", "-A") cmd.Dir = e.workspaceDir if _, err := cmd.Output(); err != nil { return "", err } // git commit cmd = exec.Command("git", "commit", "-m", a.Message) out, err := cmd.Output() if err != nil { return "", err } return string(out), nil } ``` **Step 2: 提交** ```bash git add internal/room/tools/executor.go git commit -m "feat: 实现文件操作工具执行器" ``` --- ## Task 3: 集成工具到 Room **Files:** - Modify: `internal/room/handle.go` - Modify: `internal/room/types.go` **Step 1: 修改 types.go 添加工具执行器字段** 在 Room 结构体中添加: ```go ToolExecutor *tools.Executor ``` **Step 2: 修改 handle.go 初始化工具执行器** 在 Load 函数中添加: ```go r.ToolExecutor = tools.NewExecutor(filepath.Join(roomDir, "workspace")) ``` **Step 3: 修改 room 调用逻辑以支持工具调用** 在需要的地方(如 build 模式下 Agent 调用),传入工具定义并处理工具调用结果。 **Step 4: 提交** ```bash git add internal/room/handle.go internal/room/types.go git commit -m "feat: 集成文件操作工具到 Room" ``` --- ## Task 4: 注册 Git 工具定义 **Files:** - Modify: `internal/room/tools/definitions.go` **Step 1: 添加 Git 工具定义** 在 FileTools 切片中添加: ```go { Type: openai.ToolTypeFunction, Function: openai.FunctionDefinition{ Name: "git_status", Description: "查看 Git 工作区状态", Parameters: `{"type": "object", "properties": {}}`, }, }, { Type: openai.ToolTypeFunction, Function: openai.FunctionDefinition{ Name: "git_diff", Description: "查看文件变更", Parameters: `{ "type": "object", "properties": { "filename": {"type": "string", "description": "文件名"} } }`, }, }, { Type: openai.ToolTypeFunction, Function: openai.FunctionDefinition{ Name: "git_commit", Description: "提交更改", Parameters: `{ "type": "object", "properties": { "message": {"type": "string", "description": "提交信息"} }, "required": ["message"] }`, }, }, ``` **Step 2: 提交** ```bash git add internal/room/tools/definitions.go git commit -m "feat: 添加 Git 工具定义" ``` --- ## Task 5: 测试文件操作工具 **Files:** - Create: `internal/room/tools/executor_test.go` **Step 1: 创建测试文件** ```go package tools import ( "os" "path/filepath" "testing" ) func TestGlob(t *testing.T) { // 创建临时目录和测试文件 tmpDir := t.TempDir() os.WriteFile(filepath.Join(tmpDir, "test.go"), []byte("package test"), 0644) os.WriteFile(filepath.Join(tmpDir, "test.ts"), []byte("const x = 1"), 0644) os.MkdirAll(filepath.Join(tmpDir, "sub"), 0755) os.WriteFile(filepath.Join(tmpDir, "sub", "nested.go"), []byte("package sub"), 0644) exec := NewExecutor(tmpDir) // 测试 *.go result, err := exec.glob(`{"pattern": "*.go"}`) if err != nil { t.Fatal(err) } if !contains(result, "test.go") { t.Errorf("expected test.go in result, got: %s", result) } // 测试 **/*.go result, err = exec.glob(`{"pattern": "**/*.go"}`) if err != nil { t.Fatal(err) } if !contains(result, "test.go") || !contains(result, "sub/nested.go") { t.Errorf("expected test.go and sub/nested.go, got: %s", result) } } func TestReadFile(t *testing.T) { tmpDir := t.TempDir() content := "line1\nline2\nline3\nline4\nline5" os.WriteFile(filepath.Join(tmpDir, "test.txt"), []byte(content), 0644) exec := NewExecutor(tmpDir) // 测试基本读取 result, err := exec.readFile(`{"filename": "test.txt"}`) if err != nil { t.Fatal(err) } if !contains(result, "line1") { t.Errorf("expected line1 in result, got: %s", result) } // 测试 offset 和 limit result, err = exec.readFile(`{"filename": "test.txt", "offset": 1, "limit": 2}`) if err != nil { t.Fatal(err) } if !contains(result, "line2") || !contains(result, "line3") { t.Errorf("expected line2 and: %s", result) } line3, got} func TestEditFile(t *testing.T) { tmpDir := t.TempDir() os.WriteFile(filepath.Join(tmpDir, "test.txt"), []byte("hello world"), 0644) exec := NewExecutor(tmpDir) result, err := exec.editFile(`{"filename": "test.txt", "old_string": "world", "new_string": "opencode"}`) if err != nil { t.Fatal(err) } // 验证文件内容 data, _ := os.ReadFile(filepath.Join(tmpDir, "test.txt")) if string(data) != "hello opencode" { t.Errorf("expected 'hello opencode', got: %s", string(data)) } _ = result // ignore } func TestWriteFile(t *testing.T) { tmpDir := t.TempDir() exec := NewExecutor(tmpDir) // 测试新建文件 result, err := exec.writeFile(`{"filename": "new.txt", "content": "new content"}`) if err != nil { t.Fatal(err) } data, _ := os.ReadFile(filepath.Join(tmpDir, "new.txt")) if string(data) != "new content" { t.Errorf("expected 'new content', got: %s", string(data)) } _ = result // ignore } func TestListWorkspace(t *testing.T) { tmpDir := t.TempDir() os.WriteFile(filepath.Join(tmpDir, "a.txt"), []byte("a"), 0644) os.WriteFile(filepath.Join(tmpDir, "b.txt"), []byte("b"), 0644) os.MkdirAll(filepath.Join(tmpDir, "sub"), 0755) os.WriteFile(filepath.Join(tmpDir, "sub", "c.txt"), []byte("c"), 0644) exec := NewExecutor(tmpDir) result, err := exec.listWorkspace() if err != nil { t.Fatal(err) } if !contains(result, "a.txt") || !contains(result, "b.txt") || !contains(result, "sub/c.txt") { t.Errorf("expected all files, got: %s", result) } } func contains(s, substr string) bool { return len(s) > 0 && strings.Contains(s, substr) } ``` **Step 2: 运行测试** ```bash go test ./internal/room/tools/... -v ``` **Step 3: 提交** ```bash git add internal/room/tools/executor_test.go git commit -m "test: 添加文件操作工具测试" ``` --- ## Plan complete **Two execution options:** 1. **Subagent-Driven (this session)** - I dispatch fresh subagent per task, review between tasks, fast iteration 2. **Parallel Session (separate)** - Open new session with executing-plans, batch execution with checkpoints **Which approach?**