Crush 代码修改工具学习
Crush 代码修改工具学习
概述: Crush是什么?
Crush 是由 Charm 开发的一款开源 AI 编程助手框架,它将大语言模型(LLM)与开发工具深度集成到终端环境中。本文将深入分析 Crush 中负责代码修改的核心工具实现,揭示其如何在保证安全性和可靠性的前提下,实现智能化的代码编辑功能。
项目背景
项目信息:
- 名称: Crush
- 语言: Go 1.25.0
- 类型: AI 编程助手框架
- 核心特性:
- 多模型支持(OpenAI、Anthropic、Google、Azure 等)
- 会话式交互管理
- LSP(Language Server Protocol)集成
- MCP(Model Context Protocol)扩展支持
- 跨平台支持(macOS、Linux、Windows、FreeBSD)
架构概览
Crush 的代码修改功能主要由三个核心工具组成,它们位于 internal/agent/tools/ 目录下:
Dir: internal/agent/tools/
- edit.go # 基础编辑工具
- multiedit.go # 批量编辑工具
- write.go # 文件写入工具
- view.go # 文件查看工具
- diagnostics.go # LSP 诊断集成
这些工具共同构成了一个完整的代码编辑生态系统,每个工具都有明确的职责分工,同时共享核心的安全和验证逻辑。
核心工具详解
1. Edit Tool - 精确的字符串替换
Edit Tool 是最基础也是最常用的编辑工具,通过精确的字符串匹配和替换来修改文件内容。
参数定义
type EditParams struct {
FilePath string `json:"file_path"` // 文件绝对路径
OldString string `json:"old_string"` // 要替换的文本
NewString string `json:"new_string"` // 替换后的文本
ReplaceAll bool `json:"replace_all"` // 是否替换所有出现
}
三种操作模式
Edit Tool 根据参数的不同组合,支持三种操作模式:
创见
更新
删除
crush实现的挺有意思,它在逻辑上相当于用一个新字符串在替换旧的字符串 如果说新的字符串为空,那么就是删除,如果旧的字符串为空,那么就是创建,如果新旧都不为空,那么就是编辑(想到养鱼替换操作)
1. 创建新文件 (OldString 为空)
func createNewFile(edit editContext, filePath, content string, call fantasy.ToolCall) {
// 检查文件是否已存在
if _, err := os.Stat(filePath); err == nil {
return error("file already exists")
}
// 创建父目录
os.MkdirAll(filepath.Dir(filePath), 0o755)
// 请求权限
if !permissions.Request(...) {
return ErrorPermissionDenied
}
// 写入文件
os.WriteFile(filePath, []byte(content), 0o644) <=== 这里是固定的,也许我们写的时候不会这么写。
// 创建历史记录
files.Create(ctx, sessionID, filePath, "")
files.CreateVersion(ctx, sessionID, filePath, content)
}
2. 删除内容 (NewString 为空)
func deleteContent(edit editContext, filePath, oldString string, replaceAll bool) {
// 读取并验证文件
content, _ := os.ReadFile(filePath)
oldContent, isCrlf := fsext.ToUnixLineEndings(string(content))
// 执行删除
if replaceAll {
newContent = strings.ReplaceAll(oldContent, oldString, "")
} else {
// 查找并删除单个匹配
index := strings.Index(oldContent, oldString)
newContent = oldContent[:index] + oldContent[index+len(oldString):]
}
// 写入并更新历史
os.WriteFile(filePath, []byte(newContent), 0o644)
}
3. 替换内容 (正常编辑)
func replaceContent(edit editContext, filePath, oldString, newString string, replaceAll bool) {
// 读取文件
content, _ := os.ReadFile(filePath)
oldContent, isCrlf := fsext.ToUnixLineEndings(string(content))
// 执行替换
if replaceAll {
newContent = strings.ReplaceAll(oldContent, oldString, newString)
} else {
// 单次替换,需要确保唯一性
index := strings.Index(oldContent, oldString)
lastIndex := strings.LastIndex(oldContent, oldString)
if index != lastIndex {
return error("old_string appears multiple times")
}
newContent = oldContent[:index] + newString + oldContent[index+len(oldString):]
}
}
唯一性检查
Edit Tool 的一个关键特性是唯一性检查。当 ReplaceAll 为 false 时,它会确保 OldString 在文件中只出现一次,防止模糊匹配导致的错误替换:
这里很明显,它的意图是说,如果万一要完成多出替换,或者仅做单个替换,那么这里要区别处理,很好的提醒,如果没看到这个我在写ARS一定会犯这个错,我觉得AI肯定也会。
index := strings.Index(oldContent, oldString)
lastIndex := strings.LastIndex(oldContent, oldString)
if index != lastIndex {
return fantasy.NewTextErrorResponse(
"old_string appears multiple times in the file. " +
"Please provide more context to ensure a unique match, " +
"or set replace_all to true"
)
}
2. MultiEdit Tool - 批量编辑利器
MultiEdit Tool 支持在单个文件上顺序执行多个编辑操作,这在需要进行多处修改时特别有用。
参数定义
type MultiEditParams struct {
FilePath string `json:"file_path"`
Edits []MultiEditOperation `json:"edits"`
}
type MultiEditOperation struct {
OldString string `json:"old_string"`
NewString string `json:"new_string"`
ReplaceAll bool `json:"replace_all"`
}
核心特性
1. 顺序执行与部分失败容错
MultiEdit Tool 会顺序应用每个编辑操作,如果某个操作失败,不会影响其他操作的执行:
// 应用所有编辑,记录失败的操作
var failedEdits []FailedEdit
for i, edit := range params.Edits {
newContent, err := applyEditToContent(currentContent, edit)
if err != nil {
failedEdits = append(failedEdits, FailedEdit{
Index: i + 1,
Error: err.Error(),
Edit: edit,
})
continue // 继续执行下一个编辑
}
currentContent = newContent
}
它的实际用意其实是担心由于权限啊,文件变化等原因,会导致编辑失败,如果失败了,那就接着弄其他文件。 这样听起来不合理,但是把tool执行结果抛回给Agent的时候其实Agent肯定会进行进一步的策略(比如调用Bash修改权限) 这种情况还蛮常见。
2. 支持创建时编辑
第一个编辑操作的 OldString 可以为空,表示创建文件:
if len(params.Edits) > 0 && params.Edits[0].OldString == "" {
response, err = processMultiEditWithCreation(editCtx, params, call)
} else {
response, err = processMultiEditExistingFile(editCtx, params, call)
}
3. 详细的执行报告
返回详细的执行统计信息:
type MultiEditResponseMetadata struct {
Additions int `json:"additions"` // 新增行数
Removals int `json:"removals"` // 删除行数
OldContent string `json:"old_content"` // 原始内容
NewContent string `json:"new_content"` // 修改后内容
EditsApplied int `json:"edits_applied"` // 成功应用的编辑数
EditsFailed []FailedEdit `json:"edits_failed"` // 失败的编辑列表
}
编辑应用逻辑
func applyEditToContent(content string, edit MultiEditOperation) (string, error) {
if edit.OldString == "" {
return "", fmt.Errorf("old_string cannot be empty for content replacement")
}
if edit.ReplaceAll {
newContent = strings.ReplaceAll(content, edit.OldString, edit.NewString)
replacementCount := strings.Count(content, edit.OldString)
if replacementCount == 0 {
return "", fmt.Errorf("old_string not found in content")
}
} else {
// 单次替换,需要确保唯一性
index := strings.Index(content, edit.OldString)
if index == -1 {
return "", fmt.Errorf("old_string not found in content")
}
lastIndex := strings.LastIndex(content, edit.OldString)
if index != lastIndex {
return "", fmt.Errorf("old_string appears multiple times")
}
newContent = content[:index] + edit.NewString + content[index+len(edit.OldString):]
}
return newContent, nil
}
3. Write Tool - 完整文件写入
Write Tool 用于写入完整的文件内容,适合创建新文件或完全重写文件的场景。
参数定义
type WriteParams struct {
FilePath string `json:"file_path"` // 文件路径
Content string `json:"content"` // 完整的文件内容
}
与 Edit Tool 的区别
| 特性 | Edit Tool | Write Tool |
|---|---|---|
| 输入方式 | 需要提供 OldString 上下文 | 只需提供完整内容 |
| 使用场景 | 局部修改 | 创建新文件或完全重写 |
| 性能 | 更高效(只传输差异) | 传输完整内容 |
| 安全性 | 更安全(明确修改位置) | 较低(可能误覆盖) |
核心实现
func NewWriteTool(...) fantasy.AgentTool {
return fantasy.NewAgentTool(
WriteToolName,
string(writeDescription),
func(ctx context.Context, params WriteParams, call fantasy.ToolCall) {
// 1. 检查文件是否存在且已修改
fileInfo, err := os.Stat(filePath)
if err == nil {
modTime := fileInfo.ModTime()
lastRead := getLastReadTime(filePath)
if modTime.After(lastRead) {
return error("File has been modified since last read")
}
// 检查内容是否相同
oldContent, _ := os.ReadFile(filePath)
if string(oldContent) == params.Content {
return error("File already contains the exact content")
}
}
// 2. 创建父目录
os.MkdirAll(filepath.Dir(filePath), 0o755)
// 3. 生成 Diff
diff, additions, removals := diff.GenerateDiff(
oldContent, params.Content, filePath,
)
// 4. 请求权限
if !permissions.Request(...) {
return ErrorPermissionDenied
}
// 5. 写入文件
os.WriteFile(filePath, []byte(params.Content), 0o644)
// 6. 更新历史
files.CreateVersion(ctx, sessionID, filePath, params.Content)
// 7. 通知 LSP 并返回诊断信息
notifyLSPs(ctx, lspClients, params.FilePath)
return getDiagnostics(filePath, lspClients)
},
)
}
完整的编辑工作流程
下面这个流程我让Codex帮我梳理的,我仔细对照了代码,并做了部分调整和补充,确保它我们总结的是有依有据的。
所有编辑工具都遵循相同的标准化工作流程,确保操作的安全性和可追溯性:
┌─────────────────────────────────────────────────────────────┐
│ 1. 参数验证 │
│ - FilePath 非空检查 │
│ - 其他必需参数验证 │
└─────────────────────┬───────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ 2. 路径处理 │
│ filepathext.SmartJoin(workingDir, params.FilePath) │
│ - 相对路径转换为绝对路径 │
└─────────────────────┬───────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ 3. 文件状态检查 │
│ ├─ 检查文件是否已读取: getLastReadTime() │
│ ├─ 检查修改时间: modTime.After(lastRead) │
│ ├─ 检查是否是目录 │
│ └─ 检查文件大小限制 │
└─────────────────────┬───────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ 4. 读取并处理文件内容 │
│ content, _ := os.ReadFile(filePath) │
│ oldContent, isCrlf := fsext.ToUnixLineEndings(content) │
│ - 统一为 Unix 行结束符便于处理 │
└─────────────────────┬───────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ 5. 执行编辑操作 │
│ ├─ strings.Index() 查找匹配位置 │
│ ├─ 检查唯一性(非 ReplaceAll 模式) │
│ └─ 执行字符串替换 │
└─────────────────────┬───────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ 6. 生成 Diff │
│ diff, additions, removals := diff.GenerateDiff(...) │
│ - 生成 unified diff 格式 │
│ - 计算添加/删除的行数 │
└─────────────────────┬───────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ 7. 权限请求 │
│ p := permissions.Request( │
│ SessionID, Path, ToolCallID, ToolName, │
│ Action, Description, Params │
│ ) │
│ - 发送到 UI 等待用户确认 │
└─────────────────────┬───────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ 8. 恢复行结束符格式 │
│ if isCrlf { │
│ newContent = fsext.ToWindowsLineEndings(newContent) │
│ } │
└─────────────────────┬───────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ 9. 写入文件 │
│ os.WriteFile(filePath, []byte(newContent), 0o644) │
└─────────────────────┬───────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ 10. 更新文件历史 │
│ ├─ files.GetByPathAndSession() 获取历史 │
│ ├─ files.Create() 如果不存在 │
│ ├─ files.CreateVersion() 保存中间版本(如有手动修改) │
│ └─ files.CreateVersion() 保存新版本 │
└─────────────────────┬───────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ 11. 记录读写时间 │
│ recordFileWrite(filePath) │
│ recordFileRead(filePath) │
└─────────────────────┬───────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ 12. 通知 LSP 客户端 │
│ notifyLSPs(ctx, lspClients, filePath) │
│ - 触发 LSP 重新分析文件 │
└─────────────────────┬───────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ 13. 收集诊断信息 │
│ diagnostics := getDiagnostics(filePath, lspClients) │
│ - 获取语法错误、类型错误等 │
└─────────────────────┬───────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ 14. 返回响应 │
│ - 操作结果消息 │
│ - 元数据(Diff、统计信息) │
│ - LSP 诊断信息 │
└─────────────────────────────────────────────────────────────┘
关键安全机制
Crush 的编辑工具实现了多层安全机制,确保代码修改操作的安全性和可靠性。
1. 读取前置检查
所有编辑操作都要求先使用 View Tool 读取文件,这样可以:
- 确保 AI 看到了最新的文件内容
- 记录文件的读取时间用于后续验证
- 避免盲目修改文件
if getLastReadTime(filePath).IsZero() {
return fantasy.NewTextErrorResponse(
"you must read the file before editing it. Use the View tool first"
)
}
2. 修改时间验证
防止编辑被其他进程修改过的文件,避免覆盖未知的更改:
modTime := fileInfo.ModTime()
lastRead := getLastReadTime(filePath)
if modTime.After(lastRead) {
return fantasy.NewTextErrorResponse(
fmt.Sprintf(
"file %s has been modified since it was last read\n" +
"mod time: %s, last read: %s",
filePath,
modTime.Format(time.RFC3339),
lastRead.Format(time.RFC3339),
)
)
}
3. 唯一性检查
当 ReplaceAll 为 false 时,确保 OldString 在文件中只出现一次:
index := strings.Index(oldContent, oldString)
lastIndex := strings.LastIndex(oldContent, oldString)
if index != lastIndex {
return fantasy.NewTextErrorResponse(
"old_string appears multiple times in the file. " +
"Please provide more context to ensure a unique match, " +
"or set replace_all to true"
)
}
4. 权限系统
每次编辑操作都需要通过权限服务的验证,支持:
- 会话级别的权限管理
- 工具和操作级别的控制
- 用户交互式确认
type CreatePermissionRequest struct {
SessionID string // 会话 ID
ToolCallID string // 工具调用 ID
ToolName string // 工具名称 (edit, write, multiedit)
Description string // 人类可读的操作描述
Action string // 操作类型 (read, write)
Params any // 工具参数
Path string // 文件路径
}
p := permissions.Request(permissionRequest)
if !p {
return fantasy.ToolResponse{}, permission.ErrorPermissionDenied
}
权限请求会包含完整的 Diff 信息,用户可以在批准前看到具体的修改内容。
5. 文件历史追踪
Crush 为每个会话维护完整的文件修改历史,支持版本回溯:
// 检查文件是否已在历史中
file, err := files.GetByPathAndSession(ctx, filePath, sessionID)
if err != nil {
// 首次编辑,创建历史记录
files.Create(ctx, sessionID, filePath, oldContent)
}
// 如果用户手动修改过文件,保存中间版本
if file.Content != oldContent {
files.CreateVersion(ctx, sessionID, filePath, oldContent)
}
// 保存新版本
files.CreateVersion(ctx, sessionID, filePath, newContent)
历史记录数据结构:
type File struct {
ID string // 唯一标识
SessionID string // 所属会话
Path string // 文件路径
Content string // 文件内容
Version int64 // 版本号
CreatedAt int64 // 创建时间
UpdatedAt int64 // 更新时间
}
6. 内容相同检查
避免无意义的写入操作:
if oldContent == newContent {
return fantasy.NewTextErrorResponse(
"new content is the same as old content. No changes made."
)
}
LSP 集成
Crush 与 Language Server Protocol (LSP) 深度集成,提供实时的代码诊断功能。
通知 LSP 客户端
编辑完成后自动通知 LSP 客户端文件已更改:
func notifyLSPs(
ctx context.Context,
lsps *csync.Map[string, *lsp.Client],
filepath string,
) {
// 遍历所有 LSP 客户端
lsps.Range(func(lang string, client *lsp.Client) bool {
// 检查文件是否匹配该语言
if client.MatchesFile(filepath) {
// 发送文件更改通知
client.DidChange(ctx, filepath)
}
return true
})
}
收集诊断信息
获取并格式化 LSP 诊断结果:
func getDiagnostics(
filePath string,
lsps *csync.Map[string, *lsp.Client],
) string {
var diagnostics []lsp.Diagnostic
// 从所有匹配的 LSP 客户端收集诊断
lsps.Range(func(lang string, client *lsp.Client) bool {
if client.MatchesFile(filePath) {
diags := client.GetDiagnostics(filePath)
diagnostics = append(diagnostics, diags...)
}
return true
})
if len(diagnostics) == 0 {
return ""
}
// 格式化诊断信息
var output strings.Builder
output.WriteString("\n<diagnostics>\n")
for _, diag := range diagnostics {
output.WriteString(fmt.Sprintf(
"%s:%d:%d: %s: %s\n",
filePath,
diag.Range.Start.Line,
diag.Range.Start.Character,
diag.Severity,
diag.Message,
))
}
output.WriteString("</diagnostics>\n")
return output.String()
}
集成效果
编辑完成后的响应示例:
<result>
Content replaced in file: /path/to/file.go
</result>
<diagnostics>
/path/to/file.go:15:5: error: undefined: variableName
/path/to/file.go:23:10: warning: unused variable 'temp'
</diagnostics>
这使得 AI 可以立即发现语法错误或类型错误,并在下一步操作中进行修正。
跨平台兼容性
行结束符处理
Crush 智能处理不同平台的行结束符差异(Windows 使用 CRLF \r\n,Unix/Linux 使用 LF \n):
// 1. 读取时转换为 Unix 格式
oldContent, isCrlf := fsext.ToUnixLineEndings(string(content))
// 2. 在统一的 Unix 格式上进行所有编辑操作
newContent := performEdits(oldContent)
// 3. 写入前恢复原始格式
if isCrlf {
newContent, _ = fsext.ToWindowsLineEndings(newContent)
}
// 4. 写入文件
os.WriteFile(filePath, []byte(newContent), 0o644)
实现函数:
// 转换为 Unix 行结束符,返回转换后的内容和是否原本是 CRLF
func ToUnixLineEndings(content string) (string, bool) {
isCrlf := strings.Contains(content, "\r\n")
return strings.ReplaceAll(content, "\r\n", "\n"), isCrlf
}
// 转换为 Windows 行结束符
func ToWindowsLineEndings(content string) (string, bool) {
// 先确保是 Unix 格式(防止 \r\n\r\n 的情况)
content = strings.ReplaceAll(content, "\r\n", "\n")
return strings.ReplaceAll(content, "\n", "\r\n"), true
}
这种处理方式确保了:
- 编辑逻辑简单:只需处理一种行结束符
- 保持原格式:文件原本是什么格式就保持什么格式
- 跨平台协作:在不同平台间切换不会导致格式混乱
路径处理
智能路径连接,支持相对路径和绝对路径:
func SmartJoin(workingDir, filePath string) string {
if filepath.IsAbs(filePath) {
return filePath
}
return filepath.Join(workingDir, filePath)
}
工具初始化与依赖注入
依赖关系
编辑工具依赖以下核心服务:
type editContext struct {
ctx context.Context // 上下文
permissions permission.Service // 权限服务
files history.Service // 历史服务
workingDir string // 工作目录
}
func NewEditTool(
lspClients *csync.Map[string, *lsp.Client], // LSP 客户端
permissions permission.Service, // 权限服务
files history.Service, // 历史服务
workingDir string, // 工作目录
) fantasy.AgentTool {
// ...
}
工具注册
所有工具在 Coordinator 中初始化并注册:
// internal/agent/coordinator.go
func (c *Coordinator) initializeTools() []fantasy.AgentTool {
allTools := []fantasy.AgentTool{
// 文件查看
tools.NewViewTool(
c.lspClients,
c.permissions,
c.cfg.WorkingDir(),
),
// 文件编辑
tools.NewEditTool(
c.lspClients,
c.permissions,
c.history,
c.cfg.WorkingDir(),
),
// 批量编辑
tools.NewMultiEditTool(
c.lspClients,
c.permissions,
c.history,
c.cfg.WorkingDir(),
),
// 文件写入
tools.NewWriteTool(
c.lspClients,
c.permissions,
c.history,
c.cfg.WorkingDir(),
),
// 其他工具...
tools.NewBashTool(...),
tools.NewGlobTool(...),
tools.NewGrepTool(...),
}
return allTools
}
Fantasy 框架集成
Crush 使用 Charm 的 Fantasy 框架来定义和管理工具:
func NewEditTool(...) fantasy.AgentTool {
return fantasy.NewAgentTool(
"edit", // 工具名称
string(editDescription), // 工具描述(来自 embed 的 markdown)
func(ctx context.Context, params EditParams, call fantasy.ToolCall) (fantasy.ToolResponse, error) {
// 工具执行逻辑
// ...
return fantasy.WithResponseMetadata(
fantasy.NewTextResponse("Content replaced in file: " + filePath),
EditResponseMetadata{
OldContent: oldContent,
NewContent: newContent,
Additions: additions,
Removals: removals,
},
), nil
},
)
}
Fantasy 框架提供:
- 自动参数验证和类型转换
- 统一的响应格式
- 元数据附加支持
- 错误处理机制
实际使用示例
示例 1:修改函数实现
场景:将一个函数中的变量名从 oldName 改为 newName
// AI 发起的工具调用
{
"tool": "edit",
"parameters": {
"file_path": "internal/service/user.go",
"old_string": "func UpdateUser(oldName string) error {\n return db.Update(oldName)\n}",
"new_string": "func UpdateUser(newName string) error {\n return db.Update(newName)\n}",
"replace_all": false
}
}
执行流程:
- ✅ 验证文件已被读取
- ✅ 检查文件未被修改
- ✅ 查找并确认唯一匹配
- ✅ 生成 Diff 并请求权限
- ✅ 用户确认后执行替换
- ✅ 保存历史版本
- ✅ 通知 LSP 并获取诊断
响应:
<result>
Content replaced in file: internal/service/user.go
</result>
<diagnostics>
No errors found.
</diagnostics>
示例 2:批量修改导入语句
场景:在一个文件中替换多个旧的包导入路径
// AI 发起的工具调用
{
"tool": "multiedit",
"parameters": {
"file_path": "cmd/main.go",
"edits": [
{
"old_string": "\"github.com/old/package1\"",
"new_string": "\"github.com/new/package1\"",
"replace_all": false
},
{
"old_string": "\"github.com/old/package2\"",
"new_string": "\"github.com/new/package2\"",
"replace_all": false
},
{
"old_string": "old.Function",
"new_string": "new.Function",
"replace_all": true
}
]
}
}
响应:
<result>
Applied 3 edits to file: cmd/main.go
</result>
<diagnostics>
No errors found.
</diagnostics>
示例 3:创建新配置文件
场景:创建一个新的 JSON 配置文件
// AI 发起的工具调用
{
"tool": "write",
"parameters": {
"file_path": "config/database.json",
"content": "{\n \"host\": \"localhost\",\n \"port\": 5432,\n \"database\": \"myapp\",\n \"ssl\": true\n}"
}
}
执行流程:
- ✅ 检查文件不存在
- ✅ 创建父目录
config/ - ✅ 请求写入权限
- ✅ 写入文件内容
- ✅ 创建历史记录
响应:
<result>
File successfully written: config/database.json
</result>
设计亮点总结
1. 职责分离
三个编辑工具各司其职:
- Edit: 精确的局部修改
- MultiEdit: 批量顺序编辑
- Write: 完整文件创建/重写
2. 安全第一
多层安全检查确保操作安全:
- 读取前置检查
- 修改时间验证
- 唯一性检查
- 权限系统
- 历史追踪
3. 智能容错
- MultiEdit 支持部分失败
- 详细的错误信息
- 失败编辑的统计报告
4. 跨平台兼容
- 智能行结束符处理
- 路径规范化
- 跨平台文件系统兼容
5. LSP 深度集成
- 自动诊断收集
- 实时错误反馈
- 多语言支持
6. 完整的可追溯性
- 版本历史记录
- Diff 生成
- 详细的元数据
7. 优秀的可扩展性
- 依赖注入设计
- Fantasy 框架集成
- 模块化架构
性能考量
1. 内存管理
// 大文件处理限制
const MaxFileSize = 250 * 1024 // 250KB
if fileInfo.Size() > MaxFileSize {
return error("File too large")
}
2. 字符串操作优化
使用 Go 标准库的高效字符串函数:
strings.Index()- O(n*m) 查找strings.ReplaceAll()- 单次遍历替换strings.Builder- 高效字符串拼接
3. 并发安全
// 使用并发安全的 Map
type Map[K comparable, V any] struct {
mu sync.RWMutex
m map[K]V
}
未来改进方向
看完之后感受还是挺强烈的,Crush把这个工具封装的思路简单到让我震惊,我还思考了很久他们是怎么做出左右拆分的git对比 但是一旦你接受了这个思路,它的实现几乎是天然的融入到了渲染和实际执行中,抽象概念完全对齐,太完美了。单即便如此我还是觉得有额外的优化空间。 起码我在做实现的时候可以再多做一点点更好的。以下是我和AI共同整理的一些思路,先记下,回头实现的时候可以捡起来。
1. 增量编辑支持
当前实现需要读取整个文件,对于大文件可以考虑:
- 行号定位编辑
- 字节偏移量编辑
- 流式处理
2. 并行批量编辑
MultiEdit 当前是顺序执行,可以考虑:
- 依赖分析
- 无冲突编辑并行执行
- 冲突检测和解决
3. 更智能的冲突处理
- 三方合并算法
- 自动冲突解决
- 交互式冲突解决
4. 性能优化
- 增量 Diff 算法
- 文件内容缓存
- LSP 诊断缓存
总结
Crush 的代码修改工具展示了一个生产级 AI 编程助手应该具备的核心特性:
- 安全性:多层验证确保不会误修改文件
- 可靠性:完整的错误处理和历史追踪
- 易用性:简洁的 API 和清晰的错误信息
- 可扩展性:模块化设计便于添加新功能
- 智能性:LSP 集成提供实时反馈
这些设计原则和实现细节为我构建 ARS 代码助手提供了宝贵的参考。无论是开发类似的工具,还是理解 AI 如何安全地操作代码,Crush 的实现都值得深入学习。
参考资源
- 项目地址: github.com/charmbracelet/crush
- Fantasy 框架: charm.land/fantasy
- LSP 协议: microsoft.github.io/language-server-protocol
- Go 文件操作: pkg.go.dev/os
本文分析基于 Crush 代码库当前版本(v0.21.0),部分实现细节可能随版本更新而变化。