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,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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user