feat: Phase1 项目骨架 - Maven项目结构、核心AgentLoop、6个工具、命令系统、控制台渲染、REPL会话
实现内容: - pom.xml: JDK25 + Spring AI 2.0.0-M4 + JLine3 + Picocli - core/AgentLoop: 基于ChatModel的显式工具循环(非ChatClient) - tool/: Tool接口 + ToolRegistry + ToolCallbackAdapter(适配Spring AI) - tool/impl/: BashTool, FileReadTool, FileWriteTool, FileEditTool, GlobTool, GrepTool - command/: SlashCommand接口 + CommandRegistry + /help, /clear, /exit - console/: AnsiStyle, BannerPrinter, ToolStatusRenderer, ThinkingRenderer, SpinnerAnimation, MarkdownRenderer - context/: SystemPromptBuilder, ClaudeMdLoader(多级CLAUDE.md加载) - repl/ReplSession: REPL主循环(Scanner降级方案) - config/AppConfig: Spring Bean装配 - application.yml: Anthropic/OpenAI双模型配置 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,101 @@
|
||||
package com.claudecode.tool.impl;
|
||||
|
||||
import com.claudecode.tool.Tool;
|
||||
import com.claudecode.tool.ToolContext;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 文件编辑工具 —— 对应 claude-code/src/tools/edit/EditFileTool.ts。
|
||||
* <p>
|
||||
* 通过精确匹配 old_string 并替换为 new_string 来编辑文件。
|
||||
*/
|
||||
public class FileEditTool implements Tool {
|
||||
|
||||
@Override
|
||||
public String name() {
|
||||
return "Edit";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String description() {
|
||||
return """
|
||||
Make a targeted edit to a file by replacing an exact string match with new content. \
|
||||
The old_string must match exactly one location in the file. \
|
||||
Use Read tool first to understand the file content before editing.""";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String inputSchema() {
|
||||
return """
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"file_path": {
|
||||
"type": "string",
|
||||
"description": "Path to the file to edit"
|
||||
},
|
||||
"old_string": {
|
||||
"type": "string",
|
||||
"description": "The exact string to find and replace (must be unique)"
|
||||
},
|
||||
"new_string": {
|
||||
"type": "string",
|
||||
"description": "The replacement string"
|
||||
}
|
||||
},
|
||||
"required": ["file_path", "old_string", "new_string"]
|
||||
}""";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String execute(Map<String, Object> input, ToolContext context) {
|
||||
String filePath = (String) input.get("file_path");
|
||||
String oldString = (String) input.get("old_string");
|
||||
String newString = (String) input.get("new_string");
|
||||
Path path = context.getWorkDir().resolve(filePath).normalize();
|
||||
|
||||
if (!Files.exists(path)) {
|
||||
return "Error: File not found: " + path;
|
||||
}
|
||||
|
||||
try {
|
||||
String content = Files.readString(path, StandardCharsets.UTF_8);
|
||||
|
||||
// 检查 old_string 唯一性
|
||||
int firstIdx = content.indexOf(oldString);
|
||||
if (firstIdx == -1) {
|
||||
return "Error: old_string not found in file";
|
||||
}
|
||||
|
||||
int secondIdx = content.indexOf(oldString, firstIdx + 1);
|
||||
if (secondIdx != -1) {
|
||||
return "Error: old_string matches multiple locations. Be more specific.";
|
||||
}
|
||||
|
||||
// 执行替换
|
||||
String newContent = content.substring(0, firstIdx) + newString + content.substring(firstIdx + oldString.length());
|
||||
Files.writeString(path, newContent, StandardCharsets.UTF_8);
|
||||
|
||||
// 计算变更范围
|
||||
long oldLines = oldString.lines().count();
|
||||
long newLines = newString.lines().count();
|
||||
|
||||
return "✅ Edited " + path + " (replaced " + oldLines + " lines with " + newLines + " lines)";
|
||||
|
||||
} catch (IOException e) {
|
||||
return "Error editing file: " + e.getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String activityDescription(Map<String, Object> input) {
|
||||
return "✏️ Editing " + input.getOrDefault("file_path", "file");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user