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:
liuzh
2026-04-01 19:22:04 +08:00
co-authored by Copilot
parent 2a565d314d
commit d67f41358d
33 changed files with 2456 additions and 0 deletions
@@ -0,0 +1,108 @@
package com.claudecode.console;
/**
* ANSI 终端样式工具类 —— 对应 claude-code/src/utils/terminal.ts 的颜色输出功能。
* <p>
* 提供终端颜色和样式的便捷方法。
*/
public final class AnsiStyle {
private AnsiStyle() {}
// 重置
public static final String RESET = "\033[0m";
// 基本样式
public static final String BOLD = "\033[1m";
public static final String DIM = "\033[2m";
public static final String ITALIC = "\033[3m";
public static final String UNDERLINE = "\033[4m";
// 前景色
public static final String BLACK = "\033[30m";
public static final String RED = "\033[31m";
public static final String GREEN = "\033[32m";
public static final String YELLOW = "\033[33m";
public static final String BLUE = "\033[34m";
public static final String MAGENTA = "\033[35m";
public static final String CYAN = "\033[36m";
public static final String WHITE = "\033[37m";
// 亮色前景
public static final String BRIGHT_BLACK = "\033[90m";
public static final String BRIGHT_RED = "\033[91m";
public static final String BRIGHT_GREEN = "\033[92m";
public static final String BRIGHT_YELLOW = "\033[93m";
public static final String BRIGHT_BLUE = "\033[94m";
public static final String BRIGHT_MAGENTA = "\033[95m";
public static final String BRIGHT_CYAN = "\033[96m";
public static final String BRIGHT_WHITE = "\033[97m";
// 背景色
public static final String BG_RED = "\033[41m";
public static final String BG_GREEN = "\033[42m";
public static final String BG_YELLOW = "\033[43m";
public static final String BG_BLUE = "\033[44m";
// ---- 便捷方法 ----
public static String bold(String text) {
return BOLD + text + RESET;
}
public static String dim(String text) {
return DIM + text + RESET;
}
public static String italic(String text) {
return ITALIC + text + RESET;
}
public static String red(String text) {
return RED + text + RESET;
}
public static String green(String text) {
return GREEN + text + RESET;
}
public static String yellow(String text) {
return YELLOW + text + RESET;
}
public static String blue(String text) {
return BLUE + text + RESET;
}
public static String cyan(String text) {
return CYAN + text + RESET;
}
public static String magenta(String text) {
return MAGENTA + text + RESET;
}
public static String brightBlack(String text) {
return BRIGHT_BLACK + text + RESET;
}
/** 带颜色的前缀标签 */
public static String tag(String label, String color) {
return color + BOLD + "[" + label + "]" + RESET;
}
/** 带背景色的标签 */
public static String badge(String label, String bgColor) {
return bgColor + WHITE + BOLD + " " + label + " " + RESET;
}
/** 清除当前行 */
public static String clearLine() {
return "\033[2K\r";
}
/** 光标上移 n 行 */
public static String cursorUp(int n) {
return "\033[" + n + "A";
}
}
@@ -0,0 +1,51 @@
package com.claudecode.console;
import java.io.PrintStream;
/**
* Banner 打印器 —— 对应 claude-code/src/components/Banner.tsx。
* <p>
* 在启动时打印 ASCII Art Logo 和版本信息。
*/
public class BannerPrinter {
private static final String VERSION = "0.1.0-SNAPSHOT";
/**
* 打印 claude-code-java 启动 banner。
*/
public static void print(PrintStream out) {
String banner = """
%s
██████╗██╗ █████╗ ██╗ ██╗██████╗ ███████╗
██╔════╝██║ ██╔══██╗██║ ██║██╔══██╗██╔════╝
██║ ██║ ███████║██║ ██║██║ ██║█████╗
██║ ██║ ██╔══██║██║ ██║██║ ██║██╔══╝
╚██████╗███████╗██║ ██║╚██████╔╝██████╔╝███████╗
╚═════╝╚══════╝╚═╝ ╚═╝ ╚═════╝ ╚═════╝ ╚══════╝
%s ██████╗ ██████╗ ██████╗ ███████╗
██╔════╝██╔═══██╗██╔══██╗██╔════╝
██║ ██║ ██║██║ ██║█████╗
██║ ██║ ██║██║ ██║██╔══╝
╚██████╗╚██████╔╝██████╔╝███████╗
╚═════╝ ╚═════╝ ╚═════╝ ╚══════╝
%s
""".formatted(AnsiStyle.BRIGHT_CYAN, AnsiStyle.BRIGHT_MAGENTA, AnsiStyle.RESET);
out.println(banner);
out.println(AnsiStyle.bold(" Claude Code (Java)") + AnsiStyle.dim(" v" + VERSION));
out.println(AnsiStyle.dim(" Powered by Spring AI • Type /help for commands"));
out.println();
}
/**
* 精简版 banner(用于窄终端)。
*/
public static void printCompact(PrintStream out) {
out.println();
out.println(AnsiStyle.BRIGHT_CYAN + AnsiStyle.BOLD + " ◆ Claude Code (Java)" + AnsiStyle.RESET
+ AnsiStyle.dim(" v" + VERSION));
out.println(AnsiStyle.dim(" Type /help for commands • Ctrl+D to exit"));
out.println();
}
}
@@ -0,0 +1,79 @@
package com.claudecode.console;
import java.io.PrintStream;
/**
* Markdown 简易渲染器 —— 对应 claude-code/src/renderers/markdown.ts。
* <p>
* 将 AI 回复中的 Markdown 格式转换为终端 ANSI 样式输出。
* 这是一个简化版,支持常见格式。
*/
public class MarkdownRenderer {
private final PrintStream out;
public MarkdownRenderer(PrintStream out) {
this.out = out;
}
/** 渲染 Markdown 文本 */
public void render(String markdown) {
if (markdown == null || markdown.isBlank()) return;
boolean inCodeBlock = false;
String codeBlockLang = "";
for (String line : markdown.lines().toList()) {
// 代码块
if (line.stripLeading().startsWith("```")) {
if (!inCodeBlock) {
codeBlockLang = line.stripLeading().substring(3).strip();
inCodeBlock = true;
out.println(AnsiStyle.dim(" ┌─" + (codeBlockLang.isEmpty() ? "code" : codeBlockLang) + ""));
continue;
} else {
inCodeBlock = false;
out.println(AnsiStyle.dim(" └─────"));
continue;
}
}
if (inCodeBlock) {
out.println(AnsiStyle.BRIGHT_GREEN + "" + line + AnsiStyle.RESET);
continue;
}
// 标题
if (line.startsWith("### ")) {
out.println(AnsiStyle.bold(AnsiStyle.CYAN + " " + line.substring(4)) + AnsiStyle.RESET);
} else if (line.startsWith("## ")) {
out.println(AnsiStyle.bold(AnsiStyle.BLUE + " " + line.substring(3)) + AnsiStyle.RESET);
} else if (line.startsWith("# ")) {
out.println(AnsiStyle.bold(AnsiStyle.MAGENTA + " " + line.substring(2)) + AnsiStyle.RESET);
}
// 列表项
else if (line.stripLeading().startsWith("- ") || line.stripLeading().startsWith("* ")) {
out.println(" " + AnsiStyle.CYAN + "" + AnsiStyle.RESET + " " + renderInline(line.stripLeading().substring(2)));
}
// 分隔线
else if (line.strip().matches("^-{3,}$") || line.strip().matches("^\\*{3,}$")) {
out.println(AnsiStyle.dim(" ─────────────────────────────────────────"));
}
// 普通文本
else {
out.println(" " + renderInline(line));
}
}
}
/** 行内格式渲染 */
private String renderInline(String text) {
// 粗体 **text**
text = text.replaceAll("\\*\\*(.+?)\\*\\*", AnsiStyle.BOLD + "$1" + AnsiStyle.RESET);
// 行内代码 `text`
text = text.replaceAll("`(.+?)`", AnsiStyle.BRIGHT_GREEN + "$1" + AnsiStyle.RESET);
// 斜体 *text*
text = text.replaceAll("\\*(.+?)\\*", AnsiStyle.ITALIC + "$1" + AnsiStyle.RESET);
return text;
}
}
@@ -0,0 +1,72 @@
package com.claudecode.console;
import java.io.PrintStream;
/**
* 加载动画(Spinner)—— 对应 claude-code/src/components/Spinner.tsx。
* <p>
* 在等待 AI 响应时显示旋转动画。
*/
public class SpinnerAnimation {
private static final String[] FRAMES = {"", "", "", "", "", "", "", "", "", ""};
private static final int INTERVAL_MS = 80;
private final PrintStream out;
private volatile boolean running;
private Thread thread;
private String message = "Thinking";
public SpinnerAnimation(PrintStream out) {
this.out = out;
}
/** 启动 spinner */
public void start(String message) {
if (running) return;
this.message = message;
this.running = true;
thread = Thread.ofVirtual().name("spinner").start(() -> {
int idx = 0;
while (running) {
out.print(AnsiStyle.clearLine());
out.print(AnsiStyle.CYAN + " " + FRAMES[idx % FRAMES.length]
+ " " + AnsiStyle.RESET + AnsiStyle.dim(this.message));
out.flush();
idx++;
try {
Thread.sleep(INTERVAL_MS);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
}
}
// 清除 spinner 行
out.print(AnsiStyle.clearLine());
out.flush();
});
}
/** 停止 spinner */
public void stop() {
running = false;
if (thread != null) {
thread.interrupt();
try {
thread.join(200);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
/** 更新消息 */
public void updateMessage(String newMessage) {
this.message = newMessage;
}
public boolean isRunning() {
return running;
}
}
@@ -0,0 +1,43 @@
package com.claudecode.console;
import java.io.PrintStream;
/**
* Thinking 内容渲染器 —— 对应 claude-code/src/components/Thinking.tsx。
* <p>
* 显示 AI 模型的思考过程(extended thinking)。
*/
public class ThinkingRenderer {
private final PrintStream out;
public ThinkingRenderer(PrintStream out) {
this.out = out;
}
/** 渲染 thinking 内容块 */
public void render(String thinkingContent) {
if (thinkingContent == null || thinkingContent.isBlank()) {
return;
}
out.println();
out.println(AnsiStyle.DIM + AnsiStyle.ITALIC + " 💭 Thinking..." + AnsiStyle.RESET);
// 显示 thinking 内容(缩进并用暗色)
for (String line : thinkingContent.lines().toList()) {
out.println(AnsiStyle.DIM + "" + line + AnsiStyle.RESET);
}
out.println();
}
/** 渲染 thinking 开始标记 */
public void renderStart() {
out.print(AnsiStyle.DIM + AnsiStyle.ITALIC + " 💭 Thinking..." + AnsiStyle.RESET);
}
/** 渲染 thinking 结束标记 */
public void renderEnd() {
out.println(AnsiStyle.clearLine());
}
}
@@ -0,0 +1,94 @@
package com.claudecode.console;
import java.io.PrintStream;
/**
* 工具调用状态渲染器 —— 对应 claude-code/src/components/ToolStatus.tsx。
* <p>
* 在终端中显示工具调用的进度和结果。
*/
public class ToolStatusRenderer {
private final PrintStream out;
public ToolStatusRenderer(PrintStream out) {
this.out = out;
}
/** 渲染工具调用开始 */
public void renderStart(String toolName, String args) {
out.println(AnsiStyle.dim(" ─────────────────────────────────────────"));
out.print(AnsiStyle.YELLOW + "" + AnsiStyle.BOLD + toolName + AnsiStyle.RESET);
out.println(AnsiStyle.dim(" running..."));
// 如果有简短参数,显示
if (args != null && !args.isBlank()) {
String summary = extractSummary(toolName, args);
if (summary != null) {
out.println(AnsiStyle.dim(" " + summary));
}
}
}
/** 渲染工具调用完成 */
public void renderEnd(String toolName, String result) {
// 截断长结果
String display = result;
if (display != null && display.length() > 500) {
display = display.substring(0, 497) + "...";
}
out.println(AnsiStyle.GREEN + "" + AnsiStyle.BOLD + toolName + AnsiStyle.RESET
+ AnsiStyle.dim(" done"));
if (display != null && !display.isBlank()) {
// 缩进输出每一行
for (String line : display.lines().toList()) {
out.println(AnsiStyle.dim(" " + line));
}
}
out.println(AnsiStyle.dim(" ─────────────────────────────────────────"));
}
/** 渲染工具错误 */
public void renderError(String toolName, String error) {
out.println(AnsiStyle.RED + "" + AnsiStyle.BOLD + toolName + AnsiStyle.RESET
+ AnsiStyle.red(" error"));
if (error != null) {
out.println(AnsiStyle.red(" " + error));
}
}
/** 从 JSON 参数中提取人类可读的摘要 */
private String extractSummary(String toolName, String args) {
try {
// 简单提取关键字段
if (args.contains("\"command\"")) {
int start = args.indexOf("\"command\"");
int valStart = args.indexOf("\"", start + 10) + 1;
int valEnd = args.indexOf("\"", valStart);
if (valStart > 0 && valEnd > valStart) {
String cmd = args.substring(valStart, Math.min(valEnd, valStart + 80));
return "$ " + cmd;
}
}
if (args.contains("\"file_path\"")) {
int start = args.indexOf("\"file_path\"");
int valStart = args.indexOf("\"", start + 12) + 1;
int valEnd = args.indexOf("\"", valStart);
if (valStart > 0 && valEnd > valStart) {
return args.substring(valStart, valEnd);
}
}
if (args.contains("\"pattern\"")) {
int start = args.indexOf("\"pattern\"");
int valStart = args.indexOf("\"", start + 10) + 1;
int valEnd = args.indexOf("\"", valStart);
if (valStart > 0 && valEnd > valStart) {
return "pattern: " + args.substring(valStart, valEnd);
}
}
} catch (Exception e) {
// 忽略解析错误
}
return null;
}
}