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,17 @@
|
||||
package com.claudecode.command;
|
||||
|
||||
import com.claudecode.core.AgentLoop;
|
||||
import com.claudecode.tool.ToolRegistry;
|
||||
|
||||
import java.io.PrintStream;
|
||||
|
||||
/**
|
||||
* 命令执行上下文。
|
||||
*/
|
||||
public record CommandContext(
|
||||
AgentLoop agentLoop,
|
||||
ToolRegistry toolRegistry,
|
||||
PrintStream out,
|
||||
Runnable exitCallback
|
||||
) {
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
package com.claudecode.command;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* 命令注册中心 —— 对应 claude-code/src/commands.ts 中的命令集合管理。
|
||||
*/
|
||||
public class CommandRegistry {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(CommandRegistry.class);
|
||||
|
||||
private final Map<String, SlashCommand> commands = new LinkedHashMap<>();
|
||||
|
||||
/** 注册命令(包括别名) */
|
||||
public void register(SlashCommand command) {
|
||||
commands.put(command.name().toLowerCase(), command);
|
||||
for (String alias : command.aliases()) {
|
||||
commands.put(alias.toLowerCase(), command);
|
||||
}
|
||||
log.debug("注册命令: /{}", command.name());
|
||||
}
|
||||
|
||||
/** 批量注册 */
|
||||
public void registerAll(SlashCommand... cmds) {
|
||||
for (SlashCommand cmd : cmds) {
|
||||
register(cmd);
|
||||
}
|
||||
}
|
||||
|
||||
/** 解析并执行命令 */
|
||||
public Optional<String> dispatch(String input, CommandContext context) {
|
||||
if (!input.startsWith("/")) {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
String stripped = input.substring(1).strip();
|
||||
String[] parts = stripped.split("\\s+", 2);
|
||||
String cmdName = parts[0].toLowerCase();
|
||||
String args = parts.length > 1 ? parts[1] : "";
|
||||
|
||||
SlashCommand cmd = commands.get(cmdName);
|
||||
if (cmd == null) {
|
||||
return Optional.of("Unknown command: /" + cmdName + ". Type /help for available commands.");
|
||||
}
|
||||
|
||||
return Optional.of(cmd.execute(args, context));
|
||||
}
|
||||
|
||||
/** 判断输入是否是斜杠命令 */
|
||||
public boolean isCommand(String input) {
|
||||
return input != null && input.startsWith("/");
|
||||
}
|
||||
|
||||
/** 获取所有唯一命令(用于 /help) */
|
||||
public List<SlashCommand> getCommands() {
|
||||
return commands.values().stream().distinct().toList();
|
||||
}
|
||||
|
||||
/** 获取命令名称(用于 Tab 补全) */
|
||||
public Set<String> getCommandNames() {
|
||||
return Set.copyOf(commands.keySet());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package com.claudecode.command;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 斜杠命令接口 —— 对应 claude-code/src/commands.ts 中的 Command 类型。
|
||||
* <p>
|
||||
* 用于处理以 / 开头的用户输入命令。
|
||||
*/
|
||||
public interface SlashCommand {
|
||||
|
||||
/** 命令名称(不含 / 前缀) */
|
||||
String name();
|
||||
|
||||
/** 命令描述 */
|
||||
String description();
|
||||
|
||||
/** 命令别名列表 */
|
||||
default List<String> aliases() {
|
||||
return List.of();
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行命令。
|
||||
*
|
||||
* @param args 命令参数(/ 后的文本去掉命令名后的部分)
|
||||
* @param context 命令执行上下文
|
||||
* @return 命令输出文本
|
||||
*/
|
||||
String execute(String args, CommandContext context);
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package com.claudecode.command.impl;
|
||||
|
||||
import com.claudecode.command.CommandContext;
|
||||
import com.claudecode.command.SlashCommand;
|
||||
import com.claudecode.console.AnsiStyle;
|
||||
|
||||
/**
|
||||
* /clear 命令 —— 清除对话历史。
|
||||
*/
|
||||
public class ClearCommand implements SlashCommand {
|
||||
|
||||
@Override
|
||||
public String name() {
|
||||
return "clear";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String description() {
|
||||
return "Clear conversation history";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String execute(String args, CommandContext context) {
|
||||
if (context.agentLoop() != null) {
|
||||
context.agentLoop().reset();
|
||||
}
|
||||
return AnsiStyle.green(" ✓ Conversation history cleared.");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package com.claudecode.command.impl;
|
||||
|
||||
import com.claudecode.command.CommandContext;
|
||||
import com.claudecode.command.SlashCommand;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* /exit 命令 —— 退出应用。
|
||||
*/
|
||||
public class ExitCommand implements SlashCommand {
|
||||
|
||||
@Override
|
||||
public String name() {
|
||||
return "exit";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String description() {
|
||||
return "Exit the application";
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> aliases() {
|
||||
return List.of("quit", "q");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String execute(String args, CommandContext context) {
|
||||
if (context.exitCallback() != null) {
|
||||
context.exitCallback().run();
|
||||
}
|
||||
return "Goodbye!";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
package com.claudecode.command.impl;
|
||||
|
||||
import com.claudecode.command.CommandContext;
|
||||
import com.claudecode.command.SlashCommand;
|
||||
import com.claudecode.console.AnsiStyle;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* /help 命令 —— 对应 claude-code/src/commands/help.ts。
|
||||
*/
|
||||
public class HelpCommand implements SlashCommand {
|
||||
|
||||
@Override
|
||||
public String name() {
|
||||
return "help";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String description() {
|
||||
return "Show available commands";
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> aliases() {
|
||||
return List.of("?");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String execute(String args, CommandContext context) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append(AnsiStyle.bold("\n Available Commands:\n\n"));
|
||||
|
||||
for (SlashCommand cmd : context.toolRegistry() != null
|
||||
? List.<SlashCommand>of() // 这里后续会获取注册的命令
|
||||
: List.<SlashCommand>of()) {
|
||||
sb.append(String.format(" %s%-12s%s %s%n",
|
||||
AnsiStyle.CYAN, "/" + cmd.name(), AnsiStyle.RESET, cmd.description()));
|
||||
}
|
||||
|
||||
// 硬编码展示(后续重构为动态)
|
||||
sb.append(String.format(" %s%-12s%s %s%n", AnsiStyle.CYAN, "/help", AnsiStyle.RESET, "Show available commands"));
|
||||
sb.append(String.format(" %s%-12s%s %s%n", AnsiStyle.CYAN, "/clear", AnsiStyle.RESET, "Clear conversation history"));
|
||||
sb.append(String.format(" %s%-12s%s %s%n", AnsiStyle.CYAN, "/compact", AnsiStyle.RESET, "Compact conversation context"));
|
||||
sb.append(String.format(" %s%-12s%s %s%n", AnsiStyle.CYAN, "/cost", AnsiStyle.RESET, "Show token usage and cost"));
|
||||
sb.append(String.format(" %s%-12s%s %s%n", AnsiStyle.CYAN, "/model", AnsiStyle.RESET, "Show or switch AI model"));
|
||||
sb.append(String.format(" %s%-12s%s %s%n", AnsiStyle.CYAN, "/exit", AnsiStyle.RESET, "Exit the application"));
|
||||
|
||||
sb.append("\n");
|
||||
sb.append(AnsiStyle.dim(" Tips: Press Tab for command completion, Ctrl+D to exit\n"));
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user