feat: P2全部功能 - MCP协议+插件系统+任务管理+CLI增强
A组 CLI增强: - DiffRenderer: 彩色diff渲染(行号/stat摘要/unified格式) - /hooks: 查看已注册Hook列表 - /review: AI代码审查(支持--staged/文件路径) - /stats: 会话使用统计(tokens/费用/API调用/运行时长) - /branch: 对话分支管理(save/load/list/delete) - /rewind: 回退对话历史 - /tag: 对话位置标签 - /security-review: AI安全审查 B组 任务系统: - TaskManager: 后台任务管理(线程安全/自动执行/手动管理) - TaskCreate/TaskGet/TaskList/TaskUpdate 4个工具 - ConfigTool: 配置读写工具 C组 MCP协议: - McpClient: MCP客户端(JSON-RPC 2.0/工具发现/资源读取) - McpTransport: 传输层接口 - StdioTransport: StdIO传输实现(子进程/异步读写) - McpManager: 多MCP服务器管理(配置加载/生命周期) - McpToolBridge: MCP工具桥接为本地Tool - /mcp: MCP服务器管理命令 D组 插件系统: - Plugin接口 + PluginContext + PluginManager - JAR插件加载(ClassLoader隔离/Manifest读取) - OutputStylePlugin: 内置输出样式插件 - /plugin: 插件管理命令 集成: AppConfig注册全部18工具+28命令 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,198 @@
|
||||
package com.claudecode.command.impl;
|
||||
|
||||
import com.claudecode.command.CommandContext;
|
||||
import com.claudecode.command.SlashCommand;
|
||||
import com.claudecode.console.AnsiStyle;
|
||||
import org.springframework.ai.chat.messages.Message;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* /branch 命令 —— 管理对话分支。
|
||||
* <p>
|
||||
* 允许用户保存当前对话状态为命名分支,并在不同分支之间切换。
|
||||
* 分支数据存储在静态 Map 中,在 JVM 生命周期内持久化。
|
||||
* <p>
|
||||
* 支持的子命令:
|
||||
* <ul>
|
||||
* <li>{@code /branch save <name>} —— 将当前对话保存为分支</li>
|
||||
* <li>{@code /branch load <name>} —— 恢复到指定分支的对话状态</li>
|
||||
* <li>{@code /branch list} —— 列出所有已保存的分支</li>
|
||||
* <li>{@code /branch delete <name>} —— 删除指定分支</li>
|
||||
* </ul>
|
||||
*/
|
||||
public class BranchCommand implements SlashCommand {
|
||||
|
||||
/** 静态分支存储:分支名称 -> 消息快照 */
|
||||
private static final Map<String, BranchSnapshot> branches = new LinkedHashMap<>();
|
||||
|
||||
@Override
|
||||
public String name() {
|
||||
return "branch";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String description() {
|
||||
return "Manage conversation branches";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String execute(String args, CommandContext context) {
|
||||
if (context.agentLoop() == null) {
|
||||
return AnsiStyle.red(" ✗ AgentLoop 不可用。");
|
||||
}
|
||||
|
||||
String trimmedArgs = args != null ? args.trim() : "";
|
||||
|
||||
if (trimmedArgs.isEmpty()) {
|
||||
return showUsage();
|
||||
}
|
||||
|
||||
// 解析子命令和参数
|
||||
String[] parts = trimmedArgs.split("\\s+", 2);
|
||||
String subCommand = parts[0].toLowerCase();
|
||||
String branchName = parts.length > 1 ? parts[1].trim() : "";
|
||||
|
||||
return switch (subCommand) {
|
||||
case "save" -> saveBranch(branchName, context);
|
||||
case "load" -> loadBranch(branchName, context);
|
||||
case "list" -> listBranches(context);
|
||||
case "delete" -> deleteBranch(branchName);
|
||||
default -> AnsiStyle.red(" ✗ 未知子命令: " + subCommand) + "\n" + showUsage();
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存当前对话为命名分支。
|
||||
*
|
||||
* @param branchName 分支名称
|
||||
* @param context 命令上下文
|
||||
* @return 操作结果信息
|
||||
*/
|
||||
private String saveBranch(String branchName, CommandContext context) {
|
||||
if (branchName.isEmpty()) {
|
||||
return AnsiStyle.red(" ✗ 请指定分支名称。") + "\n"
|
||||
+ AnsiStyle.dim(" 用法: /branch save <name>");
|
||||
}
|
||||
|
||||
List<Message> currentHistory = context.agentLoop().getMessageHistory();
|
||||
|
||||
// 创建消息快照(深拷贝列表引用)
|
||||
List<Message> snapshot = new ArrayList<>(currentHistory);
|
||||
String timestamp = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
|
||||
branches.put(branchName, new BranchSnapshot(snapshot, timestamp));
|
||||
|
||||
return AnsiStyle.green(" ✓ 分支已保存: ") + AnsiStyle.bold(branchName) + "\n"
|
||||
+ AnsiStyle.dim(" 消息数: " + snapshot.size() + " 时间: " + timestamp);
|
||||
}
|
||||
|
||||
/**
|
||||
* 恢复到指定分支的对话状态。
|
||||
*
|
||||
* @param branchName 分支名称
|
||||
* @param context 命令上下文
|
||||
* @return 操作结果信息
|
||||
*/
|
||||
private String loadBranch(String branchName, CommandContext context) {
|
||||
if (branchName.isEmpty()) {
|
||||
return AnsiStyle.red(" ✗ 请指定分支名称。") + "\n"
|
||||
+ AnsiStyle.dim(" 用法: /branch load <name>");
|
||||
}
|
||||
|
||||
BranchSnapshot snapshot = branches.get(branchName);
|
||||
if (snapshot == null) {
|
||||
return AnsiStyle.red(" ✗ 分支不存在: " + branchName) + "\n"
|
||||
+ AnsiStyle.dim(" 使用 /branch list 查看所有可用分支。");
|
||||
}
|
||||
|
||||
// 恢复对话历史
|
||||
context.agentLoop().replaceHistory(new ArrayList<>(snapshot.messages()));
|
||||
|
||||
return AnsiStyle.green(" ✓ 已恢复到分支: ") + AnsiStyle.bold(branchName) + "\n"
|
||||
+ AnsiStyle.dim(" 已加载 " + snapshot.messages().size() + " 条消息 (保存于 " + snapshot.timestamp() + ")");
|
||||
}
|
||||
|
||||
/**
|
||||
* 列出所有已保存的分支。
|
||||
*
|
||||
* @param context 命令上下文
|
||||
* @return 分支列表信息
|
||||
*/
|
||||
private String listBranches(CommandContext context) {
|
||||
if (branches.isEmpty()) {
|
||||
return AnsiStyle.dim(" 没有保存的分支。") + "\n"
|
||||
+ AnsiStyle.dim(" 使用 /branch save <name> 保存当前对话。");
|
||||
}
|
||||
|
||||
int currentSize = context.agentLoop().getMessageHistory().size();
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append(AnsiStyle.bold("\n 🌿 Conversation Branches\n\n"));
|
||||
|
||||
for (Map.Entry<String, BranchSnapshot> entry : branches.entrySet()) {
|
||||
String name = entry.getKey();
|
||||
BranchSnapshot snapshot = entry.getValue();
|
||||
int msgCount = snapshot.messages().size();
|
||||
|
||||
// 标记与当前对话大小相同的分支
|
||||
String marker = (msgCount == currentSize) ? AnsiStyle.green(" ◀ current size") : "";
|
||||
|
||||
sb.append(" • ")
|
||||
.append(AnsiStyle.bold(name))
|
||||
.append(AnsiStyle.dim(" (" + msgCount + " messages, " + snapshot.timestamp() + ")"))
|
||||
.append(marker)
|
||||
.append("\n");
|
||||
}
|
||||
|
||||
sb.append("\n").append(AnsiStyle.dim(" 共 " + branches.size() + " 个分支。")).append("\n");
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除指定分支。
|
||||
*
|
||||
* @param branchName 分支名称
|
||||
* @return 操作结果信息
|
||||
*/
|
||||
private String deleteBranch(String branchName) {
|
||||
if (branchName.isEmpty()) {
|
||||
return AnsiStyle.red(" ✗ 请指定分支名称。") + "\n"
|
||||
+ AnsiStyle.dim(" 用法: /branch delete <name>");
|
||||
}
|
||||
|
||||
BranchSnapshot removed = branches.remove(branchName);
|
||||
if (removed == null) {
|
||||
return AnsiStyle.red(" ✗ 分支不存在: " + branchName);
|
||||
}
|
||||
|
||||
return AnsiStyle.green(" ✓ 分支已删除: ") + AnsiStyle.bold(branchName);
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示用法帮助信息。
|
||||
*
|
||||
* @return 用法说明文本
|
||||
*/
|
||||
private String showUsage() {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append(AnsiStyle.bold("\n 🌿 Branch — 对话分支管理\n\n"));
|
||||
sb.append(" ").append(AnsiStyle.cyan("/branch save <name>")).append(" 保存当前对话为分支\n");
|
||||
sb.append(" ").append(AnsiStyle.cyan("/branch load <name>")).append(" 恢复到指定分支\n");
|
||||
sb.append(" ").append(AnsiStyle.cyan("/branch list")).append(" 列出所有分支\n");
|
||||
sb.append(" ").append(AnsiStyle.cyan("/branch delete <name>")).append(" 删除指定分支\n");
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* 分支快照记录 —— 保存消息列表和创建时间。
|
||||
*
|
||||
* @param messages 消息快照
|
||||
* @param timestamp 创建时间戳
|
||||
*/
|
||||
private record BranchSnapshot(List<Message> messages, String timestamp) {}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
package com.claudecode.command.impl;
|
||||
|
||||
import com.claudecode.command.CommandContext;
|
||||
import com.claudecode.command.SlashCommand;
|
||||
import com.claudecode.console.AnsiStyle;
|
||||
import com.claudecode.core.HookManager;
|
||||
import com.claudecode.core.HookManager.HookRegistration;
|
||||
import com.claudecode.core.HookManager.HookType;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* /hooks 命令 —— 显示所有已注册的 Hook 信息。
|
||||
* <p>
|
||||
* 按 Hook 类型分组展示,包含每个 Hook 的名称和优先级。
|
||||
* 如果没有注册任何 Hook,则显示提示信息。
|
||||
*/
|
||||
public class HooksCommand implements SlashCommand {
|
||||
|
||||
@Override
|
||||
public String name() {
|
||||
return "hooks";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String description() {
|
||||
return "Show all registered hooks";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String execute(String args, CommandContext context) {
|
||||
if (context.agentLoop() == null || context.agentLoop().getHookManager() == null) {
|
||||
return AnsiStyle.yellow(" ⚠ Hook 管理器不可用。");
|
||||
}
|
||||
|
||||
HookManager hookManager = context.agentLoop().getHookManager();
|
||||
List<HookRegistration> allHooks = hookManager.getHooks();
|
||||
|
||||
// 无已注册的 Hook
|
||||
if (allHooks.isEmpty()) {
|
||||
return AnsiStyle.dim(" No hooks registered.");
|
||||
}
|
||||
|
||||
// 按类型分组
|
||||
Map<HookType, List<HookRegistration>> grouped = allHooks.stream()
|
||||
.collect(Collectors.groupingBy(HookRegistration::type));
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append(AnsiStyle.bold("\n 📎 Registered Hooks\n"));
|
||||
|
||||
// 遍历所有 HookType,保持固定顺序
|
||||
for (HookType type : HookType.values()) {
|
||||
List<HookRegistration> hooks = grouped.get(type);
|
||||
sb.append("\n");
|
||||
sb.append(" ").append(AnsiStyle.cyan(formatTypeName(type))).append("\n");
|
||||
|
||||
if (hooks == null || hooks.isEmpty()) {
|
||||
sb.append(" ").append(AnsiStyle.dim("(none)")).append("\n");
|
||||
} else {
|
||||
// 按优先级排序后展示
|
||||
hooks.stream()
|
||||
.sorted((a, b) -> Integer.compare(a.priority(), b.priority()))
|
||||
.forEach(hook -> {
|
||||
String priorityStr = AnsiStyle.dim("[priority=" + hook.priority() + "]");
|
||||
sb.append(" • ")
|
||||
.append(AnsiStyle.bold(hook.name()))
|
||||
.append(" ")
|
||||
.append(priorityStr)
|
||||
.append("\n");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 统计总数
|
||||
sb.append("\n ").append(AnsiStyle.dim("Total: " + allHooks.size() + " hook(s) registered.")).append("\n");
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 HookType 枚举格式化为可读名称。
|
||||
*
|
||||
* @param type Hook 类型枚举
|
||||
* @return 格式化后的类型名称
|
||||
*/
|
||||
private String formatTypeName(HookType type) {
|
||||
return switch (type) {
|
||||
case PRE_TOOL_USE -> "PRE_TOOL_USE (工具执行前)";
|
||||
case POST_TOOL_USE -> "POST_TOOL_USE (工具执行后)";
|
||||
case PRE_PROMPT -> "PRE_PROMPT (发送提示前)";
|
||||
case POST_RESPONSE -> "POST_RESPONSE (收到响应后)";
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,361 @@
|
||||
package com.claudecode.command.impl;
|
||||
|
||||
import com.claudecode.command.CommandContext;
|
||||
import com.claudecode.command.SlashCommand;
|
||||
import com.claudecode.console.AnsiStyle;
|
||||
import com.claudecode.mcp.McpClient;
|
||||
import com.claudecode.mcp.McpException;
|
||||
import com.claudecode.mcp.McpManager;
|
||||
import com.claudecode.tool.ToolRegistry;
|
||||
import com.claudecode.tool.impl.McpToolBridge;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* /mcp 命令 —— 管理 MCP(Model Context Protocol)服务器连接。
|
||||
* <p>
|
||||
* 子命令:
|
||||
* <ul>
|
||||
* <li>{@code /mcp} —— 列出所有 MCP 服务器及状态</li>
|
||||
* <li>{@code /mcp connect <name> <command> [args...]} —— 连接到 MCP 服务器</li>
|
||||
* <li>{@code /mcp disconnect <name>} —— 断开 MCP 服务器</li>
|
||||
* <li>{@code /mcp tools [server]} —— 列出 MCP 工具</li>
|
||||
* <li>{@code /mcp resources [server]} —— 列出 MCP 资源</li>
|
||||
* <li>{@code /mcp reload} —— 从配置文件重新加载</li>
|
||||
* </ul>
|
||||
*/
|
||||
public class McpCommand implements SlashCommand {
|
||||
|
||||
@Override
|
||||
public String name() {
|
||||
return "mcp";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String description() {
|
||||
return "Manage MCP server connections";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String execute(String args, CommandContext context) {
|
||||
// 从工具注册表的上下文中获取 McpManager 不可行,
|
||||
// 因此使用 CommandContext。这里通过反射或约定获取。
|
||||
// 实际实现中,McpManager 应作为 CommandContext 的扩展字段提供。
|
||||
// 暂时通过静态持有者或类似机制获取。
|
||||
McpManager manager = McpManagerHolder.getInstance();
|
||||
if (manager == null) {
|
||||
return AnsiStyle.red(" ❌ MCP 管理器未初始化");
|
||||
}
|
||||
|
||||
String trimmed = args.strip();
|
||||
if (trimmed.isEmpty()) {
|
||||
return showStatus(manager);
|
||||
}
|
||||
|
||||
String[] parts = trimmed.split("\\s+", 2);
|
||||
String subCommand = parts[0].toLowerCase();
|
||||
String subArgs = parts.length > 1 ? parts[1].strip() : "";
|
||||
|
||||
return switch (subCommand) {
|
||||
case "connect" -> handleConnect(manager, subArgs, context);
|
||||
case "disconnect" -> handleDisconnect(manager, subArgs);
|
||||
case "tools" -> handleTools(manager, subArgs);
|
||||
case "resources" -> handleResources(manager, subArgs);
|
||||
case "reload" -> handleReload(manager, context);
|
||||
case "help" -> showHelp();
|
||||
default -> AnsiStyle.red(" 未知子命令: " + subCommand) + "\n" + showHelp();
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示所有 MCP 服务器状态。
|
||||
*/
|
||||
private String showStatus(McpManager manager) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("\n");
|
||||
sb.append(AnsiStyle.bold(" 🔌 MCP 服务器状态\n"));
|
||||
sb.append(" ").append("─".repeat(50)).append("\n\n");
|
||||
|
||||
Map<String, McpClient> clients = manager.getClients();
|
||||
if (clients.isEmpty()) {
|
||||
sb.append(" 无已连接的 MCP 服务器\n\n");
|
||||
sb.append(AnsiStyle.dim(" 提示: 使用 /mcp connect <name> <command> [args] 连接服务器\n"));
|
||||
sb.append(AnsiStyle.dim(" 或在 .mcp.json 配置文件中定义服务器\n"));
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
for (Map.Entry<String, McpClient> entry : clients.entrySet()) {
|
||||
String name = entry.getKey();
|
||||
McpClient client = entry.getValue();
|
||||
|
||||
String statusIcon;
|
||||
String statusText;
|
||||
if (client.isConnected() && client.isInitialized()) {
|
||||
statusIcon = "✅";
|
||||
statusText = AnsiStyle.green("已连接");
|
||||
} else if (client.isConnected()) {
|
||||
statusIcon = "🔄";
|
||||
statusText = AnsiStyle.yellow("连接中");
|
||||
} else {
|
||||
statusIcon = "❌";
|
||||
statusText = AnsiStyle.red("已断开");
|
||||
}
|
||||
|
||||
sb.append(String.format(" %s %-18s %s%n", statusIcon, AnsiStyle.bold(name), statusText));
|
||||
|
||||
// 显示工具和资源数量
|
||||
int toolCount = client.getTools().size();
|
||||
int resCount = client.getResources().size();
|
||||
sb.append(String.format(" %s%n",
|
||||
AnsiStyle.dim(toolCount + " 工具, " + resCount + " 资源")));
|
||||
|
||||
// 显示服务器信息
|
||||
if (client.getServerInfo() != null) {
|
||||
sb.append(String.format(" %s%n",
|
||||
AnsiStyle.dim("Info: " + client.getServerInfo().toString())));
|
||||
}
|
||||
}
|
||||
|
||||
sb.append("\n");
|
||||
sb.append(AnsiStyle.dim(" 共 " + clients.size() + " 个服务器, "
|
||||
+ manager.getAllTools().size() + " 个工具\n"));
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理 /mcp connect 子命令。
|
||||
*/
|
||||
private String handleConnect(McpManager manager, String args, CommandContext context) {
|
||||
if (args.isEmpty()) {
|
||||
return AnsiStyle.red(" 用法: /mcp connect <name> <command> [args...]");
|
||||
}
|
||||
|
||||
String[] parts = args.split("\\s+");
|
||||
if (parts.length < 2) {
|
||||
return AnsiStyle.red(" 用法: /mcp connect <name> <command> [args...]");
|
||||
}
|
||||
|
||||
String name = parts[0];
|
||||
String command = parts[1];
|
||||
List<String> cmdArgs = parts.length > 2
|
||||
? List.of(Arrays.copyOfRange(parts, 2, parts.length))
|
||||
: List.of();
|
||||
|
||||
try {
|
||||
McpClient client = manager.connect(name, command, cmdArgs, null);
|
||||
|
||||
// 将 MCP 工具桥接到工具注册表
|
||||
registerBridgedTools(client, name, context);
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append(AnsiStyle.green(" ✅ 已连接 MCP 服务器: " + name)).append("\n");
|
||||
sb.append(AnsiStyle.dim(" " + client.getTools().size() + " 个工具, "
|
||||
+ client.getResources().size() + " 个资源")).append("\n");
|
||||
|
||||
// 列出发现的工具
|
||||
if (!client.getTools().isEmpty()) {
|
||||
sb.append("\n 工具:\n");
|
||||
for (McpClient.McpTool tool : client.getTools()) {
|
||||
sb.append(" • ").append(tool.name());
|
||||
if (!tool.description().isEmpty()) {
|
||||
sb.append(AnsiStyle.dim(" - " + truncate(tool.description(), 60)));
|
||||
}
|
||||
sb.append("\n");
|
||||
}
|
||||
}
|
||||
|
||||
return sb.toString();
|
||||
} catch (McpException e) {
|
||||
return AnsiStyle.red(" ❌ 连接失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理 /mcp disconnect 子命令。
|
||||
*/
|
||||
private String handleDisconnect(McpManager manager, String args) {
|
||||
if (args.isEmpty()) {
|
||||
return AnsiStyle.red(" 用法: /mcp disconnect <name>");
|
||||
}
|
||||
|
||||
String name = args.split("\\s+")[0];
|
||||
try {
|
||||
manager.disconnect(name);
|
||||
return AnsiStyle.green(" ✅ 已断开 MCP 服务器: " + name);
|
||||
} catch (McpException e) {
|
||||
return AnsiStyle.red(" ❌ 断开失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理 /mcp tools 子命令。
|
||||
*/
|
||||
private String handleTools(McpManager manager, String args) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("\n");
|
||||
sb.append(AnsiStyle.bold(" 🛠️ MCP 工具列表\n"));
|
||||
sb.append(" ").append("─".repeat(50)).append("\n\n");
|
||||
|
||||
String serverFilter = args.isEmpty() ? null : args.split("\\s+")[0];
|
||||
|
||||
List<McpClient.McpTool> tools;
|
||||
if (serverFilter != null) {
|
||||
tools = manager.getServerTools(serverFilter);
|
||||
if (tools.isEmpty()) {
|
||||
return sb + " 服务器 '" + serverFilter + "' 无工具或不存在\n";
|
||||
}
|
||||
sb.append(AnsiStyle.dim(" 服务器: " + serverFilter)).append("\n\n");
|
||||
} else {
|
||||
tools = manager.getAllTools();
|
||||
if (tools.isEmpty()) {
|
||||
return sb + " 无可用的 MCP 工具\n";
|
||||
}
|
||||
}
|
||||
|
||||
for (McpClient.McpTool tool : tools) {
|
||||
sb.append(" • ").append(AnsiStyle.bold(tool.name())).append("\n");
|
||||
if (!tool.description().isEmpty()) {
|
||||
sb.append(" ").append(AnsiStyle.dim(tool.description())).append("\n");
|
||||
}
|
||||
if (tool.inputSchema() != null) {
|
||||
sb.append(" ").append(AnsiStyle.dim("Schema: " +
|
||||
truncate(tool.inputSchema().toString(), 80))).append("\n");
|
||||
}
|
||||
sb.append("\n");
|
||||
}
|
||||
|
||||
sb.append(AnsiStyle.dim(" 共 " + tools.size() + " 个工具")).append("\n");
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理 /mcp resources 子命令。
|
||||
*/
|
||||
private String handleResources(McpManager manager, String args) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("\n");
|
||||
sb.append(AnsiStyle.bold(" 📦 MCP 资源列表\n"));
|
||||
sb.append(" ").append("─".repeat(50)).append("\n\n");
|
||||
|
||||
String serverFilter = args.isEmpty() ? null : args.split("\\s+")[0];
|
||||
|
||||
List<McpClient.McpResource> resourceList;
|
||||
if (serverFilter != null) {
|
||||
resourceList = manager.getServerResources(serverFilter);
|
||||
if (resourceList.isEmpty()) {
|
||||
return sb + " 服务器 '" + serverFilter + "' 无资源或不存在\n";
|
||||
}
|
||||
sb.append(AnsiStyle.dim(" 服务器: " + serverFilter)).append("\n\n");
|
||||
} else {
|
||||
resourceList = manager.getAllResources();
|
||||
if (resourceList.isEmpty()) {
|
||||
return sb + " 无可用的 MCP 资源\n";
|
||||
}
|
||||
}
|
||||
|
||||
for (McpClient.McpResource resource : resourceList) {
|
||||
sb.append(" • ").append(AnsiStyle.bold(resource.name())).append("\n");
|
||||
sb.append(" URI: ").append(AnsiStyle.cyan(resource.uri())).append("\n");
|
||||
if (!resource.description().isEmpty()) {
|
||||
sb.append(" ").append(AnsiStyle.dim(resource.description())).append("\n");
|
||||
}
|
||||
sb.append(" MIME: ").append(AnsiStyle.dim(resource.mimeType())).append("\n\n");
|
||||
}
|
||||
|
||||
sb.append(AnsiStyle.dim(" 共 " + resourceList.size() + " 个资源")).append("\n");
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理 /mcp reload 子命令。
|
||||
*/
|
||||
private String handleReload(McpManager manager, CommandContext context) {
|
||||
try {
|
||||
manager.reload();
|
||||
|
||||
// 重新桥接所有工具
|
||||
for (Map.Entry<String, McpClient> entry : manager.getClients().entrySet()) {
|
||||
registerBridgedTools(entry.getValue(), entry.getKey(), context);
|
||||
}
|
||||
|
||||
return AnsiStyle.green(" ✅ MCP 配置已重新加载: "
|
||||
+ manager.getClients().size() + " 个服务器, "
|
||||
+ manager.getAllTools().size() + " 个工具");
|
||||
} catch (Exception e) {
|
||||
return AnsiStyle.red(" ❌ 重载失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 MCP 工具桥接注册到工具注册表。
|
||||
*/
|
||||
private void registerBridgedTools(McpClient client, String serverName, CommandContext context) {
|
||||
if (context.toolRegistry() == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
ToolRegistry registry = context.toolRegistry();
|
||||
List<McpToolBridge> bridges = McpToolBridge.createBridges(serverName, client.getTools());
|
||||
for (McpToolBridge bridge : bridges) {
|
||||
registry.register(bridge);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示帮助信息。
|
||||
*/
|
||||
private String showHelp() {
|
||||
return """
|
||||
|
||||
\033[1m🔌 MCP 命令帮助\033[0m
|
||||
──────────────────────────────────────
|
||||
|
||||
/mcp 列出所有 MCP 服务器状态
|
||||
/mcp connect <name> <cmd> [args] 连接到 MCP 服务器
|
||||
/mcp disconnect <name> 断开 MCP 服务器
|
||||
/mcp tools [server] 列出 MCP 工具
|
||||
/mcp resources [server] 列出 MCP 资源
|
||||
/mcp reload 从配置文件重新加载
|
||||
/mcp help 显示此帮助信息
|
||||
|
||||
配置文件:
|
||||
项目级: .mcp.json
|
||||
全局: ~/.claude-code-java/mcp.json
|
||||
""";
|
||||
}
|
||||
|
||||
/**
|
||||
* 截断字符串。
|
||||
*/
|
||||
private static String truncate(String s, int maxLen) {
|
||||
if (s == null) return "";
|
||||
return s.length() <= maxLen ? s : s.substring(0, maxLen - 3) + "...";
|
||||
}
|
||||
|
||||
// ========== McpManager 持有者(简单单例,供命令和其他组件访问) ==========
|
||||
|
||||
/**
|
||||
* MCP 管理器全局持有者 —— 用于在命令和工具间共享 McpManager 实例。
|
||||
* <p>
|
||||
* 在应用启动时通过 {@link #setInstance(McpManager)} 注入。
|
||||
* 这是一种简单的服务定位器模式,后续可迁移到 Spring DI。
|
||||
*/
|
||||
public static final class McpManagerHolder {
|
||||
|
||||
private static volatile McpManager instance;
|
||||
|
||||
private McpManagerHolder() {
|
||||
}
|
||||
|
||||
/** 设置全局 MCP 管理器实例(应用启动时调用) */
|
||||
public static void setInstance(McpManager manager) {
|
||||
instance = manager;
|
||||
}
|
||||
|
||||
/** 获取全局 MCP 管理器实例 */
|
||||
public static McpManager getInstance() {
|
||||
return instance;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,255 @@
|
||||
package com.claudecode.command.impl;
|
||||
|
||||
import com.claudecode.command.CommandContext;
|
||||
import com.claudecode.command.SlashCommand;
|
||||
import com.claudecode.console.AnsiStyle;
|
||||
import com.claudecode.plugin.Plugin;
|
||||
import com.claudecode.plugin.PluginManager;
|
||||
import com.claudecode.plugin.PluginManager.PluginInfo;
|
||||
import com.claudecode.tool.Tool;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* /plugin 命令 —— 管理已加载的插件。
|
||||
* <p>
|
||||
* 子命令:
|
||||
* <ul>
|
||||
* <li>{@code /plugin} —— 列出所有已加载插件</li>
|
||||
* <li>{@code /plugin load <path>} —— 从 JAR 路径加载插件</li>
|
||||
* <li>{@code /plugin unload <id>} —— 卸载指定插件</li>
|
||||
* <li>{@code /plugin reload} —— 重载所有插件</li>
|
||||
* <li>{@code /plugin info <id>} —— 显示插件详细信息</li>
|
||||
* </ul>
|
||||
* <p>
|
||||
* 通过 {@link com.claudecode.tool.ToolContext} 中 key 为
|
||||
* {@code "PLUGIN_MANAGER"} 的共享状态获取 {@link PluginManager} 实例。
|
||||
*/
|
||||
public class PluginCommand implements SlashCommand {
|
||||
|
||||
@Override
|
||||
public String name() {
|
||||
return "plugin";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String description() {
|
||||
return "Manage loaded plugins";
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> aliases() {
|
||||
return List.of("plugins");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String execute(String args, CommandContext context) {
|
||||
PluginManager manager = getPluginManager(context);
|
||||
if (manager == null) {
|
||||
return AnsiStyle.red(" ✗ 插件系统未初始化");
|
||||
}
|
||||
|
||||
String trimmed = (args == null) ? "" : args.trim();
|
||||
|
||||
// 无参数:列出所有插件
|
||||
if (trimmed.isEmpty()) {
|
||||
return listPlugins(manager);
|
||||
}
|
||||
|
||||
// 解析子命令
|
||||
String[] parts = trimmed.split("\\s+", 2);
|
||||
String subCommand = parts[0].toLowerCase();
|
||||
String subArgs = (parts.length > 1) ? parts[1].trim() : "";
|
||||
|
||||
return switch (subCommand) {
|
||||
case "load" -> loadPlugin(manager, subArgs);
|
||||
case "unload" -> unloadPlugin(manager, subArgs);
|
||||
case "reload" -> reloadPlugins(manager);
|
||||
case "info" -> pluginInfo(manager, subArgs);
|
||||
default -> AnsiStyle.yellow(" 未知子命令: " + subCommand) + "\n"
|
||||
+ usageHelp();
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 列出所有已加载的插件。
|
||||
*/
|
||||
private String listPlugins(PluginManager manager) {
|
||||
List<PluginInfo> plugins = manager.getPlugins();
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("\n");
|
||||
sb.append(AnsiStyle.bold(" 🔌 Loaded Plugins")).append("\n");
|
||||
sb.append(" ").append("─".repeat(50)).append("\n\n");
|
||||
|
||||
if (plugins.isEmpty()) {
|
||||
sb.append(AnsiStyle.dim(" No plugins loaded.")).append("\n");
|
||||
sb.append(AnsiStyle.dim(" Place JAR files in ~/.claude-code-java/plugins/ to load them.")).append("\n");
|
||||
} else {
|
||||
for (PluginInfo info : plugins) {
|
||||
Plugin p = info.plugin();
|
||||
String scopeBadge = scopeColor(info.scope());
|
||||
sb.append(String.format(" %s %s %s%n",
|
||||
AnsiStyle.bold(p.name()),
|
||||
AnsiStyle.dim("v" + p.version()),
|
||||
scopeBadge));
|
||||
sb.append(String.format(" ID: %s | %s%n",
|
||||
AnsiStyle.cyan(p.id()),
|
||||
p.description()));
|
||||
sb.append(String.format(" 工具: %d | 命令: %d%n",
|
||||
p.getTools().size(),
|
||||
p.getCommands().size()));
|
||||
sb.append("\n");
|
||||
}
|
||||
sb.append(AnsiStyle.dim(String.format(" 共 %d 个插件", plugins.size()))).append("\n");
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 JAR 路径加载插件。
|
||||
*/
|
||||
private String loadPlugin(PluginManager manager, String pathStr) {
|
||||
if (pathStr.isEmpty()) {
|
||||
return AnsiStyle.yellow(" 用法: /plugin load <jar-path>");
|
||||
}
|
||||
Path jarPath = Path.of(pathStr);
|
||||
boolean success = manager.loadPlugin(jarPath);
|
||||
if (success) {
|
||||
return AnsiStyle.green(" ✓ 插件加载成功: " + jarPath.getFileName());
|
||||
} else {
|
||||
return AnsiStyle.red(" ✗ 插件加载失败: " + jarPath.getFileName())
|
||||
+ "\n" + AnsiStyle.dim(" 请检查 JAR 是否包含有效的 Plugin-Class 属性");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 卸载指定 ID 的插件。
|
||||
*/
|
||||
private String unloadPlugin(PluginManager manager, String pluginId) {
|
||||
if (pluginId.isEmpty()) {
|
||||
return AnsiStyle.yellow(" 用法: /plugin unload <plugin-id>");
|
||||
}
|
||||
boolean success = manager.unload(pluginId);
|
||||
if (success) {
|
||||
return AnsiStyle.green(" ✓ 插件已卸载: " + pluginId);
|
||||
} else {
|
||||
return AnsiStyle.red(" ✗ 未找到插件: " + pluginId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 重载所有插件(先全部卸载,再重新扫描加载)。
|
||||
*/
|
||||
private String reloadPlugins(PluginManager manager) {
|
||||
int beforeCount = manager.getPlugins().size();
|
||||
manager.shutdown();
|
||||
manager.loadAll();
|
||||
int afterCount = manager.getPlugins().size();
|
||||
return AnsiStyle.green(
|
||||
String.format(" ✓ 插件已重载(之前: %d,现在: %d)", beforeCount, afterCount));
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示指定插件的详细信息。
|
||||
*/
|
||||
private String pluginInfo(PluginManager manager, String pluginId) {
|
||||
if (pluginId.isEmpty()) {
|
||||
return AnsiStyle.yellow(" 用法: /plugin info <plugin-id>");
|
||||
}
|
||||
|
||||
PluginInfo info = manager.findPlugin(pluginId);
|
||||
if (info == null) {
|
||||
return AnsiStyle.red(" ✗ 未找到插件: " + pluginId);
|
||||
}
|
||||
|
||||
Plugin p = info.plugin();
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("\n");
|
||||
sb.append(AnsiStyle.bold(" 🔌 Plugin Details")).append("\n");
|
||||
sb.append(" ").append("─".repeat(40)).append("\n\n");
|
||||
|
||||
sb.append(" ").append(AnsiStyle.bold("Name: ")).append(p.name()).append("\n");
|
||||
sb.append(" ").append(AnsiStyle.bold("ID: ")).append(AnsiStyle.cyan(p.id())).append("\n");
|
||||
sb.append(" ").append(AnsiStyle.bold("Version: ")).append(p.version()).append("\n");
|
||||
sb.append(" ").append(AnsiStyle.bold("Description: ")).append(p.description()).append("\n");
|
||||
sb.append(" ").append(AnsiStyle.bold("Scope: ")).append(scopeColor(info.scope())).append("\n");
|
||||
sb.append(" ").append(AnsiStyle.bold("JAR: "))
|
||||
.append(AnsiStyle.dim(info.jarPath() != null ? info.jarPath().toString() : "built-in"))
|
||||
.append("\n");
|
||||
|
||||
// 工具列表
|
||||
List<Tool> tools = p.getTools();
|
||||
sb.append("\n ").append(AnsiStyle.bold("Tools (" + tools.size() + "):")).append("\n");
|
||||
if (tools.isEmpty()) {
|
||||
sb.append(AnsiStyle.dim(" (none)")).append("\n");
|
||||
} else {
|
||||
for (Tool tool : tools) {
|
||||
sb.append(" • ").append(AnsiStyle.cyan(tool.name()))
|
||||
.append(" - ").append(tool.description()).append("\n");
|
||||
}
|
||||
}
|
||||
|
||||
// 命令列表
|
||||
List<SlashCommand> commands = p.getCommands();
|
||||
sb.append("\n ").append(AnsiStyle.bold("Commands (" + commands.size() + "):")).append("\n");
|
||||
if (commands.isEmpty()) {
|
||||
sb.append(AnsiStyle.dim(" (none)")).append("\n");
|
||||
} else {
|
||||
for (SlashCommand cmd : commands) {
|
||||
sb.append(" • ").append(AnsiStyle.green("/" + cmd.name()))
|
||||
.append(" - ").append(cmd.description()).append("\n");
|
||||
}
|
||||
}
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 CommandContext 获取 PluginManager 实例。
|
||||
* <p>
|
||||
* 通过 AgentLoop → ToolContext → 共享状态(key: "PLUGIN_MANAGER")获取。
|
||||
*
|
||||
* @param context 命令执行上下文
|
||||
* @return PluginManager 实例,未找到时返回 null
|
||||
*/
|
||||
private PluginManager getPluginManager(CommandContext context) {
|
||||
if (context.agentLoop() == null) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
Object manager = context.agentLoop().getToolContext().get("PLUGIN_MANAGER");
|
||||
if (manager instanceof PluginManager pm) {
|
||||
return pm;
|
||||
}
|
||||
} catch (Exception ignored) {
|
||||
// ToolContext 中可能未注册 PLUGIN_MANAGER
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 为作用域标签着色。
|
||||
*/
|
||||
private String scopeColor(String scope) {
|
||||
return switch (scope) {
|
||||
case "global" -> AnsiStyle.blue("[global]");
|
||||
case "project" -> AnsiStyle.green("[project]");
|
||||
case "dynamic" -> AnsiStyle.magenta("[dynamic]");
|
||||
default -> AnsiStyle.dim("[" + scope + "]");
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用帮助文本。
|
||||
*/
|
||||
private String usageHelp() {
|
||||
return AnsiStyle.dim("""
|
||||
用法:
|
||||
/plugin 列出所有插件
|
||||
/plugin load <path> 加载 JAR 插件
|
||||
/plugin unload <id> 卸载插件
|
||||
/plugin reload 重载所有插件
|
||||
/plugin info <id> 查看插件详情""");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,171 @@
|
||||
package com.claudecode.command.impl;
|
||||
|
||||
import com.claudecode.command.CommandContext;
|
||||
import com.claudecode.command.SlashCommand;
|
||||
import com.claudecode.console.AnsiStyle;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.InputStreamReader;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* /review 命令 —— 通过 AI 进行代码审查。
|
||||
* <p>
|
||||
* 支持三种模式:
|
||||
* <ul>
|
||||
* <li>无参数:审查当前未暂存的变更({@code git diff})</li>
|
||||
* <li>{@code --staged}:审查已暂存的变更({@code git diff --staged})</li>
|
||||
* <li>指定文件路径:审查特定文件的变更</li>
|
||||
* </ul>
|
||||
* <p>
|
||||
* 获取 diff 内容后,发送给 AI 模型进行代码审查。
|
||||
*/
|
||||
public class ReviewCommand implements SlashCommand {
|
||||
|
||||
@Override
|
||||
public String name() {
|
||||
return "review";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String description() {
|
||||
return "Review code changes using AI";
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> aliases() {
|
||||
return List.of("rev");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String execute(String args, CommandContext context) {
|
||||
if (context.agentLoop() == null) {
|
||||
return AnsiStyle.red(" ✗ AgentLoop 不可用,无法执行代码审查。");
|
||||
}
|
||||
|
||||
String trimmedArgs = args != null ? args.trim() : "";
|
||||
|
||||
try {
|
||||
// 构建 git diff 命令
|
||||
List<String> command = buildGitDiffCommand(trimmedArgs);
|
||||
String diffOutput = executeGitCommand(command);
|
||||
|
||||
// 检查 diff 是否为空
|
||||
if (diffOutput.isBlank()) {
|
||||
return AnsiStyle.yellow(" ⚠ 没有检测到代码变更。") + "\n"
|
||||
+ AnsiStyle.dim(" 提示: 使用 --staged 审查已暂存的变更,或指定文件路径。");
|
||||
}
|
||||
|
||||
// 构建审查提示
|
||||
String reviewPrompt = buildReviewPrompt(trimmedArgs, diffOutput);
|
||||
|
||||
// 输出审查进行中的提示
|
||||
context.out().println(AnsiStyle.cyan(" 🔍 正在审查代码变更..."));
|
||||
context.out().println(AnsiStyle.dim(" diff 大小: " + diffOutput.lines().count() + " 行"));
|
||||
context.out().println();
|
||||
|
||||
// 发送给 AI 进行审查
|
||||
String result = context.agentLoop().run(reviewPrompt);
|
||||
return result;
|
||||
|
||||
} catch (Exception e) {
|
||||
return AnsiStyle.red(" ✗ 代码审查失败: " + e.getMessage()) + "\n"
|
||||
+ AnsiStyle.dim(" 请确保当前目录是一个 Git 仓库。");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据参数构建 git diff 命令。
|
||||
*
|
||||
* @param args 用户输入的参数
|
||||
* @return git diff 命令列表
|
||||
*/
|
||||
private List<String> buildGitDiffCommand(String args) {
|
||||
List<String> command = new ArrayList<>();
|
||||
command.add("git");
|
||||
command.add("diff");
|
||||
|
||||
if (args.contains("--staged")) {
|
||||
// 审查已暂存的变更
|
||||
command.add("--staged");
|
||||
} else if (!args.isEmpty() && !args.startsWith("-")) {
|
||||
// 审查指定文件
|
||||
command.add("--");
|
||||
command.add(args);
|
||||
}
|
||||
// 默认:审查未暂存的变更(无额外参数)
|
||||
|
||||
return command;
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行 git 命令并返回输出。
|
||||
*
|
||||
* @param command 命令列表
|
||||
* @return 命令的标准输出
|
||||
* @throws Exception 执行失败时抛出异常
|
||||
*/
|
||||
private String executeGitCommand(List<String> command) throws Exception {
|
||||
ProcessBuilder pb = new ProcessBuilder(command);
|
||||
pb.redirectErrorStream(false);
|
||||
|
||||
Process process = pb.start();
|
||||
|
||||
String output;
|
||||
try (BufferedReader reader = new BufferedReader(
|
||||
new InputStreamReader(process.getInputStream(), StandardCharsets.UTF_8))) {
|
||||
output = reader.lines().collect(Collectors.joining("\n"));
|
||||
}
|
||||
|
||||
// 读取错误输出
|
||||
String errorOutput;
|
||||
try (BufferedReader errorReader = new BufferedReader(
|
||||
new InputStreamReader(process.getErrorStream(), StandardCharsets.UTF_8))) {
|
||||
errorOutput = errorReader.lines().collect(Collectors.joining("\n"));
|
||||
}
|
||||
|
||||
int exitCode = process.waitFor();
|
||||
if (exitCode != 0) {
|
||||
throw new RuntimeException("git diff 执行失败 (exit=" + exitCode + "): " + errorOutput);
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建代码审查提示词。
|
||||
*
|
||||
* @param args 用户输入的参数
|
||||
* @param diffOutput git diff 的输出内容
|
||||
* @return 完整的审查提示词
|
||||
*/
|
||||
private String buildReviewPrompt(String args, String diffOutput) {
|
||||
StringBuilder prompt = new StringBuilder();
|
||||
prompt.append("Please review these code changes:\n\n");
|
||||
|
||||
// 描述审查范围
|
||||
if (args.contains("--staged")) {
|
||||
prompt.append("(Staged changes)\n\n");
|
||||
} else if (!args.isEmpty() && !args.startsWith("-")) {
|
||||
prompt.append("(Changes in file: ").append(args).append(")\n\n");
|
||||
} else {
|
||||
prompt.append("(Unstaged changes)\n\n");
|
||||
}
|
||||
|
||||
prompt.append("```diff\n");
|
||||
prompt.append(diffOutput);
|
||||
prompt.append("\n```\n\n");
|
||||
prompt.append("Please provide a thorough code review covering:\n");
|
||||
prompt.append("1. **Correctness** — Are there any bugs or logic errors?\n");
|
||||
prompt.append("2. **Code Quality** — Is the code clean, readable, and well-structured?\n");
|
||||
prompt.append("3. **Performance** — Are there any performance concerns?\n");
|
||||
prompt.append("4. **Security** — Are there any security vulnerabilities?\n");
|
||||
prompt.append("5. **Best Practices** — Does the code follow established patterns and conventions?\n");
|
||||
prompt.append("6. **Suggestions** — What improvements would you recommend?\n");
|
||||
|
||||
return prompt.toString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
package com.claudecode.command.impl;
|
||||
|
||||
import com.claudecode.command.CommandContext;
|
||||
import com.claudecode.command.SlashCommand;
|
||||
import com.claudecode.console.AnsiStyle;
|
||||
import org.springframework.ai.chat.messages.Message;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* /rewind 命令 —— 回滚对话到之前的某个位置。
|
||||
* <p>
|
||||
* 按消息对(用户消息 + 助手消息)为单位进行回滚。
|
||||
* <ul>
|
||||
* <li>{@code /rewind} —— 移除最后 1 个消息对</li>
|
||||
* <li>{@code /rewind <n>} —— 移除最后 n 个消息对</li>
|
||||
* </ul>
|
||||
* <p>
|
||||
* 使用 {@code agentLoop.getMessageHistory()} 获取当前消息历史,
|
||||
* 然后通过 {@code agentLoop.replaceHistory()} 用截断后的列表替换。
|
||||
*/
|
||||
public class RewindCommand implements SlashCommand {
|
||||
|
||||
/** 每个消息对包含的消息数(用户消息 + 助手消息) */
|
||||
private static final int MESSAGES_PER_PAIR = 2;
|
||||
|
||||
@Override
|
||||
public String name() {
|
||||
return "rewind";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String description() {
|
||||
return "Roll back conversation to a previous point";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String execute(String args, CommandContext context) {
|
||||
if (context.agentLoop() == null) {
|
||||
return AnsiStyle.red(" ✗ AgentLoop 不可用。");
|
||||
}
|
||||
|
||||
// 解析要回滚的消息对数量
|
||||
int pairsToRemove = parseRewindCount(args);
|
||||
if (pairsToRemove < 0) {
|
||||
return AnsiStyle.red(" ✗ 无效的回滚数量。请输入一个正整数。") + "\n"
|
||||
+ AnsiStyle.dim(" 用法: /rewind [n] (n 为要移除的消息对数,默认为 1)");
|
||||
}
|
||||
|
||||
List<Message> currentHistory = context.agentLoop().getMessageHistory();
|
||||
int currentSize = currentHistory.size();
|
||||
|
||||
if (currentSize == 0) {
|
||||
return AnsiStyle.yellow(" ⚠ 对话历史为空,无法回滚。");
|
||||
}
|
||||
|
||||
// 计算需要移除的消息数量
|
||||
int messagesToRemove = pairsToRemove * MESSAGES_PER_PAIR;
|
||||
|
||||
// 如果要移除的消息数超过总消息数,则清除所有消息
|
||||
if (messagesToRemove >= currentSize) {
|
||||
context.agentLoop().replaceHistory(new ArrayList<>());
|
||||
return AnsiStyle.green(" ✓ 已清除全部 " + currentSize + " 条消息。") + "\n"
|
||||
+ AnsiStyle.dim(" (请求移除 " + pairsToRemove + " 对,实际清除全部消息)");
|
||||
}
|
||||
|
||||
// 截断消息历史
|
||||
int newSize = currentSize - messagesToRemove;
|
||||
List<Message> truncatedHistory = new ArrayList<>(currentHistory.subList(0, newSize));
|
||||
context.agentLoop().replaceHistory(truncatedHistory);
|
||||
|
||||
// 构建结果输出
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append(AnsiStyle.green(" ✓ 已回滚 " + pairsToRemove + " 个消息对")).append("\n");
|
||||
sb.append(AnsiStyle.dim(" 移除: " + messagesToRemove + " 条消息")).append("\n");
|
||||
sb.append(AnsiStyle.dim(" 剩余: " + newSize + " 条消息")).append("\n");
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析回滚数量参数。
|
||||
*
|
||||
* @param args 命令参数
|
||||
* @return 要回滚的消息对数量,默认为 1;解析失败返回 -1
|
||||
*/
|
||||
private int parseRewindCount(String args) {
|
||||
String trimmed = args != null ? args.trim() : "";
|
||||
|
||||
if (trimmed.isEmpty()) {
|
||||
return 1; // 默认回滚 1 个消息对
|
||||
}
|
||||
|
||||
try {
|
||||
int count = Integer.parseInt(trimmed);
|
||||
return count > 0 ? count : -1;
|
||||
} catch (NumberFormatException e) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,174 @@
|
||||
package com.claudecode.command.impl;
|
||||
|
||||
import com.claudecode.command.CommandContext;
|
||||
import com.claudecode.command.SlashCommand;
|
||||
import com.claudecode.console.AnsiStyle;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.InputStreamReader;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* /security-review 命令 —— 通过 AI 进行安全审查。
|
||||
* <p>
|
||||
* 获取当前项目的最新代码变更({@code git diff HEAD}),
|
||||
* 发送给 AI 模型进行安全方面的专项审查。
|
||||
* <p>
|
||||
* 审查重点包括:
|
||||
* <ul>
|
||||
* <li>SQL 注入漏洞</li>
|
||||
* <li>跨站脚本攻击(XSS)</li>
|
||||
* <li>身份认证和授权问题</li>
|
||||
* <li>敏感信息泄露(密钥、密码等)</li>
|
||||
* <li>依赖安全问题</li>
|
||||
* </ul>
|
||||
*/
|
||||
public class SecurityReviewCommand implements SlashCommand {
|
||||
|
||||
@Override
|
||||
public String name() {
|
||||
return "security-review";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String description() {
|
||||
return "Review code changes for security vulnerabilities";
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> aliases() {
|
||||
return List.of("sec");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String execute(String args, CommandContext context) {
|
||||
if (context.agentLoop() == null) {
|
||||
return AnsiStyle.red(" ✗ AgentLoop 不可用,无法执行安全审查。");
|
||||
}
|
||||
|
||||
try {
|
||||
// 获取最近的代码变更
|
||||
String diffOutput = executeGitDiff();
|
||||
|
||||
if (diffOutput.isBlank()) {
|
||||
return AnsiStyle.yellow(" ⚠ 没有检测到代码变更。") + "\n"
|
||||
+ AnsiStyle.dim(" git diff HEAD 未返回任何内容。请确认是否有提交记录。");
|
||||
}
|
||||
|
||||
// 输出审查进行中的提示
|
||||
context.out().println(AnsiStyle.magenta(" 🔒 正在进行安全审查..."));
|
||||
context.out().println(AnsiStyle.dim(" diff 大小: " + diffOutput.lines().count() + " 行"));
|
||||
context.out().println();
|
||||
|
||||
// 构建安全审查提示词
|
||||
String securityPrompt = buildSecurityPrompt(diffOutput);
|
||||
|
||||
// 发送给 AI 进行安全审查
|
||||
String result = context.agentLoop().run(securityPrompt);
|
||||
return result;
|
||||
|
||||
} catch (Exception e) {
|
||||
return AnsiStyle.red(" ✗ 安全审查失败: " + e.getMessage()) + "\n"
|
||||
+ AnsiStyle.dim(" 请确保当前目录是一个 Git 仓库且有提交历史。");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行 {@code git diff HEAD} 获取最近的代码变更。
|
||||
*
|
||||
* @return diff 输出内容
|
||||
* @throws Exception 命令执行失败时抛出异常
|
||||
*/
|
||||
private String executeGitDiff() throws Exception {
|
||||
ProcessBuilder pb = new ProcessBuilder("git", "diff", "HEAD");
|
||||
pb.redirectErrorStream(false);
|
||||
|
||||
Process process = pb.start();
|
||||
|
||||
String output;
|
||||
try (BufferedReader reader = new BufferedReader(
|
||||
new InputStreamReader(process.getInputStream(), StandardCharsets.UTF_8))) {
|
||||
output = reader.lines().collect(Collectors.joining("\n"));
|
||||
}
|
||||
|
||||
// 读取错误输出
|
||||
String errorOutput;
|
||||
try (BufferedReader errorReader = new BufferedReader(
|
||||
new InputStreamReader(process.getErrorStream(), StandardCharsets.UTF_8))) {
|
||||
errorOutput = errorReader.lines().collect(Collectors.joining("\n"));
|
||||
}
|
||||
|
||||
int exitCode = process.waitFor();
|
||||
if (exitCode != 0) {
|
||||
throw new RuntimeException("git diff HEAD 执行失败 (exit=" + exitCode + "): " + errorOutput);
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建安全审查提示词。
|
||||
* <p>
|
||||
* 要求 AI 从多个安全维度对代码变更进行全面审查,
|
||||
* 并按严重程度分类报告发现的问题。
|
||||
*
|
||||
* @param diffOutput git diff 的输出内容
|
||||
* @return 完整的安全审查提示词
|
||||
*/
|
||||
private String buildSecurityPrompt(String diffOutput) {
|
||||
return """
|
||||
Please perform a comprehensive security review of the following code changes.
|
||||
|
||||
```diff
|
||||
%s
|
||||
```
|
||||
|
||||
Analyze the code changes for the following security concerns:
|
||||
|
||||
## 1. SQL Injection
|
||||
- Are there any raw SQL queries with string concatenation?
|
||||
- Are parameterized queries/prepared statements used properly?
|
||||
- Is user input sanitized before database operations?
|
||||
|
||||
## 2. Cross-Site Scripting (XSS)
|
||||
- Is user input properly escaped before rendering in HTML?
|
||||
- Are there any unsafe innerHTML or DOM manipulation patterns?
|
||||
- Is output encoding applied correctly?
|
||||
|
||||
## 3. Authentication & Authorization
|
||||
- Are authentication checks properly implemented?
|
||||
- Are authorization boundaries enforced correctly?
|
||||
- Are session tokens handled securely?
|
||||
- Are passwords hashed with strong algorithms (bcrypt, Argon2)?
|
||||
|
||||
## 4. Secrets & Sensitive Data
|
||||
- Are any API keys, passwords, or tokens hardcoded?
|
||||
- Are sensitive data properly encrypted at rest and in transit?
|
||||
- Are credentials stored in environment variables or secure vaults?
|
||||
- Are there any logging statements that might expose sensitive information?
|
||||
|
||||
## 5. Dependency & Configuration Security
|
||||
- Are there any known vulnerable dependencies?
|
||||
- Are security headers properly configured?
|
||||
- Are CORS policies appropriately restrictive?
|
||||
- Is TLS/SSL properly configured?
|
||||
|
||||
## 6. Other Security Concerns
|
||||
- Path traversal vulnerabilities
|
||||
- Command injection risks
|
||||
- Insecure deserialization
|
||||
- Race conditions
|
||||
- Improper error handling that leaks information
|
||||
|
||||
For each issue found, please provide:
|
||||
- **Severity**: Critical / High / Medium / Low
|
||||
- **Location**: File and line reference
|
||||
- **Description**: What the vulnerability is
|
||||
- **Recommendation**: How to fix it
|
||||
|
||||
If no security issues are found, explicitly state that the changes look secure.
|
||||
""".formatted(diffOutput);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
package com.claudecode.command.impl;
|
||||
|
||||
import com.claudecode.command.CommandContext;
|
||||
import com.claudecode.command.SlashCommand;
|
||||
import com.claudecode.console.AnsiStyle;
|
||||
import com.claudecode.core.TokenTracker;
|
||||
|
||||
import java.lang.management.ManagementFactory;
|
||||
import java.lang.management.RuntimeMXBean;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* /stats 命令 —— 显示当前会话的使用统计信息。
|
||||
* <p>
|
||||
* 展示内容包括:
|
||||
* <ul>
|
||||
* <li>近似对话轮数(消息历史大小 / 2)</li>
|
||||
* <li>API 调用总次数</li>
|
||||
* <li>Token 使用量(输入/输出/总计)</li>
|
||||
* <li>估算费用(美元)</li>
|
||||
* <li>每次调用平均 Token 数</li>
|
||||
* <li>当前使用的模型名称</li>
|
||||
* <li>JVM 运行时长</li>
|
||||
* </ul>
|
||||
*/
|
||||
public class StatsCommand implements SlashCommand {
|
||||
|
||||
@Override
|
||||
public String name() {
|
||||
return "stats";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String description() {
|
||||
return "Show usage statistics";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String execute(String args, CommandContext context) {
|
||||
if (context.agentLoop() == null) {
|
||||
return AnsiStyle.red(" ✗ AgentLoop 不可用。");
|
||||
}
|
||||
|
||||
TokenTracker tracker = context.agentLoop().getTokenTracker();
|
||||
if (tracker == null) {
|
||||
return AnsiStyle.yellow(" ⚠ Token 追踪器不可用。");
|
||||
}
|
||||
|
||||
// 收集统计数据
|
||||
int messageCount = context.agentLoop().getMessageHistory().size();
|
||||
int conversationRounds = messageCount / 2; // 近似对话轮数
|
||||
long apiCalls = tracker.getApiCallCount();
|
||||
long inputTokens = tracker.getInputTokens();
|
||||
long outputTokens = tracker.getOutputTokens();
|
||||
long totalTokens = tracker.getTotalTokens();
|
||||
double estimatedCost = tracker.estimateCost();
|
||||
String modelName = tracker.getModelName();
|
||||
|
||||
// 计算每次调用平均 Token 数
|
||||
long avgTokensPerCall = apiCalls > 0 ? totalTokens / apiCalls : 0;
|
||||
|
||||
// 计算 JVM 运行时长
|
||||
String uptime = formatUptime();
|
||||
|
||||
// 构建输出
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append(AnsiStyle.bold("\n 📊 Session Statistics\n"));
|
||||
sb.append("\n");
|
||||
|
||||
// 模型信息
|
||||
sb.append(formatRow("Model", AnsiStyle.cyan(modelName)));
|
||||
sb.append(formatRow("Uptime", uptime));
|
||||
sb.append("\n");
|
||||
|
||||
// 对话统计
|
||||
sb.append(AnsiStyle.bold(" ── Conversation ──\n"));
|
||||
sb.append(formatRow("Messages", String.valueOf(messageCount)));
|
||||
sb.append(formatRow("Conversations (approx)", String.valueOf(conversationRounds)));
|
||||
sb.append(formatRow("API Calls", String.valueOf(apiCalls)));
|
||||
sb.append("\n");
|
||||
|
||||
// Token 统计
|
||||
sb.append(AnsiStyle.bold(" ── Token Usage ──\n"));
|
||||
sb.append(formatRow("Input Tokens", TokenTracker.formatTokens(inputTokens)));
|
||||
sb.append(formatRow("Output Tokens", TokenTracker.formatTokens(outputTokens)));
|
||||
sb.append(formatRow("Total Tokens", AnsiStyle.bold(TokenTracker.formatTokens(totalTokens))));
|
||||
sb.append(formatRow("Avg Tokens/Call", TokenTracker.formatTokens(avgTokensPerCall)));
|
||||
sb.append("\n");
|
||||
|
||||
// 费用统计
|
||||
sb.append(AnsiStyle.bold(" ── Cost ──\n"));
|
||||
sb.append(formatRow("Estimated Cost", formatCost(estimatedCost)));
|
||||
sb.append("\n");
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化表格行(左对齐标签 + 右对齐值)。
|
||||
*
|
||||
* @param label 标签名
|
||||
* @param value 值
|
||||
* @return 格式化的行字符串
|
||||
*/
|
||||
private String formatRow(String label, String value) {
|
||||
return String.format(" %-24s %s%n", AnsiStyle.dim(label), value);
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化费用值(保留 4 位小数)。
|
||||
* <p>
|
||||
* 根据费用高低使用不同颜色:
|
||||
* <ul>
|
||||
* <li>$0 以下(即免费):绿色</li>
|
||||
* <li>$0.01 ~ $1.00:黄色</li>
|
||||
* <li>$1.00 以上:红色</li>
|
||||
* </ul>
|
||||
*
|
||||
* @param cost 费用(美元)
|
||||
* @return 带颜色的费用字符串
|
||||
*/
|
||||
private String formatCost(double cost) {
|
||||
String formatted = String.format("$%.4f", cost);
|
||||
if (cost < 0.01) {
|
||||
return AnsiStyle.green(formatted);
|
||||
} else if (cost < 1.0) {
|
||||
return AnsiStyle.yellow(formatted);
|
||||
} else {
|
||||
return AnsiStyle.red(formatted);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化 JVM 运行时长。
|
||||
* <p>
|
||||
* 使用 {@link ManagementFactory#getRuntimeMXBean()} 获取 JVM 启动时间,
|
||||
* 并计算至今的运行时长,格式为 "Xh Ym Zs"。
|
||||
*
|
||||
* @return 格式化的运行时长字符串
|
||||
*/
|
||||
private String formatUptime() {
|
||||
RuntimeMXBean runtimeMXBean = ManagementFactory.getRuntimeMXBean();
|
||||
long uptimeMillis = runtimeMXBean.getUptime();
|
||||
|
||||
long hours = TimeUnit.MILLISECONDS.toHours(uptimeMillis);
|
||||
long minutes = TimeUnit.MILLISECONDS.toMinutes(uptimeMillis) % 60;
|
||||
long seconds = TimeUnit.MILLISECONDS.toSeconds(uptimeMillis) % 60;
|
||||
|
||||
if (hours > 0) {
|
||||
return String.format("%dh %dm %ds", hours, minutes, seconds);
|
||||
} else if (minutes > 0) {
|
||||
return String.format("%dm %ds", minutes, seconds);
|
||||
} else {
|
||||
return String.format("%ds", seconds);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,196 @@
|
||||
package com.claudecode.command.impl;
|
||||
|
||||
import com.claudecode.command.CommandContext;
|
||||
import com.claudecode.command.SlashCommand;
|
||||
import com.claudecode.console.AnsiStyle;
|
||||
import org.springframework.ai.chat.messages.Message;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* /tag 命令 —— 为当前对话位置打标签。
|
||||
* <p>
|
||||
* 标签记录当前消息历史的大小,可用于后续快速回溯到该位置。
|
||||
* 标签数据存储在静态 Map 中,在 JVM 生命周期内持久化。
|
||||
* <p>
|
||||
* 支持的子命令:
|
||||
* <ul>
|
||||
* <li>{@code /tag <name>} —— 为当前位置打标签</li>
|
||||
* <li>{@code /tag list} —— 列出所有标签</li>
|
||||
* <li>{@code /tag goto <name>} —— 回溯到指定标签位置</li>
|
||||
* </ul>
|
||||
*/
|
||||
public class TagCommand implements SlashCommand {
|
||||
|
||||
/** 静态标签存储:标签名称 -> 标签信息 */
|
||||
private static final Map<String, TagInfo> tags = new LinkedHashMap<>();
|
||||
|
||||
@Override
|
||||
public String name() {
|
||||
return "tag";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String description() {
|
||||
return "Tag current conversation point with a label";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String execute(String args, CommandContext context) {
|
||||
if (context.agentLoop() == null) {
|
||||
return AnsiStyle.red(" ✗ AgentLoop 不可用。");
|
||||
}
|
||||
|
||||
String trimmedArgs = args != null ? args.trim() : "";
|
||||
|
||||
if (trimmedArgs.isEmpty()) {
|
||||
return showUsage();
|
||||
}
|
||||
|
||||
// 解析子命令
|
||||
String[] parts = trimmedArgs.split("\\s+", 2);
|
||||
String firstArg = parts[0].toLowerCase();
|
||||
|
||||
return switch (firstArg) {
|
||||
case "list" -> listTags(context);
|
||||
case "goto" -> {
|
||||
String tagName = parts.length > 1 ? parts[1].trim() : "";
|
||||
yield gotoTag(tagName, context);
|
||||
}
|
||||
default -> {
|
||||
// 第一个参数不是子命令,视为标签名称
|
||||
yield createTag(trimmedArgs, context);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建标签,记录当前消息历史的大小。
|
||||
*
|
||||
* @param tagName 标签名称
|
||||
* @param context 命令上下文
|
||||
* @return 操作结果信息
|
||||
*/
|
||||
private String createTag(String tagName, CommandContext context) {
|
||||
if (tagName.isEmpty()) {
|
||||
return AnsiStyle.red(" ✗ 请指定标签名称。");
|
||||
}
|
||||
|
||||
// 标签名称不能与子命令冲突(虽然 list/goto 已在 switch 中处理)
|
||||
int position = context.agentLoop().getMessageHistory().size();
|
||||
String timestamp = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
|
||||
|
||||
boolean isOverwrite = tags.containsKey(tagName);
|
||||
tags.put(tagName, new TagInfo(position, timestamp));
|
||||
|
||||
String action = isOverwrite ? "已更新" : "已创建";
|
||||
return AnsiStyle.green(" ✓ 标签" + action + ": ") + AnsiStyle.bold(tagName) + "\n"
|
||||
+ AnsiStyle.dim(" 位置: 第 " + position + " 条消息 时间: " + timestamp);
|
||||
}
|
||||
|
||||
/**
|
||||
* 列出所有已保存的标签。
|
||||
*
|
||||
* @param context 命令上下文
|
||||
* @return 标签列表信息
|
||||
*/
|
||||
private String listTags(CommandContext context) {
|
||||
if (tags.isEmpty()) {
|
||||
return AnsiStyle.dim(" 没有保存的标签。") + "\n"
|
||||
+ AnsiStyle.dim(" 使用 /tag <name> 为当前位置打标签。");
|
||||
}
|
||||
|
||||
int currentPosition = context.agentLoop().getMessageHistory().size();
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append(AnsiStyle.bold("\n 🏷️ Conversation Tags\n\n"));
|
||||
|
||||
for (Map.Entry<String, TagInfo> entry : tags.entrySet()) {
|
||||
String name = entry.getKey();
|
||||
TagInfo info = entry.getValue();
|
||||
|
||||
// 高亮当前位置匹配的标签
|
||||
String marker = (info.position() == currentPosition)
|
||||
? AnsiStyle.green(" ◀ current")
|
||||
: "";
|
||||
|
||||
sb.append(" • ")
|
||||
.append(AnsiStyle.bold(name))
|
||||
.append(AnsiStyle.dim(" (position=" + info.position() + ", " + info.timestamp() + ")"))
|
||||
.append(marker)
|
||||
.append("\n");
|
||||
}
|
||||
|
||||
sb.append("\n")
|
||||
.append(AnsiStyle.dim(" 当前位置: " + currentPosition + " 条消息 | 共 " + tags.size() + " 个标签"))
|
||||
.append("\n");
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* 回溯到指定标签对应的对话位置。
|
||||
* <p>
|
||||
* 将消息历史截断到标签记录的位置。如果当前消息数少于标签位置,
|
||||
* 则不做截断(标签位置可能超出当前对话长度)。
|
||||
*
|
||||
* @param tagName 标签名称
|
||||
* @param context 命令上下文
|
||||
* @return 操作结果信息
|
||||
*/
|
||||
private String gotoTag(String tagName, CommandContext context) {
|
||||
if (tagName.isEmpty()) {
|
||||
return AnsiStyle.red(" ✗ 请指定标签名称。") + "\n"
|
||||
+ AnsiStyle.dim(" 用法: /tag goto <name>");
|
||||
}
|
||||
|
||||
TagInfo info = tags.get(tagName);
|
||||
if (info == null) {
|
||||
return AnsiStyle.red(" ✗ 标签不存在: " + tagName) + "\n"
|
||||
+ AnsiStyle.dim(" 使用 /tag list 查看所有可用标签。");
|
||||
}
|
||||
|
||||
List<Message> currentHistory = context.agentLoop().getMessageHistory();
|
||||
int currentSize = currentHistory.size();
|
||||
int targetPosition = info.position();
|
||||
|
||||
if (targetPosition >= currentSize) {
|
||||
return AnsiStyle.yellow(" ⚠ 标签位置 (" + targetPosition + ") 不小于当前消息数 ("
|
||||
+ currentSize + "),无需回溯。");
|
||||
}
|
||||
|
||||
// 截断到标签位置
|
||||
List<Message> truncated = new ArrayList<>(currentHistory.subList(0, targetPosition));
|
||||
context.agentLoop().replaceHistory(truncated);
|
||||
|
||||
int removedCount = currentSize - targetPosition;
|
||||
return AnsiStyle.green(" ✓ 已回溯到标签: ") + AnsiStyle.bold(tagName) + "\n"
|
||||
+ AnsiStyle.dim(" 移除了 " + removedCount + " 条消息,当前消息数: " + targetPosition);
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示用法帮助信息。
|
||||
*
|
||||
* @return 用法说明文本
|
||||
*/
|
||||
private String showUsage() {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append(AnsiStyle.bold("\n 🏷️ Tag — 对话位置标签\n\n"));
|
||||
sb.append(" ").append(AnsiStyle.cyan("/tag <name>")).append(" 为当前位置打标签\n");
|
||||
sb.append(" ").append(AnsiStyle.cyan("/tag list")).append(" 列出所有标签\n");
|
||||
sb.append(" ").append(AnsiStyle.cyan("/tag goto <name>")).append(" 回溯到指定标签位置\n");
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* 标签信息记录 —— 保存标签的消息位置和创建时间。
|
||||
*
|
||||
* @param position 消息历史中的位置(消息数量)
|
||||
* @param timestamp 创建时间戳
|
||||
*/
|
||||
private record TagInfo(int position, String timestamp) {}
|
||||
}
|
||||
Reference in New Issue
Block a user