diff --git a/src/main/java/com/claudecode/tool/impl/FileEditTool.java b/src/main/java/com/claudecode/tool/impl/FileEditTool.java
index afc4da9..b3a04a6 100644
--- a/src/main/java/com/claudecode/tool/impl/FileEditTool.java
+++ b/src/main/java/com/claudecode/tool/impl/FileEditTool.java
@@ -15,6 +15,7 @@ import java.util.Map;
* 文件编辑工具 —— 对应 claude-code/src/tools/edit/EditFileTool.ts。
*
* 通过精确匹配 old_string 并替换为 new_string 来编辑文件。
+ * 编辑后返回 unified diff 格式的变更摘要。
*/
public class FileEditTool implements Tool {
@@ -91,17 +92,88 @@ public class FileEditTool implements Tool {
String newContent = content.substring(0, firstIdx) + newString + content.substring(firstIdx + oldString.length());
Files.writeString(path, newContent, StandardCharsets.UTF_8);
- // 计算变更范围
+ // 生成 unified diff 摘要
+ String diff = generateUnifiedDiff(path.toString(), content, newContent);
+
+ // 计算变更统计
long oldLines = oldString.lines().count();
long newLines = newString.lines().count();
- return "✅ Edited " + path + " (replaced " + oldLines + " lines with " + newLines + " lines)";
+ StringBuilder result = new StringBuilder();
+ result.append("✅ Edited ").append(path)
+ .append(" (replaced ").append(oldLines)
+ .append(" lines with ").append(newLines).append(" lines)\n\n");
+ result.append(diff);
+
+ return result.toString().stripTrailing();
} catch (IOException e) {
return "Error editing file: " + e.getMessage();
}
}
+ /**
+ * 生成 unified diff 格式输出。
+ * 类似 TS 版本的 getPatchForEdit(),但使用纯 Java 实现。
+ */
+ private String generateUnifiedDiff(String filePath, String original, String modified) {
+ List oldLines = original.lines().collect(java.util.stream.Collectors.toList());
+ List newLines = modified.lines().collect(java.util.stream.Collectors.toList());
+
+ // 找出变更区域
+ int contextLines = 3;
+
+ // 找到第一个不同的行
+ int firstDiff = 0;
+ int minLen = Math.min(oldLines.size(), newLines.size());
+ while (firstDiff < minLen && oldLines.get(firstDiff).equals(newLines.get(firstDiff))) {
+ firstDiff++;
+ }
+
+ // 找到最后一个不同的行(从末尾向前扫描)
+ int oldEnd = oldLines.size();
+ int newEnd = newLines.size();
+ while (oldEnd > firstDiff && newEnd > firstDiff
+ && oldLines.get(oldEnd - 1).equals(newLines.get(newEnd - 1))) {
+ oldEnd--;
+ newEnd--;
+ }
+
+ // 计算包含上下文的范围
+ int ctxStart = Math.max(0, firstDiff - contextLines);
+ int oldCtxEnd = Math.min(oldLines.size(), oldEnd + contextLines);
+ int newCtxEnd = Math.min(newLines.size(), newEnd + contextLines);
+
+ StringBuilder sb = new StringBuilder();
+ sb.append("--- a/").append(filePath).append('\n');
+ sb.append("+++ b/").append(filePath).append('\n');
+
+ // Hunk header: @@ -oldStart,oldCount +newStart,newCount @@
+ int oldHunkSize = oldCtxEnd - ctxStart;
+ int newHunkSize = newCtxEnd - ctxStart - (oldEnd - firstDiff) + (newEnd - firstDiff);
+ sb.append(String.format("@@ -%d,%d +%d,%d @@%n",
+ ctxStart + 1, oldHunkSize, ctxStart + 1, newHunkSize));
+
+ // Context before
+ for (int i = ctxStart; i < firstDiff; i++) {
+ sb.append(' ').append(oldLines.get(i)).append('\n');
+ }
+ // Removed lines
+ for (int i = firstDiff; i < oldEnd; i++) {
+ sb.append('-').append(oldLines.get(i)).append('\n');
+ }
+ // Added lines
+ for (int i = firstDiff; i < newEnd; i++) {
+ sb.append('+').append(newLines.get(i)).append('\n');
+ }
+ // Context after
+ for (int i = oldEnd; i < oldCtxEnd; i++) {
+ sb.append(' ').append(oldLines.get(i)).append('\n');
+ }
+
+ return sb.toString();
+ }
+
@Override
public String activityDescription(Map input) {
return "✏️ Editing " + input.getOrDefault("file_path", "file");
diff --git a/src/main/java/com/claudecode/tool/impl/FileReadTool.java b/src/main/java/com/claudecode/tool/impl/FileReadTool.java
index 54159bb..9ca0d9e 100644
--- a/src/main/java/com/claudecode/tool/impl/FileReadTool.java
+++ b/src/main/java/com/claudecode/tool/impl/FileReadTool.java
@@ -7,19 +7,28 @@ import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
+import java.util.Base64;
import java.util.Map;
-import java.util.stream.Collectors;
+import java.util.Set;
/**
* 文件读取工具 —— 对应 claude-code/src/tools/read/ReadFileTool.ts。
*
- * 读取文件内容,支持行号范围过滤。
+ * 读取文件内容,支持行号范围过滤和图片读取(Base64编码)。
*/
public class FileReadTool implements Tool {
/** 单次读取最大行数 */
private static final int MAX_LINES = 2000;
+ /** 图片最大文件大小 (20MB) */
+ private static final long MAX_IMAGE_SIZE = 20 * 1024 * 1024;
+
+ /** 支持的图片扩展名 */
+ private static final Set IMAGE_EXTENSIONS = Set.of(
+ "png", "jpg", "jpeg", "gif", "webp", "svg", "bmp", "ico"
+ );
+
@Override
public String name() {
return "Read";
@@ -28,8 +37,10 @@ public class FileReadTool implements Tool {
@Override
public String description() {
return """
- Read the contents of a file. Use line_start and line_end to read specific line ranges. \
- For large files, read in chunks. Supports text files only.""";
+ Read the contents of a file. Supports text files with optional line ranges, \
+ and image files (png, jpg, jpeg, gif, webp, svg, bmp, ico) which are returned \
+ as Base64-encoded data with MIME type. For large text files, read in chunks \
+ using line_start and line_end.""";
}
@Override
@@ -44,11 +55,11 @@ public class FileReadTool implements Tool {
},
"line_start": {
"type": "integer",
- "description": "Starting line number (1-based, inclusive)"
+ "description": "Starting line number (1-based, inclusive). Only for text files."
},
"line_end": {
"type": "integer",
- "description": "Ending line number (1-based, inclusive)"
+ "description": "Ending line number (1-based, inclusive). Only for text files."
}
},
"required": ["file_path"]
@@ -72,6 +83,47 @@ public class FileReadTool implements Tool {
return "Error: Not a regular file: " + path;
}
+ // Check if it's an image file
+ String ext = getExtension(path.getFileName().toString());
+ if (IMAGE_EXTENSIONS.contains(ext)) {
+ return readImage(path, ext);
+ }
+
+ // Text file reading
+ return readText(path, input);
+ }
+
+ /**
+ * 读取图片文件,返回 Base64 编码数据和 MIME 类型信息。
+ */
+ private String readImage(Path path, String ext) {
+ try {
+ long size = Files.size(path);
+ if (size > MAX_IMAGE_SIZE) {
+ return "Error: Image too large (" + formatSize(size) + "). Max: " + formatSize(MAX_IMAGE_SIZE);
+ }
+ if (size == 0) {
+ return "Error: Image file is empty: " + path;
+ }
+
+ byte[] bytes = Files.readAllBytes(path);
+ String base64 = Base64.getEncoder().encodeToString(bytes);
+ String mimeType = getMimeType(ext);
+
+ return String.format("""
+ {"type": "image", "file_path": "%s", "mime_type": "%s", \
+ "size": %d, "base64_length": %d, "base64": "%s"}""",
+ escapeJson(path.toString()), mimeType, size, base64.length(), base64);
+
+ } catch (IOException e) {
+ return "Error reading image: " + e.getMessage();
+ }
+ }
+
+ /**
+ * 读取文本文件,支持行范围过滤。
+ */
+ private String readText(Path path, Map input) {
try {
var allLines = Files.readAllLines(path, StandardCharsets.UTF_8);
int total = allLines.size();
@@ -86,7 +138,6 @@ public class FileReadTool implements Tool {
end = ((Number) input.get("line_end")).intValue();
}
- // 参数校验
start = Math.max(1, start);
end = Math.min(total, end);
@@ -94,12 +145,10 @@ public class FileReadTool implements Tool {
return "Error: line_start (" + start + ") > line_end (" + end + ")";
}
- // 限制行数
if (end - start + 1 > MAX_LINES) {
end = start + MAX_LINES - 1;
}
- // 构建带行号的输出
StringBuilder sb = new StringBuilder();
for (int i = start - 1; i < end; i++) {
sb.append(String.format("%4d | %s%n", i + 1, allLines.get(i)));
@@ -116,8 +165,49 @@ public class FileReadTool implements Tool {
}
}
+ /**
+ * 获取文件扩展名(小写,不含点)。
+ */
+ private String getExtension(String filename) {
+ int dot = filename.lastIndexOf('.');
+ if (dot < 0 || dot == filename.length() - 1) return "";
+ return filename.substring(dot + 1).toLowerCase();
+ }
+
+ /**
+ * 根据扩展名返回 MIME 类型。
+ */
+ private String getMimeType(String ext) {
+ return switch (ext) {
+ case "png" -> "image/png";
+ case "jpg", "jpeg" -> "image/jpeg";
+ case "gif" -> "image/gif";
+ case "webp" -> "image/webp";
+ case "svg" -> "image/svg+xml";
+ case "bmp" -> "image/bmp";
+ case "ico" -> "image/x-icon";
+ default -> "application/octet-stream";
+ };
+ }
+
+ private String formatSize(long bytes) {
+ if (bytes < 1024) return bytes + " B";
+ if (bytes < 1024 * 1024) return String.format("%.1f KB", bytes / 1024.0);
+ return String.format("%.1f MB", bytes / (1024.0 * 1024));
+ }
+
+ private String escapeJson(String s) {
+ if (s == null) return "";
+ return s.replace("\\", "\\\\").replace("\"", "\\\"").replace("\n", "\\n");
+ }
+
@Override
public String activityDescription(Map input) {
- return "📄 Reading " + input.getOrDefault("file_path", "file");
+ String fp = (String) input.getOrDefault("file_path", "file");
+ String ext = getExtension(fp);
+ if (IMAGE_EXTENSIONS.contains(ext)) {
+ return "🖼️ Reading image " + fp;
+ }
+ return "📄 Reading " + fp;
}
}