16 KiB
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: 创建工具定义文件
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: 提交
git add internal/room/tools/definitions.go
git commit -m "feat: 添加文件操作工具定义"
Task 2: 实现工具执行器
Files:
- Create:
internal/room/tools/executor.go
Step 1: 实现工具执行器
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: 提交
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 结构体中添加:
ToolExecutor *tools.Executor
Step 2: 修改 handle.go 初始化工具执行器
在 Load 函数中添加:
r.ToolExecutor = tools.NewExecutor(filepath.Join(roomDir, "workspace"))
Step 3: 修改 room 调用逻辑以支持工具调用
在需要的地方(如 build 模式下 Agent 调用),传入工具定义并处理工具调用结果。
Step 4: 提交
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 切片中添加:
{
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: 提交
git add internal/room/tools/definitions.go
git commit -m "feat: 添加 Git 工具定义"
Task 5: 测试文件操作工具
Files:
- Create:
internal/room/tools/executor_test.go
Step 1: 创建测试文件
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: 运行测试
go test ./internal/room/tools/... -v
Step 3: 提交
git add internal/room/tools/executor_test.go
git commit -m "test: 添加文件操作工具测试"
Plan complete
Two execution options:
-
Subagent-Driven (this session) - I dispatch fresh subagent per task, review between tasks, fast iteration
-
Parallel Session (separate) - Open new session with executing-plans, batch execution with checkpoints
Which approach?