feat: 多层上下文压缩 + 多级权限管理系统

权限管理:
- PermissionTypes: 权限行为/模式/规则/决策/选择类型系统
- PermissionSettings: 用户级/项目级/会话级权限持久化(settings.json)
- PermissionRuleEngine: 多层规则引擎(deny→allow→readOnly→mode→dangerous→ASK)
- DangerousPatterns: 危险shell命令检测(rm -rf, eval, exec等)
- ReplSession: Y/A/N/D四选项权限确认UI
- ConfigCommand: permission-mode/list/reset子命令
- AgentLoop: 集成规则引擎,PermissionChoice回调

上下文压缩:
- TokenTracker: 上下文窗口监控(93%自动压缩/82%警告/98%阻塞)
- MicroCompact: 微压缩,裁剪旧tool_result(无API调用)
- SessionMemoryCompact: Session Memory压缩,AI摘要旧消息保留近期段
- FullCompact: 全量压缩+PTL重试(按API Round逐步丢弃)
- AutoCompactManager: 压缩编排器(micro→session→full),含熔断机制
- CompactCommand: 改造为委托FullCompact
- StatusLine: token使用百分比+颜色告警(绿→黄→红→闪烁)
- AppConfig: 注册PermissionSettings/RuleEngine/AutoCompactManager Bean

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
pull/1/head
liuzh 1 month ago
parent c989d3b451
commit 730551cb3f
  1. 92
      src/main/java/com/claudecode/command/impl/CompactCommand.java
  2. 102
      src/main/java/com/claudecode/command/impl/ConfigCommand.java
  3. 32
      src/main/java/com/claudecode/config/AppConfig.java
  4. 17
      src/main/java/com/claudecode/console/StatusLine.java
  5. 83
      src/main/java/com/claudecode/core/AgentLoop.java
  6. 100
      src/main/java/com/claudecode/core/TokenTracker.java
  7. 178
      src/main/java/com/claudecode/core/compact/AutoCompactManager.java
  8. 46
      src/main/java/com/claudecode/core/compact/CompactionResult.java
  9. 175
      src/main/java/com/claudecode/core/compact/FullCompact.java
  10. 91
      src/main/java/com/claudecode/core/compact/MicroCompact.java
  11. 297
      src/main/java/com/claudecode/core/compact/SessionMemoryCompact.java
  12. 105
      src/main/java/com/claudecode/permission/DangerousPatterns.java
  13. 173
      src/main/java/com/claudecode/permission/PermissionRuleEngine.java
  14. 200
      src/main/java/com/claudecode/permission/PermissionSettings.java
  15. 110
      src/main/java/com/claudecode/permission/PermissionTypes.java
  16. 61
      src/main/java/com/claudecode/repl/ReplSession.java

@ -4,10 +4,10 @@ import com.claudecode.command.CommandContext;
import com.claudecode.command.SlashCommand;
import com.claudecode.console.AnsiStyle;
import com.claudecode.core.TokenTracker;
import com.claudecode.core.compact.AutoCompactManager;
import com.claudecode.core.compact.FullCompact;
import org.springframework.ai.chat.messages.*;
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.chat.prompt.Prompt;
import java.util.ArrayList;
import java.util.List;
@ -16,22 +16,10 @@ import java.util.List;
* /compact 命令 AI 生成摘要来压缩上下文
* <p>
* 对应 claude-code/src/commands/compact.ts
* 将详细的对话历史替换为 AI 生成的摘要大幅减少 token 消耗
* 保留系统提示词和最近一轮对话
* 委托给 FullCompact 执行实际压缩逻辑
*/
public class CompactCommand implements SlashCommand {
private static final String COMPACT_PROMPT = """
Please compress the following conversation history into a concise summary. Requirements:
1. Preserve all key decisions, code changes, and technical details
2. Keep file paths, function names, and specific information
3. Preserve user preferences and requirements
4. Omit repeated discussions and irrelevant details
5. Output within 500 words
Conversation history:
""";
@Override
public String name() {
return "compact";
@ -58,78 +46,38 @@ public class CompactCommand implements SlashCommand {
TokenTracker tracker = context.agentLoop().getTokenTracker();
long tokensBefore = tracker.getInputTokens() + tracker.getOutputTokens();
// 尝试用 AI 生成摘要
String summary = generateSummary(context, history);
// 构建压缩后的历史:系统提示 + 摘要作为系统消息 + 保留最后一轮对话
List<Message> compacted = new ArrayList<>();
compacted.add(history.getFirst()); // 原始系统提示词
if (summary != null && !summary.isBlank()) {
compacted.add(new SystemMessage("[Conversation Summary] " + summary));
// 优先使用 AutoCompactManager 中的 FullCompact
FullCompact fullCompact;
AutoCompactManager acm = context.agentLoop().getAutoCompactManager();
if (acm != null) {
fullCompact = acm.getFullCompact();
} else {
fullCompact = new FullCompact(context.agentLoop().getChatModel());
}
// 保留最后一轮用户消息和助手回复(如果有)
for (int i = Math.max(1, before - 2); i < before; i++) {
compacted.add(history.get(i));
}
// 执行全量压缩
List<Message> compacted = fullCompact.compact(new ArrayList<>(history));
if (compacted != null) {
context.agentLoop().replaceHistory(compacted);
int after = compacted.size();
// 重置熔断器(手动压缩成功说明 AI 摘要功能正常)
if (acm != null) {
acm.resetCircuitBreaker();
}
StringBuilder sb = new StringBuilder();
sb.append(AnsiStyle.green(" ✅ Context compacted")).append("\n");
sb.append(" Messages: ").append(before).append(" → ").append(after).append("\n");
if (tokensBefore > 0) {
sb.append(" Tokens before compaction: ").append(TokenTracker.formatTokens(tokensBefore)).append("\n");
}
if (summary != null) {
sb.append(AnsiStyle.dim(" 📝 AI summary generated and injected into context"));
} else {
sb.append(AnsiStyle.dim(" ⚠ AI summary generation failed, keeping recent conversation only"));
}
return sb.toString();
}
/** 调用 AI 生成对话摘要 */
private String generateSummary(CommandContext context, List<Message> history) {
try {
ChatModel chatModel = context.agentLoop().getChatModel();
// 构建摘要请求的消息列史
StringBuilder dialogText = new StringBuilder();
for (Message msg : history) {
switch (msg) {
case UserMessage um -> dialogText.append("[User] ").append(um.getText()).append("\n");
case AssistantMessage am -> {
if (am.getText() != null && !am.getText().isBlank()) {
// 截断过长的助手回复
String text = am.getText();
if (text.length() > 500) text = text.substring(0, 500) + "...";
dialogText.append("[Assistant] ").append(text).append("\n");
}
if (am.hasToolCalls()) {
for (var tc : am.getToolCalls()) {
dialogText.append("[Tool Call] ").append(tc.name()).append("\n");
}
}
}
default -> {} // 跳过系统消息和工具响应
}
}
if (dialogText.isEmpty()) return null;
Prompt summaryPrompt = new Prompt(List.of(
new UserMessage(COMPACT_PROMPT + dialogText)
));
ChatResponse response = chatModel.call(summaryPrompt);
return response.getResult().getOutput().getText();
} catch (Exception e) {
// 摘要生成失败不影响压缩操作
return null;
}
return AnsiStyle.yellow(" ⚠ Compaction failed — AI summary generation failed") + "\n"
+ AnsiStyle.dim(" The conversation history was not modified.");
}
}

@ -3,6 +3,9 @@ package com.claudecode.command.impl;
import com.claudecode.command.CommandContext;
import com.claudecode.command.SlashCommand;
import com.claudecode.console.AnsiStyle;
import com.claudecode.permission.PermissionRuleEngine;
import com.claudecode.permission.PermissionSettings;
import com.claudecode.permission.PermissionTypes.PermissionMode;
import java.util.List;
import java.util.Map;
@ -10,8 +13,7 @@ import java.util.Map;
/**
* /config 命令 查看和设置应用配置
* <p>
* 支持查看当前配置设置单个配置项
* 配置变更仅在当前会话内生效
* 支持查看当前配置设置单个配置项以及权限管理子命令
*/
public class ConfigCommand implements SlashCommand {
@ -21,9 +23,24 @@ public class ConfigCommand implements SlashCommand {
"max-tokens", "Maximum output tokens per response",
"temperature", "Response randomness (0.0-1.0)",
"verbose", "Enable verbose logging (true/false)",
"auto-compact", "Auto compact when context is large (true/false)"
"auto-compact", "Auto compact when context is large (true/false)",
"permission-mode", "Permission mode: default/accept-edits/bypass/dont-ask",
"permission-list", "List all saved permission rules",
"permission-reset", "Clear all permission rules"
);
private PermissionSettings permissionSettings;
public ConfigCommand() {}
public ConfigCommand(PermissionSettings permissionSettings) {
this.permissionSettings = permissionSettings;
}
public void setPermissionSettings(PermissionSettings settings) {
this.permissionSettings = settings;
}
@Override
public String name() {
return "config";
@ -90,17 +107,6 @@ public class ConfigCommand implements SlashCommand {
return sb.toString();
}
private String showConfig(String key, CommandContext context) {
if (!CONFIG_KEYS.containsKey(key)) {
return AnsiStyle.yellow(" ⚠ Unknown config key: " + key) + "\n"
+ AnsiStyle.dim(" Available: " + String.join(", ", CONFIG_KEYS.keySet()));
}
String desc = CONFIG_KEYS.get(key);
return " " + AnsiStyle.bold(key) + ": " + AnsiStyle.dim(desc) + "\n"
+ AnsiStyle.dim(" Set with: /config " + key + " <value>");
}
private String setConfig(String key, String value, CommandContext context) {
return switch (key) {
case "model" -> {
@ -112,6 +118,9 @@ public class ConfigCommand implements SlashCommand {
boolean verbose = Boolean.parseBoolean(value);
yield AnsiStyle.green(" ✅ Verbose mode: " + (verbose ? "ON" : "OFF"));
}
case "permission-mode" -> setPermissionMode(value);
case "permission-list" -> listPermissionRules();
case "permission-reset" -> resetPermissionRules();
default -> {
if (!CONFIG_KEYS.containsKey(key)) {
yield AnsiStyle.yellow(" ⚠ Unknown config key: " + key);
@ -121,4 +130,69 @@ public class ConfigCommand implements SlashCommand {
}
};
}
private String showConfig(String key, CommandContext context) {
// 无参数的权限子命令
if (key.equals("permission-list")) return listPermissionRules();
if (key.equals("permission-reset")) return resetPermissionRules();
if (!CONFIG_KEYS.containsKey(key)) {
return AnsiStyle.yellow(" ⚠ Unknown config key: " + key) + "\n"
+ AnsiStyle.dim(" Available: " + String.join(", ", CONFIG_KEYS.keySet()));
}
String desc = CONFIG_KEYS.get(key);
return " " + AnsiStyle.bold(key) + ": " + AnsiStyle.dim(desc) + "\n"
+ AnsiStyle.dim(" Set with: /config " + key + " <value>");
}
// ── 权限管理子命令 ──
private String setPermissionMode(String value) {
if (permissionSettings == null) {
return AnsiStyle.yellow(" ⚠ Permission settings not initialized");
}
try {
PermissionMode mode = switch (value.toLowerCase()) {
case "default" -> PermissionMode.DEFAULT;
case "accept-edits", "acceptedits" -> PermissionMode.ACCEPT_EDITS;
case "bypass" -> PermissionMode.BYPASS;
case "dont-ask", "dontask" -> PermissionMode.DONT_ASK;
default -> throw new IllegalArgumentException(value);
};
permissionSettings.setCurrentMode(mode);
return AnsiStyle.green(" ✅ Permission mode set to: " + mode);
} catch (IllegalArgumentException e) {
return AnsiStyle.yellow(" ⚠ Unknown mode: " + value) + "\n"
+ AnsiStyle.dim(" Available: default, accept-edits, bypass, dont-ask");
}
}
private String listPermissionRules() {
if (permissionSettings == null) {
return AnsiStyle.yellow(" ⚠ Permission settings not initialized");
}
var rules = permissionSettings.listRules();
if (rules.isEmpty()) {
return AnsiStyle.dim(" No saved permission rules") + "\n"
+ AnsiStyle.dim(" Mode: " + permissionSettings.getCurrentMode());
}
StringBuilder sb = new StringBuilder();
sb.append("\n");
sb.append(AnsiStyle.bold(" 🔒 Permission Rules")).append("\n");
sb.append(" ").append("─".repeat(40)).append("\n");
sb.append(" ").append(AnsiStyle.bold("Mode: ")).append(permissionSettings.getCurrentMode()).append("\n\n");
for (String rule : rules) {
sb.append(" ").append(rule).append("\n");
}
return sb.toString();
}
private String resetPermissionRules() {
if (permissionSettings == null) {
return AnsiStyle.yellow(" ⚠ Permission settings not initialized");
}
permissionSettings.clearAll();
return AnsiStyle.green(" ✅ All permission rules cleared");
}
}

@ -9,7 +9,10 @@ import com.claudecode.context.SystemPromptBuilder;
import com.claudecode.core.AgentLoop;
import com.claudecode.core.TaskManager;
import com.claudecode.core.TokenTracker;
import com.claudecode.core.compact.AutoCompactManager;
import com.claudecode.mcp.McpManager;
import com.claudecode.permission.PermissionRuleEngine;
import com.claudecode.permission.PermissionSettings;
import com.claudecode.plugin.OutputStylePlugin;
import com.claudecode.plugin.PluginManager;
import com.claudecode.repl.ReplSession;
@ -115,7 +118,8 @@ public class AppConfig {
}
@Bean
public CommandRegistry commandRegistry(PluginManager pluginManager) {
public CommandRegistry commandRegistry(PluginManager pluginManager, PermissionSettings permissionSettings) {
ConfigCommand configCommand = new ConfigCommand(permissionSettings);
CommandRegistry registry = new CommandRegistry();
registry.registerAll(
// 基础命令
@ -127,7 +131,7 @@ public class AppConfig {
new StatusCommand(),
new ContextCommand(),
new InitCommand(),
new ConfigCommand(),
configCommand,
new HistoryCommand(),
// P0 命令
new DiffCommand(),
@ -192,6 +196,23 @@ public class AppConfig {
return new ProviderInfo(provider, baseUrl, model);
}
@Bean
public PermissionSettings permissionSettings() {
PermissionSettings settings = new PermissionSettings();
settings.load();
return settings;
}
@Bean
public PermissionRuleEngine permissionRuleEngine(PermissionSettings permissionSettings) {
return new PermissionRuleEngine(permissionSettings);
}
@Bean
public AutoCompactManager autoCompactManager(ChatModel activeChatModel, TokenTracker tokenTracker) {
return new AutoCompactManager(activeChatModel, tokenTracker);
}
@Bean
public TokenTracker tokenTracker(ProviderInfo info) {
TokenTracker tracker = new TokenTracker();
@ -223,9 +244,14 @@ public class AppConfig {
@Bean
public AgentLoop agentLoop(ChatModel activeChatModel, ToolRegistry toolRegistry,
ToolContext toolContext, String systemPrompt, TokenTracker tokenTracker,
PluginManager pluginManager) {
PluginManager pluginManager, PermissionRuleEngine permissionRuleEngine,
AutoCompactManager autoCompactManager) {
AgentLoop mainLoop = new AgentLoop(activeChatModel, toolRegistry, toolContext, systemPrompt, tokenTracker);
// 注入权限引擎和自动压缩管理器
mainLoop.setPermissionEngine(permissionRuleEngine);
mainLoop.setAutoCompactManager(autoCompactManager);
// 注册子 Agent 工厂
toolContext.set(AgentTool.AGENT_FACTORY_KEY,
(java.util.function.Function<String, String>) prompt -> {

@ -84,10 +84,25 @@ public class StatusLine {
// 模型名
sb.append(" ").append(modelName);
// Token 用量
// Token 用量 + 上下文窗口占比
sb.append(" │ ↑").append(TokenTracker.formatTokens(inputTokens));
sb.append(" ↓").append(TokenTracker.formatTokens(outputTokens));
// 上下文窗口使用百分比(带颜色)
double usagePct = tokenTracker.getUsagePercentage();
if (usagePct > 0) {
String pctStr = String.format(" %.0f%%", usagePct * 100);
var warningState = tokenTracker.getTokenWarningState();
sb.append(AnsiStyle.RESET); // 先重置再着色
sb.append(switch (warningState) {
case NORMAL -> AnsiStyle.DIM + AnsiStyle.GREEN + pctStr;
case WARNING -> AnsiStyle.BOLD + AnsiStyle.YELLOW + pctStr;
case ERROR -> AnsiStyle.BOLD + AnsiStyle.RED + pctStr;
case BLOCKING -> AnsiStyle.BOLD + AnsiStyle.RED + "⚠" + pctStr;
});
sb.append(AnsiStyle.RESET).append(AnsiStyle.DIM);
}
// 费用
if (cost > 0) {
sb.append(String.format(" $%.4f", cost));

@ -1,5 +1,9 @@
package com.claudecode.core;
import com.claudecode.core.compact.AutoCompactManager;
import com.claudecode.permission.PermissionRuleEngine;
import com.claudecode.permission.PermissionTypes.PermissionChoice;
import com.claudecode.permission.PermissionTypes.PermissionDecision;
import com.claudecode.tool.ToolCallbackAdapter;
import com.claudecode.tool.ToolContext;
import com.claudecode.tool.ToolRegistry;
@ -52,6 +56,12 @@ public class AgentLoop {
private final TokenTracker tokenTracker;
private final HookManager hookManager;
/** 权限规则引擎(可选,为 null 时使用传统回调方式) */
private PermissionRuleEngine permissionEngine;
/** 自动压缩管理器(可选) */
private AutoCompactManager autoCompactManager;
/** 消息历史 —— 自行管理,不依赖 Spring AI ChatMemory */
private final List<Message> messageHistory = new ArrayList<>();
@ -64,8 +74,8 @@ public class AgentLoop {
/** 流式输出开始回调:通知 UI 停止 spinner */
private Runnable onStreamStart;
/** 权限确认回调:危险操作前请求用户确认(返回 true 表示允许) */
private Function<PermissionRequest, Boolean> onPermissionRequest;
/** 权限确认回调:危险操作前请求用户确认(返回 PermissionChoice) */
private Function<PermissionRequest, PermissionChoice> onPermissionRequest;
/** Thinking 内容回调:显示 AI 的思考过程 */
private Consumer<String> onThinkingContent;
@ -98,10 +108,22 @@ public class AgentLoop {
this.onStreamStart = onStreamStart;
}
public void setOnPermissionRequest(Function<PermissionRequest, Boolean> onPermissionRequest) {
public void setOnPermissionRequest(Function<PermissionRequest, PermissionChoice> onPermissionRequest) {
this.onPermissionRequest = onPermissionRequest;
}
public void setPermissionEngine(PermissionRuleEngine engine) {
this.permissionEngine = engine;
}
public void setAutoCompactManager(AutoCompactManager manager) {
this.autoCompactManager = manager;
}
public AutoCompactManager getAutoCompactManager() {
return autoCompactManager;
}
public void setOnThinkingContent(Consumer<String> onThinkingContent) {
this.onThinkingContent = onThinkingContent;
}
@ -183,6 +205,14 @@ public class AgentLoop {
// 执行工具调用
executeToolCalls(result.assistant.getToolCalls(), callbacks);
// 自动压缩检查(在工具调用后,下次 API 调用前)
if (autoCompactManager != null) {
autoCompactManager.autoCompactIfNeeded(
() -> messageHistory,
this::replaceHistory
);
}
}
if (iteration >= MAX_ITERATIONS) {
@ -302,12 +332,34 @@ public class AgentLoop {
String result;
ToolCallbackAdapter adapter = findCallbackByName(callbacks, toolName);
if (adapter != null) {
// 权限确认:非只读工具需要用户确认
// 权限检查:优先使用规则引擎,回退到传统回调
boolean permitted = true;
if (!adapter.getTool().isReadOnly() && onPermissionRequest != null) {
if (permissionEngine != null) {
PermissionDecision decision = permissionEngine.evaluate(
toolName, parsedArgs, adapter.getTool().isReadOnly());
if (decision.isAllowed()) {
permitted = true;
} else if (decision.isDenied()) {
permitted = false;
log.info("[{}] Denied by rule: {}", toolName, decision.reason());
} else if (decision.needsAsk() && onPermissionRequest != null) {
String activity = adapter.getTool().activityDescription(parsedArgs);
PermissionRequest req = new PermissionRequest(toolName, toolArgs, activity);
req.setDecision(decision);
PermissionChoice choice = onPermissionRequest.apply(req);
permitted = (choice == PermissionChoice.ALLOW_ONCE || choice == PermissionChoice.ALWAYS_ALLOW);
// 持久化用户选择
String command = parsedArgs != null ? (String) parsedArgs.get("command") : null;
permissionEngine.applyChoice(choice, toolName, command);
} else {
permitted = false;
}
} else if (!adapter.getTool().isReadOnly() && onPermissionRequest != null) {
// 传统回调模式(向后兼容)
String activity = adapter.getTool().activityDescription(parsedArgs);
PermissionRequest req = new PermissionRequest(toolName, toolArgs, activity);
permitted = onPermissionRequest.apply(req);
PermissionChoice choice = onPermissionRequest.apply(req);
permitted = (choice == PermissionChoice.ALLOW_ONCE || choice == PermissionChoice.ALWAYS_ALLOW);
}
if (permitted) {
@ -438,7 +490,24 @@ public class AgentLoop {
}
/** 权限确认请求 */
public record PermissionRequest(String toolName, String arguments, String activityDescription) {}
public static class PermissionRequest {
private final String toolName;
private final String arguments;
private final String activityDescription;
private PermissionDecision decision;
public PermissionRequest(String toolName, String arguments, String activityDescription) {
this.toolName = toolName;
this.arguments = arguments;
this.activityDescription = activityDescription;
}
public String toolName() { return toolName; }
public String arguments() { return arguments; }
public String activityDescription() { return activityDescription; }
public PermissionDecision decision() { return decision; }
public void setDecision(PermissionDecision decision) { this.decision = decision; }
}
/** 工具事件,用于 UI 展示 */
public record ToolEvent(String toolName, Phase phase, String arguments, String result) {

@ -3,29 +3,72 @@ package com.claudecode.core;
import java.util.concurrent.atomic.AtomicLong;
/**
* Token 使用量追踪器 记录 API 调用的 token 消耗
* Token 使用量追踪器 记录 API 调用的 token 消耗并监控上下文窗口
* <p>
* ChatResponse usage 元数据中提取 token 统计信息
* 支持按会话累计和费用估算
* 支持按会话累计费用估算和上下文窗口阈值监控
*/
public class TokenTracker {
// ── 上下文窗口阈值常量 ──
/** 自动压缩触发百分比(有效窗口的 93%) */
public static final double AUTO_COMPACT_THRESHOLD_PCT = 0.93;
/** 警告阈值百分比(82%) */
public static final double WARNING_THRESHOLD_PCT = 0.82;
/** 阻塞阈值百分比(98%,必须压缩才能继续) */
public static final double BLOCKING_THRESHOLD_PCT = 0.98;
/** 自动压缩缓冲 token 数 */
public static final long AUTO_COMPACT_BUFFER_TOKENS = 13_000;
/** 手动压缩缓冲 token 数 */
public static final long MANUAL_COMPACT_BUFFER_TOKENS = 3_000;
/** 上下文窗口警告状态 */
public enum TokenWarningState {
NORMAL, // 正常(绿色)
WARNING, // 接近阈值(黄色)
ERROR, // 达到压缩阈值(红色)
BLOCKING // 必须压缩才能继续(闪烁红)
}
private final AtomicLong totalInputTokens = new AtomicLong(0);
private final AtomicLong totalOutputTokens = new AtomicLong(0);
private final AtomicLong totalCacheReadTokens = new AtomicLong(0);
private final AtomicLong totalCacheCreationTokens = new AtomicLong(0);
private final AtomicLong apiCallCount = new AtomicLong(0);
/** 最近一次 API 调用报告的 prompt token 数(近似当前上下文大小) */
private final AtomicLong lastPromptTokens = new AtomicLong(0);
/** 模型定价(每百万 token 的美元价格) */
private double inputPricePerMillion = 3.0; // Claude Sonnet 4 input
private double outputPricePerMillion = 15.0; // Claude Sonnet 4 output
private double cacheReadPricePerMillion = 0.3; // 缓存读取
private String modelName = "claude-sonnet-4-20250514";
/** 上下文窗口总大小(token) */
private long contextWindowSize;
/** 预留给输出的 token 数 */
private long reservedTokens = 20_000;
public TokenTracker() {
// 支持环境变量覆盖上下文窗口大小
String envWindow = System.getenv("CLAUDE_CODE_CONTEXT_WINDOW");
if (envWindow != null && !envWindow.isBlank()) {
try {
this.contextWindowSize = Long.parseLong(envWindow.trim());
} catch (NumberFormatException e) {
this.contextWindowSize = 200_000; // 默认 200K
}
} else {
this.contextWindowSize = 200_000;
}
}
/** 记录一次 API 调用的 token 使用 */
public void recordUsage(long inputTokens, long outputTokens) {
totalInputTokens.addAndGet(inputTokens);
totalOutputTokens.addAndGet(outputTokens);
lastPromptTokens.set(inputTokens);
apiCallCount.incrementAndGet();
}
@ -35,6 +78,7 @@ public class TokenTracker {
totalOutputTokens.addAndGet(outputTokens);
totalCacheReadTokens.addAndGet(cacheRead);
totalCacheCreationTokens.addAndGet(cacheCreation);
lastPromptTokens.set(inputTokens);
apiCallCount.incrementAndGet();
}
@ -73,12 +117,64 @@ public class TokenTracker {
return inputCost + outputCost + cacheCost;
}
// ── 上下文窗口监控 ──
/** 有效上下文窗口大小(总窗口 - 预留输出) */
public long getEffectiveWindow() {
return contextWindowSize - reservedTokens;
}
/** 最近一次 prompt 的 token 数(近似当前上下文大小) */
public long getLastPromptTokens() {
return lastPromptTokens.get();
}
/** 当前上下文使用百分比 */
public double getUsagePercentage() {
long effective = getEffectiveWindow();
if (effective <= 0) return 0;
return (double) lastPromptTokens.get() / effective;
}
/** 是否应触发自动压缩 */
public boolean shouldAutoCompact() {
return getUsagePercentage() >= AUTO_COMPACT_THRESHOLD_PCT;
}
/** 是否已达到阻塞阈值(必须压缩才能继续) */
public boolean isBlocking() {
return getUsagePercentage() >= BLOCKING_THRESHOLD_PCT;
}
/** 获取自动压缩触发的 token 阈值 */
public long getAutoCompactThreshold() {
return (long) (getEffectiveWindow() * AUTO_COMPACT_THRESHOLD_PCT);
}
/** 获取当前 token 警告状态 */
public TokenWarningState getTokenWarningState() {
double pct = getUsagePercentage();
if (pct >= BLOCKING_THRESHOLD_PCT) return TokenWarningState.BLOCKING;
if (pct >= AUTO_COMPACT_THRESHOLD_PCT) return TokenWarningState.ERROR;
if (pct >= WARNING_THRESHOLD_PCT) return TokenWarningState.WARNING;
return TokenWarningState.NORMAL;
}
public long getContextWindowSize() { return contextWindowSize; }
public void setContextWindowSize(long size) { this.contextWindowSize = size; }
public long getReservedTokens() { return reservedTokens; }
public void setReservedTokens(long reserved) { this.reservedTokens = reserved; }
/** 重置统计 */
public void reset() {
totalInputTokens.set(0);
totalOutputTokens.set(0);
totalCacheReadTokens.set(0);
totalCacheCreationTokens.set(0);
lastPromptTokens.set(0);
apiCallCount.set(0);
}

@ -0,0 +1,178 @@
package com.claudecode.core.compact;
import com.claudecode.core.TokenTracker;
import com.claudecode.core.compact.CompactionResult.CompactLayer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.ai.chat.messages.Message;
import org.springframework.ai.chat.model.ChatModel;
import java.util.List;
import java.util.function.Consumer;
import java.util.function.Supplier;
/**
* 自动压缩编排器 根据 token 使用量自动选择并执行压缩策略
* <p>
* 对应 claude-code 的自动压缩编排逻辑 AgentLoop 中每次 API 响应后调用
* 流程检查阈值 微压缩 Session Memory 压缩 全量压缩兜底
* 熔断器连续失败 {@value MAX_CONSECUTIVE_FAILURES} 次后暂停自动压缩
*/
public class AutoCompactManager {
private static final Logger log = LoggerFactory.getLogger(AutoCompactManager.class);
/** 连续失败阈值,超过后暂停自动压缩 */
private static final int MAX_CONSECUTIVE_FAILURES = 3;
private final MicroCompact microCompact;
private final SessionMemoryCompact sessionMemoryCompact;
private final FullCompact fullCompact;
private final TokenTracker tokenTracker;
/** 连续压缩失败次数 */
private int consecutiveFailures = 0;
/** 是否已触发过熔断 */
private boolean circuitBroken = false;
/** 压缩事件回调(用于通知 UI) */
private Consumer<CompactionResult> onCompactionEvent;
public AutoCompactManager(ChatModel chatModel, TokenTracker tokenTracker) {
this.tokenTracker = tokenTracker;
this.microCompact = new MicroCompact();
this.sessionMemoryCompact = new SessionMemoryCompact(chatModel);
this.fullCompact = new FullCompact(chatModel);
}
public void setOnCompactionEvent(Consumer<CompactionResult> onCompactionEvent) {
this.onCompactionEvent = onCompactionEvent;
}
/**
* 在每次 API 响应后调用根据 token 使用状态自动执行压缩
*
* @param historySupplier 获取当前消息历史的函数
* @param historyReplacer 替换消息历史的函数
* @return 如果执行了压缩返回结果否则返回 null
*/
public CompactionResult autoCompactIfNeeded(
Supplier<List<Message>> historySupplier,
Consumer<List<Message>> historyReplacer) {
// 熔断器检查
if (circuitBroken) {
return null;
}
// 检查是否需要压缩
if (!tokenTracker.shouldAutoCompact()) {
// 即使不需要自动压缩,也执行微压缩(成本极低)
List<Message> history = historySupplier.get();
if (history instanceof java.util.ArrayList<Message> mutableHistory) {
microCompact.compact(mutableHistory);
}
return null;
}
log.info("Auto-compact triggered at {}% token usage",
String.format("%.1f", tokenTracker.getUsagePercentage() * 100));
List<Message> history = historySupplier.get();
// 阶段 1:微压缩
if (history instanceof java.util.ArrayList<Message> mutableHistory) {
CompactionResult microResult = microCompact.compact(mutableHistory);
if (microResult.success()) {
notifyEvent(microResult);
// 微压缩后重新检查是否仍需深度压缩
if (!tokenTracker.shouldAutoCompact()) {
consecutiveFailures = 0;
return microResult;
}
}
}
// 阶段 2:Session Memory 压缩
try {
List<Message> compacted = sessionMemoryCompact.getCompactedHistory(history);
if (compacted != null) {
historyReplacer.accept(compacted);
CompactionResult result = CompactionResult.success(
CompactLayer.SESSION_MEMORY,
history.size(), compacted.size(),
"Auto session memory compact");
consecutiveFailures = 0;
notifyEvent(result);
log.info("Session memory compact: {} → {} messages", history.size(), compacted.size());
return result;
}
} catch (Exception e) {
log.warn("Session memory compact failed: {}", e.getMessage());
}
// 阶段 3:全量压缩(兜底)
try {
List<Message> compacted = fullCompact.compact(history);
if (compacted != null) {
historyReplacer.accept(compacted);
CompactionResult result = CompactionResult.success(
CompactLayer.FULL,
history.size(), compacted.size(),
"Auto full compact (fallback)");
consecutiveFailures = 0;
notifyEvent(result);
log.info("Full compact fallback: {} → {} messages", history.size(), compacted.size());
return result;
}
} catch (Exception e) {
log.warn("Full compact failed: {}", e.getMessage());
}
// 所有压缩方式均失败
consecutiveFailures++;
if (consecutiveFailures >= MAX_CONSECUTIVE_FAILURES) {
circuitBroken = true;
log.error("Auto-compact circuit breaker triggered after {} consecutive failures",
consecutiveFailures);
CompactionResult result = CompactionResult.failure(CompactLayer.FULL,
"Circuit breaker: auto-compact disabled after " + consecutiveFailures + " failures");
notifyEvent(result);
return result;
}
return CompactionResult.failure(CompactLayer.SESSION_MEMORY,
"All compression strategies failed");
}
/** 手动重置熔断器 */
public void resetCircuitBreaker() {
circuitBroken = false;
consecutiveFailures = 0;
log.info("Auto-compact circuit breaker reset");
}
public boolean isCircuitBroken() {
return circuitBroken;
}
public int getConsecutiveFailures() {
return consecutiveFailures;
}
/** 获取 FullCompact 实例(供 CompactCommand 委托使用) */
public FullCompact getFullCompact() {
return fullCompact;
}
private void notifyEvent(CompactionResult result) {
if (onCompactionEvent != null) {
try {
onCompactionEvent.accept(result);
} catch (Exception e) {
log.debug("Compaction event notification failed", e);
}
}
}
}

@ -0,0 +1,46 @@
package com.claudecode.core.compact;
/**
* 压缩操作的结果数据
*
* @param success 是否成功
* @param layer 执行的压缩层级
* @param messagesBefore 压缩前消息数
* @param messagesAfter 压缩后消息数
* @param summary AI 生成的摘要可能为 null
* @param reason 结果原因/描述
*/
public record CompactionResult(
boolean success,
CompactLayer layer,
int messagesBefore,
int messagesAfter,
String summary,
String reason
) {
/** 压缩层级 */
public enum CompactLayer {
/** 微压缩:裁剪旧 tool_result 内容 */
MICRO,
/** Session Memory:AI 摘要旧消息,保留近期段 */
SESSION_MEMORY,
/** 全量压缩:AI 摘要全部,PTL 重试 */
FULL,
/** 用户手动触发的全量压缩 */
MANUAL
}
public static CompactionResult success(CompactLayer layer, int before, int after, String summary) {
return new CompactionResult(true, layer, before, after, summary,
"Compacted from " + before + " to " + after + " messages");
}
public static CompactionResult noAction(CompactLayer layer, String reason) {
return new CompactionResult(false, layer, 0, 0, null, reason);
}
public static CompactionResult failure(CompactLayer layer, String reason) {
return new CompactionResult(false, layer, 0, 0, null, reason);
}
}

@ -0,0 +1,175 @@
package com.claudecode.core.compact;
import com.claudecode.core.compact.CompactionResult.CompactLayer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.ai.chat.messages.*;
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.chat.prompt.Prompt;
import java.util.ArrayList;
import java.util.List;
/**
* 全量压缩 AI 摘要全部对话历史 PTLPrompt Too Long重试
* <p>
* 对应 claude-code fullCompact SessionMemoryCompact 无法有效压缩时作为兜底
* PTL 重试策略 API Rounduserassistanttool_result 为一组逐步丢弃最旧的组
*/
public class FullCompact {
private static final Logger log = LoggerFactory.getLogger(FullCompact.class);
/** PTL 重试最大次数 */
private static final int MAX_PTL_RETRIES = 5;
/** 保留最近 N 条消息(不压缩) */
private static final int KEEP_RECENT_MESSAGES = 2;
private static final String FULL_COMPACT_PROMPT = """
Please compress the following conversation history into a thorough summary. Requirements:
1. Preserve ALL key decisions, code changes, and technical details
2. Keep file paths, function names, class names, and specific identifiers
3. Preserve user preferences, requirements, and constraints
4. Record the current state of work: what was completed, what remains, what's blocked
5. Note any errors encountered and their resolutions
6. Keep important context about the project structure and architecture
7. Output within 1000 words, using structured bullet points
Conversation history:
""";
private final ChatModel chatModel;
public FullCompact(ChatModel chatModel) {
this.chatModel = chatModel;
}
/**
* 执行全量压缩
*
* @param history 当前消息历史
* @return 压缩后的新历史如果失败返回 null
*/
public List<Message> compact(List<Message> history) {
if (history.size() <= KEEP_RECENT_MESSAGES + 2) {
return null;
}
int before = history.size();
Message systemMsg = history.getFirst();
// 按 API Round 分组
List<ApiRound> rounds = groupByRounds(history);
// PTL 重试循环:逐步丢弃最旧的 round
int dropCount = 0;
while (dropCount < rounds.size() - 1 && dropCount < MAX_PTL_RETRIES) {
List<ApiRound> remaining = rounds.subList(dropCount, rounds.size());
try {
String summary = generateFullSummary(remaining);
if (summary != null && !summary.isBlank()) {
// 构建新历史
List<Message> newHistory = new ArrayList<>();
newHistory.add(systemMsg);
newHistory.add(new SystemMessage("[Conversation Summary]\n" + summary));
// 保留最后几条消息
for (int i = Math.max(1, before - KEEP_RECENT_MESSAGES); i < before; i++) {
newHistory.add(history.get(i));
}
log.info("Full compact succeeded: {} → {} messages (dropped {} rounds)",
before, newHistory.size(), dropCount);
return newHistory;
}
} catch (Exception e) {
log.warn("Full compact attempt failed (drop={}): {}", dropCount, e.getMessage());
// PTL error — drop oldest round and retry
}
dropCount++;
}
log.error("Full compact failed after {} PTL retries", dropCount);
return null;
}
/**
* 执行全量压缩并返回 CompactionResult
*/
public CompactionResult compactWithResult(List<Message> history) {
int before = history.size();
List<Message> result = compact(history);
if (result == null) {
return CompactionResult.failure(CompactLayer.FULL, "Full compact failed");
}
return CompactionResult.success(CompactLayer.FULL, before, result.size(), null);
}
// ── 内部方法 ──
/** 按 API Round 分组:一个 round = [UserMessage] + [AssistantMessage + ToolResponseMessages...] */
private List<ApiRound> groupByRounds(List<Message> history) {
List<ApiRound> rounds = new ArrayList<>();
List<Message> currentRound = new ArrayList<>();
for (int i = 1; i < history.size(); i++) { // 跳过系统消息
Message msg = history.get(i);
if (msg instanceof UserMessage && !currentRound.isEmpty()) {
rounds.add(new ApiRound(List.copyOf(currentRound)));
currentRound.clear();
}
currentRound.add(msg);
}
if (!currentRound.isEmpty()) {
rounds.add(new ApiRound(List.copyOf(currentRound)));
}
return rounds;
}
/** 生成全量摘要 */
private String generateFullSummary(List<ApiRound> rounds) {
StringBuilder dialogText = new StringBuilder();
for (ApiRound round : rounds) {
for (Message msg : round.messages()) {
switch (msg) {
case UserMessage um -> dialogText.append("[User] ").append(um.getText()).append("\n");
case AssistantMessage am -> {
if (am.getText() != null && !am.getText().isBlank()) {
String text = am.getText();
if (text.length() > 600) text = text.substring(0, 600) + "...";
dialogText.append("[Assistant] ").append(text).append("\n");
}
if (am.hasToolCalls()) {
for (var tc : am.getToolCalls()) {
dialogText.append("[Tool Call] ").append(tc.name()).append("\n");
}
}
}
case ToolResponseMessage trm -> {
for (var resp : trm.getResponses()) {
dialogText.append("[Tool Result: ").append(resp.name()).append("]\n");
}
}
default -> {}
}
}
dialogText.append("---\n");
}
if (dialogText.isEmpty()) return null;
Prompt prompt = new Prompt(List.of(new UserMessage(FULL_COMPACT_PROMPT + dialogText)));
ChatResponse response = chatModel.call(prompt);
return response.getResult().getOutput().getText();
}
/** API Round:一个用户请求 + AI 响应 + 工具调用的完整回合 */
private record ApiRound(List<Message> messages) {}
}

@ -0,0 +1,91 @@
package com.claudecode.core.compact;
import com.claudecode.core.compact.CompactionResult.CompactLayer;
import org.springframework.ai.chat.messages.*;
import java.util.List;
/**
* 微压缩 在每次 API 调用后执行裁剪旧的 tool_result 内容
* <p>
* 对应 claude-code microCompact不需要额外 API 调用纯本地操作
* 策略保留最近 N 轮的 tool 结果更早的只保留摘要行 "[Tool result truncated]"
*/
public class MicroCompact {
/** 保留最近 N 条 ToolResponseMessage 的完整内容 */
private static final int KEEP_RECENT_TOOL_RESULTS = 6;
/** 截断阈值:超过此长度的旧 tool result 才会被截断 */
private static final int TRUNCATE_THRESHOLD = 200;
/** 截断后的占位文本 */
private static final String TRUNCATED_MARKER = "[Tool result truncated — %d chars omitted]";
/**
* 对消息历史执行微压缩
* 直接在原始列表上原地修改以提升性能
*
* @param history 消息列表直接修改
* @return 压缩结果
*/
public CompactionResult compact(List<Message> history) {
int totalToolResponses = 0;
int truncated = 0;
// 倒序扫描,找到所有 ToolResponseMessage 的位置
int recentCount = 0;
for (int i = history.size() - 1; i >= 0; i--) {
if (history.get(i) instanceof ToolResponseMessage) {
totalToolResponses++;
recentCount++;
if (recentCount > KEEP_RECENT_TOOL_RESULTS) {
// 需要截断
ToolResponseMessage trm = (ToolResponseMessage) history.get(i);
if (shouldTruncate(trm)) {
history.set(i, truncateToolResponse(trm));
truncated++;
}
}
}
}
if (truncated == 0) {
return CompactionResult.noAction(CompactLayer.MICRO, "No tool results to truncate");
}
return CompactionResult.success(CompactLayer.MICRO, totalToolResponses,
totalToolResponses - truncated, null);
}
/** 判断 ToolResponseMessage 是否需要截断 */
private boolean shouldTruncate(ToolResponseMessage trm) {
var responses = trm.getResponses();
if (responses == null || responses.isEmpty()) return false;
for (var resp : responses) {
if (resp.responseData() != null && resp.responseData().toString().length() > TRUNCATE_THRESHOLD) {
return true;
}
}
return false;
}
/** 创建截断后的 ToolResponseMessage */
private ToolResponseMessage truncateToolResponse(ToolResponseMessage original) {
var responses = original.getResponses();
if (responses == null || responses.isEmpty()) return original;
var truncatedResponses = responses.stream().map(resp -> {
String data = resp.responseData() != null ? resp.responseData().toString() : "";
if (data.length() > TRUNCATE_THRESHOLD) {
String marker = String.format(TRUNCATED_MARKER, data.length());
return new ToolResponseMessage.ToolResponse(resp.id(), resp.name(), marker);
}
return resp;
}).toList();
return ToolResponseMessage.builder()
.responses(truncatedResponses)
.build();
}
}

@ -0,0 +1,297 @@
package com.claudecode.core.compact;
import com.claudecode.core.compact.CompactionResult.CompactLayer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.ai.chat.messages.*;
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.chat.prompt.Prompt;
import java.util.ArrayList;
import java.util.List;
/**
* Session Memory 压缩 保留近期消息段 AI 摘要旧消息
* <p>
* 对应 claude-code sessionMemoryCompact这是主要的自动压缩方式
* 算法
* <ol>
* <li>找到上次压缩的边界通过检测 [Conversation Summary] 标记</li>
* <li>计算需要保留的近期消息段至少保留 MIN_KEEP_TOKENS token 估算量 + MIN_KEEP_TEXT_MSGS 条文本消息</li>
* <li>将边界之后保留段之前的消息通过 AI 生成摘要</li>
* <li> [系统提示] + [历史摘要] + [新摘要] + [保留段] 替换历史</li>
* </ol>
*/
public class SessionMemoryCompact {
private static final Logger log = LoggerFactory.getLogger(SessionMemoryCompact.class);
/** 最少保留的文本消息数(用户 + 助手) */
private static final int MIN_KEEP_TEXT_MSGS = 5;
/** 估算最少保留的 token 数 */
private static final int MIN_KEEP_TOKENS = 10_000;
/** 估算最多保留的 token 数 */
private static final int MAX_KEEP_TOKENS = 40_000;
/** 每字符估算的 token 数(粗略近似) */
private static final double CHARS_PER_TOKEN = 4.0;
private static final String SUMMARY_PROMPT = """
Summarize the following conversation segment concisely but thoroughly.
Preserve:
- All key technical decisions and their rationale
- File paths, function names, class names, and specific code identifiers
- User requirements and preferences
- Current state of work (what was done, what remains)
- Any errors encountered and their resolutions
Keep the summary under 800 words. Use bullet points for clarity.
Conversation segment to summarize:
""";
private final ChatModel chatModel;
public SessionMemoryCompact(ChatModel chatModel) {
this.chatModel = chatModel;
}
/**
* 执行 Session Memory 压缩
*
* @param history 当前消息历史不直接修改返回新列表
* @return 压缩结果如果无法压缩返回 noAction
*/
public CompactionResult compact(List<Message> history) {
if (history.size() <= MIN_KEEP_TEXT_MSGS + 2) {
return CompactionResult.noAction(CompactLayer.SESSION_MEMORY,
"Too few messages to compact");
}
int before = history.size();
// 找到系统提示词(第一条)
Message systemMsg = history.getFirst();
// 找到上一次摘要的位置(如果有的话)
int lastSummaryIndex = findLastSummaryIndex(history);
// 从摘要之后开始计算可压缩区域
int compressibleStart = lastSummaryIndex + 1;
// 从末尾向前找保留段的起始位置
int keepStart = findKeepStart(history, compressibleStart);
// 如果可压缩区域太小,不值得压缩
if (keepStart - compressibleStart < 4) {
return CompactionResult.noAction(CompactLayer.SESSION_MEMORY,
"Not enough messages to compress (only " + (keepStart - compressibleStart) + " in range)");
}
// 提取需要压缩的消息段
List<Message> toCompress = history.subList(compressibleStart, keepStart);
// 生成摘要
String summary;
try {
summary = generateSummary(toCompress);
} catch (Exception e) {
log.warn("Session memory compression failed: {}", e.getMessage());
return CompactionResult.failure(CompactLayer.SESSION_MEMORY,
"Summary generation failed: " + e.getMessage());
}
if (summary == null || summary.isBlank()) {
return CompactionResult.failure(CompactLayer.SESSION_MEMORY,
"Empty summary generated");
}
// 构建新历史
List<Message> newHistory = new ArrayList<>();
newHistory.add(systemMsg);
// 保留旧的摘要(如果有的话,合并到新摘要中)
String previousSummary = extractPreviousSummary(history, lastSummaryIndex);
if (previousSummary != null) {
summary = "=== Earlier Context ===\n" + previousSummary + "\n\n=== Recent Activity ===\n" + summary;
}
// 添加新的摘要消息
newHistory.add(new SystemMessage("[Conversation Summary]\n" + summary));
// 添加保留段
for (int i = keepStart; i < history.size(); i++) {
newHistory.add(history.get(i));
}
int after = newHistory.size();
return new CompactionResult(true, CompactLayer.SESSION_MEMORY, before, after, summary,
"Session memory compacted: " + before + " → " + after + " messages");
}
/**
* 获取压缩后的新历史调用方需要先调用 compact() 确认成功然后调用此方法获取结果
* 为避免重复逻辑此方法重新执行压缩并返回新历史
*/
public List<Message> getCompactedHistory(List<Message> history) {
if (history.size() <= MIN_KEEP_TEXT_MSGS + 2) return null;
Message systemMsg = history.getFirst();
int lastSummaryIndex = findLastSummaryIndex(history);
int compressibleStart = lastSummaryIndex + 1;
int keepStart = findKeepStart(history, compressibleStart);
if (keepStart - compressibleStart < 4) return null;
List<Message> toCompress = history.subList(compressibleStart, keepStart);
String summary;
try {
summary = generateSummary(toCompress);
} catch (Exception e) {
return null;
}
if (summary == null || summary.isBlank()) return null;
List<Message> newHistory = new ArrayList<>();
newHistory.add(systemMsg);
String previousSummary = extractPreviousSummary(history, lastSummaryIndex);
if (previousSummary != null) {
summary = "=== Earlier Context ===\n" + previousSummary + "\n\n=== Recent Activity ===\n" + summary;
}
newHistory.add(new SystemMessage("[Conversation Summary]\n" + summary));
for (int i = keepStart; i < history.size(); i++) {
newHistory.add(history.get(i));
}
return newHistory;
}
// ── 内部方法 ──
/** 找到历史中最后一个 [Conversation Summary] 系统消息的索引 */
private int findLastSummaryIndex(List<Message> history) {
for (int i = history.size() - 1; i >= 1; i--) {
if (history.get(i) instanceof SystemMessage sm
&& sm.getText() != null
&& sm.getText().startsWith("[Conversation Summary]")) {
return i;
}
}
return 0; // 没有摘要,从系统提示之后开始
}
/** 从末尾向前找保留段的起始位置 */
private int findKeepStart(List<Message> history, int minStart) {
int textMsgCount = 0;
long estimatedTokens = 0;
for (int i = history.size() - 1; i >= minStart; i--) {
Message msg = history.get(i);
// 估算 token 量
long msgTokens = estimateTokens(msg);
estimatedTokens += msgTokens;
if (msg instanceof UserMessage || msg instanceof AssistantMessage) {
textMsgCount++;
}
// 确保不会拆分 tool_use / tool_result 对
// 如果当前是 ToolResponseMessage,它的 AssistantMessage(含 tool_calls)应在前面
if (msg instanceof ToolResponseMessage && i > minStart) {
continue; // 继续往前包含对应的 AssistantMessage
}
// 满足最小保留条件,且已达到上限则停止
if (textMsgCount >= MIN_KEEP_TEXT_MSGS && estimatedTokens >= MIN_KEEP_TOKENS) {
// 检查是否达到 token 上限
if (estimatedTokens >= MAX_KEEP_TOKENS) {
return i;
}
}
}
// 如果从 minStart 开始全部都在保留范围内,返回 minStart
// 说明消息不够多,不需要压缩
return minStart;
}
/** 估算消息的 token 数 */
private long estimateTokens(Message msg) {
String text = switch (msg) {
case UserMessage um -> um.getText();
case AssistantMessage am -> am.getText();
case SystemMessage sm -> sm.getText();
case ToolResponseMessage trm -> {
StringBuilder sb = new StringBuilder();
for (var resp : trm.getResponses()) {
if (resp.responseData() != null) {
sb.append(resp.responseData().toString());
}
}
yield sb.toString();
}
default -> "";
};
if (text == null || text.isEmpty()) return 10; // 最小估算
return (long) (text.length() / CHARS_PER_TOKEN);
}
/** 提取上一次的摘要文本 */
private String extractPreviousSummary(List<Message> history, int summaryIndex) {
if (summaryIndex <= 0) return null;
Message msg = history.get(summaryIndex);
if (msg instanceof SystemMessage sm && sm.getText() != null) {
String text = sm.getText();
if (text.startsWith("[Conversation Summary]\n")) {
return text.substring("[Conversation Summary]\n".length());
}
if (text.startsWith("[Conversation Summary] ")) {
return text.substring("[Conversation Summary] ".length());
}
}
return null;
}
/** 调用 AI 生成对话段摘要 */
private String generateSummary(List<Message> segment) {
StringBuilder dialogText = new StringBuilder();
for (Message msg : segment) {
switch (msg) {
case UserMessage um -> dialogText.append("[User] ").append(um.getText()).append("\n");
case AssistantMessage am -> {
if (am.getText() != null && !am.getText().isBlank()) {
String text = am.getText();
if (text.length() > 800) text = text.substring(0, 800) + "...";
dialogText.append("[Assistant] ").append(text).append("\n");
}
if (am.hasToolCalls()) {
for (var tc : am.getToolCalls()) {
dialogText.append("[Tool Call] ").append(tc.name()).append("\n");
}
}
}
case ToolResponseMessage trm -> {
for (var resp : trm.getResponses()) {
String data = resp.responseData() != null ? resp.responseData().toString() : "";
if (data.length() > 200) data = data.substring(0, 200) + "...";
dialogText.append("[Tool Result: ").append(resp.name()).append("] ")
.append(data).append("\n");
}
}
default -> {}
}
}
if (dialogText.isEmpty()) return null;
Prompt prompt = new Prompt(List.of(new UserMessage(SUMMARY_PROMPT + dialogText)));
ChatResponse response = chatModel.call(prompt);
return response.getResult().getOutput().getText();
}
}

@ -0,0 +1,105 @@
package com.claudecode.permission;
import java.util.List;
import java.util.Set;
/**
* 危险命令模式检测 识别可能有害的 shell 命令
* <p>
* 即使在 BYPASS 模式下也会对这些命令发出警告
*/
public final class DangerousPatterns {
private DangerousPatterns() {}
/** 危险 shell 命令前缀(不区分大小写匹配) */
private static final List<String> DANGEROUS_BASH_PREFIXES = List.of(
"rm -rf /",
"rm -rf ~",
"rm -rf .",
"rm -r /",
"rmdir /s",
"del /f /s /q",
"format ",
"mkfs.",
"dd if=",
"> /dev/sda",
"chmod -R 777 /",
"chown -R",
":(){:|:&};:" // fork bomb
);
/** 危险代码执行模式 */
private static final List<String> CODE_EXECUTION_PATTERNS = List.of(
"eval ",
"exec ",
"python -c",
"python3 -c",
"node -e",
"ruby -e",
"perl -e",
"| sh",
"| bash",
"| zsh",
"| powershell",
"| pwsh",
"curl | sh",
"wget | sh",
"Invoke-Expression",
"iex ",
"Start-Process",
"Add-Type"
);
/** 在规则匹配中应自动拒绝的工具级通配符 */
private static final Set<String> DANGEROUS_TOOL_WILDCARDS = Set.of(
"Bash", // 不应允许所有 bash 命令
"Bash(*)",
"PowerShell",
"PowerShell(*)"
);
/**
* 检测命令是否包含危险模式
*
* @param command shell 命令文本
* @return 如果危险返回原因描述否则返回 null
*/
public static String detectDangerous(String command) {
if (command == null || command.isBlank()) return null;
String lower = command.toLowerCase().trim();
for (String prefix : DANGEROUS_BASH_PREFIXES) {
if (lower.startsWith(prefix.toLowerCase()) || lower.contains(prefix.toLowerCase())) {
return "Dangerous command detected: " + prefix.trim();
}
}
for (String pattern : CODE_EXECUTION_PATTERNS) {
if (lower.contains(pattern.toLowerCase())) {
return "Code execution pattern detected: " + pattern.trim();
}
}
return null;
}
/**
* 检测是否为危险的工具级通配符规则
* <p>
* 用于防止用户添加过于宽泛的 "always allow" 规则
*/
public static boolean isDangerousWildcard(String ruleStr) {
return DANGEROUS_TOOL_WILDCARDS.contains(ruleStr);
}
/**
* 获取危险原因的简短描述
*/
public static String getDangerLevel(String command) {
String reason = detectDangerous(command);
if (reason == null) return "LOW";
if (reason.contains("Dangerous command")) return "HIGH";
return "MEDIUM";
}
}

@ -0,0 +1,173 @@
package com.claudecode.permission;
import com.claudecode.permission.PermissionTypes.*;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* 权限规则引擎 根据规则模式和工具属性做出权限决策
* <p>
* 决策流程
* <ol>
* <li>检查全局模式BYPASS 全部允许DONT_ASK 拒绝需确认的</li>
* <li>检查 alwaysDeny 规则 匹配则 DENY</li>
* <li>检查 alwaysAllow 规则 匹配则 ALLOW</li>
* <li>只读工具 ALLOW</li>
* <li>ACCEPT_EDITS 模式下文件操作 ALLOW</li>
* <li>检查危险命令 强制 ASK</li>
* <li>默认 ASK</li>
* </ol>
*/
public class PermissionRuleEngine {
private static final Set<String> FILE_EDIT_TOOLS = Set.of("Write", "Edit", "NotebookEdit");
private static final Set<String> READ_ONLY_TOOLS = Set.of(
"Read", "Glob", "Grep", "ListFiles", "WebFetch", "WebSearch",
"TodoRead", "TaskGet", "TaskList", "AskUserQuestion"
);
private final PermissionSettings settings;
public PermissionRuleEngine(PermissionSettings settings) {
this.settings = settings;
}
/**
* 评估工具调用的权限
*
* @param toolName 工具名称
* @param input 工具参数
* @param isReadOnly 工具是否为只读
* @return 权限决策
*/
public PermissionDecision evaluate(String toolName, Map<String, Object> input, boolean isReadOnly) {
PermissionMode mode = settings.getCurrentMode();
// BYPASS 模式:全部允许
if (mode == PermissionMode.BYPASS) {
return PermissionDecision.allow("Bypass mode enabled");
}
// 获取命令内容(用于 Bash/PowerShell 的命令匹配)
String command = extractCommand(toolName, input);
// 检查所有持久化规则
List<PermissionRule> rules = settings.getAllRules();
// 1. 检查 alwaysDeny 规则
for (var rule : rules) {
if (rule.behavior() == PermissionBehavior.DENY && matchesRule(rule, toolName, command)) {
return PermissionDecision.deny("Denied by rule: " + PermissionSettings.formatRule(rule));
}
}
// 2. 检查 alwaysAllow 规则
for (var rule : rules) {
if (rule.behavior() == PermissionBehavior.ALLOW && matchesRule(rule, toolName, command)) {
return PermissionDecision.allow("Allowed by rule: " + PermissionSettings.formatRule(rule));
}
}
// 3. 只读工具直接放行
if (isReadOnly || READ_ONLY_TOOLS.contains(toolName)) {
return PermissionDecision.allow("Read-only tool");
}
// 4. ACCEPT_EDITS 模式:文件操作工具自动允许
if (mode == PermissionMode.ACCEPT_EDITS && FILE_EDIT_TOOLS.contains(toolName)) {
return PermissionDecision.allow("File edits auto-allowed in accept-edits mode");
}
// 5. DONT_ASK 模式:自动拒绝
if (mode == PermissionMode.DONT_ASK) {
return PermissionDecision.deny("Auto-denied in dont-ask mode");
}
// 6. 检查危险命令(强制 ASK,附带警告)
if (command != null) {
String danger = DangerousPatterns.detectDangerous(command);
if (danger != null) {
String prefix = extractCommandPrefix(command);
return new PermissionDecision(
PermissionBehavior.ASK,
"⚠ DANGEROUS: " + danger,
toolName, prefix, List.of()
);
}
}
// 7. 默认:需要用户确认
String prefix = extractCommandPrefix(command);
return PermissionDecision.ask(toolName, prefix);
}
/**
* 根据用户选择应用权限变更
*/
public void applyChoice(PermissionChoice choice, String toolName, String command) {
String prefix = extractCommandPrefix(command);
switch (choice) {
case ALWAYS_ALLOW -> {
var rule = prefix != null
? PermissionRule.forCommand(toolName, prefix, PermissionBehavior.ALLOW)
: PermissionRule.forTool(toolName, PermissionBehavior.ALLOW);
// 检查是否为危险通配符
String ruleStr = PermissionSettings.formatRule(rule);
if (!DangerousPatterns.isDangerousWildcard(ruleStr)) {
settings.addUserRule(rule);
}
}
case ALWAYS_DENY -> {
var rule = prefix != null
? PermissionRule.forCommand(toolName, prefix, PermissionBehavior.DENY)
: PermissionRule.forTool(toolName, PermissionBehavior.DENY);
settings.addUserRule(rule);
}
case ALLOW_ONCE, DENY_ONCE -> {
// 单次操作,不持久化
}
}
}
// ── 内部匹配方法 ──
/** 检查规则是否匹配当前工具和命令 */
boolean matchesRule(PermissionRule rule, String toolName, String command) {
// 工具名不匹配直接跳过
if (!rule.toolName().equalsIgnoreCase(toolName)) return false;
String content = rule.ruleContent();
// 通配符 * 匹配所有命令
if ("*".equals(content)) return true;
// 前缀匹配模式:npm:* 匹配以 "npm" 开头的命令
if (content.endsWith(":*") && command != null) {
String prefix = content.substring(0, content.length() - 2);
return command.toLowerCase().startsWith(prefix.toLowerCase());
}
// 精确匹配
return content.equalsIgnoreCase(command);
}
/** 从工具参数中提取命令文本 */
private String extractCommand(String toolName, Map<String, Object> input) {
if (input == null) return null;
return switch (toolName) {
case "Bash" -> (String) input.get("command");
case "Write" -> (String) input.get("file_path");
case "Edit" -> (String) input.get("file_path");
default -> null;
};
}
/** 提取命令前缀(第一个空格前的部分) */
private String extractCommandPrefix(String command) {
if (command == null || command.isBlank()) return null;
String trimmed = command.trim();
int space = trimmed.indexOf(' ');
return space > 0 ? trimmed.substring(0, space) : trimmed;
}
}

@ -0,0 +1,200 @@
package com.claudecode.permission;
import com.claudecode.permission.PermissionTypes.PermissionBehavior;
import com.claudecode.permission.PermissionTypes.PermissionRule;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
/**
* 权限设置持久化 管理用户级和项目级权限规则文件
* <p>
* 存储结构
* <ul>
* <li>用户级: ~/.claude-code-java/settings.json</li>
* <li>项目级: .claude-code-java/settings.json</li>
* </ul>
* 加载优先级: 项目级 > 用户级 > 会话级
*/
public class PermissionSettings {
private static final String SETTINGS_DIR = ".claude-code-java";
private static final String SETTINGS_FILE = "settings.json";
private static final ObjectMapper MAPPER = new ObjectMapper()
.enable(SerializationFeature.INDENT_OUTPUT);
/** 内存中的合并规则(从所有来源加载后合并) */
private final List<PermissionRule> sessionRules = new ArrayList<>();
private PermissionTypes.PermissionMode currentMode = PermissionTypes.PermissionMode.DEFAULT;
private final Path userSettingsPath;
private final Path projectSettingsPath;
private SettingsData userData = new SettingsData();
private SettingsData projectData = new SettingsData();
public PermissionSettings() {
this(Path.of(System.getProperty("user.home")),
Path.of(System.getProperty("user.dir")));
}
public PermissionSettings(Path userHome, Path projectDir) {
this.userSettingsPath = userHome.resolve(SETTINGS_DIR).resolve(SETTINGS_FILE);
this.projectSettingsPath = projectDir.resolve(SETTINGS_DIR).resolve(SETTINGS_FILE);
}
/** 从磁盘加载所有设置 */
public void load() {
userData = loadFromFile(userSettingsPath);
projectData = loadFromFile(projectSettingsPath);
// 项目级模式优先
if (projectData.permissions.mode != null) {
currentMode = projectData.permissions.mode;
} else if (userData.permissions.mode != null) {
currentMode = userData.permissions.mode;
}
}
/** 获取所有合并后的规则(项目级 > 用户级 > 会话级) */
public List<PermissionRule> getAllRules() {
var rules = new ArrayList<PermissionRule>();
// 项目级优先
rules.addAll(toRules(projectData.permissions.alwaysAllow, PermissionBehavior.ALLOW));
rules.addAll(toRules(projectData.permissions.alwaysDeny, PermissionBehavior.DENY));
// 用户级
rules.addAll(toRules(userData.permissions.alwaysAllow, PermissionBehavior.ALLOW));
rules.addAll(toRules(userData.permissions.alwaysDeny, PermissionBehavior.DENY));
// 会话级
rules.addAll(sessionRules);
return rules;
}
/** 添加规则并保存到用户级设置 */
public void addUserRule(PermissionRule rule) {
if (rule.behavior() == PermissionBehavior.ALLOW) {
userData.permissions.alwaysAllow.add(formatRule(rule));
} else if (rule.behavior() == PermissionBehavior.DENY) {
userData.permissions.alwaysDeny.add(formatRule(rule));
}
saveToFile(userSettingsPath, userData);
}
/** 添加规则到会话级(不持久化) */
public void addSessionRule(PermissionRule rule) {
sessionRules.add(rule);
}
/** 移除用户级规则 */
public void removeUserRule(String ruleStr) {
userData.permissions.alwaysAllow.remove(ruleStr);
userData.permissions.alwaysDeny.remove(ruleStr);
saveToFile(userSettingsPath, userData);
}
/** 清除所有规则 */
public void clearAll() {
userData.permissions.alwaysAllow.clear();
userData.permissions.alwaysDeny.clear();
projectData.permissions.alwaysAllow.clear();
projectData.permissions.alwaysDeny.clear();
sessionRules.clear();
saveToFile(userSettingsPath, userData);
}
public PermissionTypes.PermissionMode getCurrentMode() {
return currentMode;
}
public void setCurrentMode(PermissionTypes.PermissionMode mode) {
this.currentMode = mode;
userData.permissions.mode = mode;
saveToFile(userSettingsPath, userData);
}
/** 获取所有已保存规则的可读列表 */
public List<String> listRules() {
var result = new ArrayList<String>();
for (var r : userData.permissions.alwaysAllow) {
result.add("[user] ALLOW " + r);
}
for (var r : userData.permissions.alwaysDeny) {
result.add("[user] DENY " + r);
}
for (var r : projectData.permissions.alwaysAllow) {
result.add("[proj] ALLOW " + r);
}
for (var r : projectData.permissions.alwaysDeny) {
result.add("[proj] DENY " + r);
}
for (var r : sessionRules) {
result.add("[sess] " + r.behavior() + " " + formatRule(r));
}
return result;
}
// ── 内部方法 ──
private SettingsData loadFromFile(Path path) {
if (!Files.exists(path)) return new SettingsData();
try {
return MAPPER.readValue(path.toFile(), SettingsData.class);
} catch (IOException e) {
return new SettingsData();
}
}
private void saveToFile(Path path, SettingsData data) {
try {
Files.createDirectories(path.getParent());
MAPPER.writeValue(path.toFile(), data);
} catch (IOException e) {
// 静默失败,不影响主流程
}
}
private List<PermissionRule> toRules(List<String> ruleStrings, PermissionBehavior behavior) {
return ruleStrings.stream()
.map(s -> parseRule(s, behavior))
.toList();
}
/** 解析规则字符串,格式: "ToolName(pattern)" 或 "ToolName" */
static PermissionRule parseRule(String ruleStr, PermissionBehavior behavior) {
int parenStart = ruleStr.indexOf('(');
if (parenStart > 0 && ruleStr.endsWith(")")) {
String toolName = ruleStr.substring(0, parenStart);
String content = ruleStr.substring(parenStart + 1, ruleStr.length() - 1);
return new PermissionRule(toolName, content, behavior);
}
return PermissionRule.forTool(ruleStr, behavior);
}
/** 格式化规则为字符串 */
static String formatRule(PermissionRule rule) {
if ("*".equals(rule.ruleContent())) {
return rule.toolName();
}
return rule.toolName() + "(" + rule.ruleContent() + ")";
}
// ── JSON 数据结构 ──
@JsonIgnoreProperties(ignoreUnknown = true)
static class SettingsData {
public PermissionsBlock permissions = new PermissionsBlock();
}
@JsonIgnoreProperties(ignoreUnknown = true)
static class PermissionsBlock {
public PermissionTypes.PermissionMode mode;
public List<String> alwaysAllow = new ArrayList<>();
public List<String> alwaysDeny = new ArrayList<>();
public List<String> additionalDirectories = new ArrayList<>();
}
}

@ -0,0 +1,110 @@
package com.claudecode.permission;
import java.util.List;
/**
* 权限管理类型定义 对应 claude-code 中的 permissions.ts
*/
public final class PermissionTypes {
private PermissionTypes() {}
/** 权限行为 */
public enum PermissionBehavior {
ALLOW, // 允许执行
DENY, // 拒绝执行
ASK // 需要用户确认
}
/** 权限模式 */
public enum PermissionMode {
/** 默认模式:非只读工具需要用户确认 */
DEFAULT,
/** 自动允许文件编辑,shell 命令仍需确认 */
ACCEPT_EDITS,
/** 跳过所有权限检查(不安全) */
BYPASS,
/** 自动拒绝而非询问用户(无头模式) */
DONT_ASK
}
/**
* 权限规则 定义工具和命令模式的权限行为
* <p>
* 示例
* <ul>
* <li>{@code PermissionRule("Bash", "npm:*", ALLOW)} 允许所有 npm 命令</li>
* <li>{@code PermissionRule("Bash", "rm -rf:*", DENY)} 拒绝 rm -rf</li>
* <li>{@code PermissionRule("Write", "*", ALLOW)} 允许所有文件写入</li>
* </ul>
*
* @param toolName 工具名称 Bash, Write, Edit
* @param ruleContent 规则内容支持通配符 * "npm:*", "git:*", "*"
* @param behavior 权限行为
*/
public record PermissionRule(
String toolName,
String ruleContent,
PermissionBehavior behavior
) {
/** 匹配整个工具(无命令模式限制) */
public static PermissionRule forTool(String toolName, PermissionBehavior behavior) {
return new PermissionRule(toolName, "*", behavior);
}
/** 匹配工具的特定命令前缀 */
public static PermissionRule forCommand(String toolName, String prefix, PermissionBehavior behavior) {
return new PermissionRule(toolName, prefix + ":*", behavior);
}
}
/** 权限决策结果 */
public record PermissionDecision(
PermissionBehavior behavior,
String reason,
String toolName,
String commandPrefix,
List<PermissionRule> suggestedRules
) {
public static PermissionDecision allow(String reason) {
return new PermissionDecision(PermissionBehavior.ALLOW, reason, null, null, List.of());
}
public static PermissionDecision deny(String reason) {
return new PermissionDecision(PermissionBehavior.DENY, reason, null, null, List.of());
}
public static PermissionDecision ask(String toolName, String commandPrefix) {
// 生成建议规则供用户选择 "always allow"
var suggested = List.of(
PermissionRule.forCommand(toolName, commandPrefix, PermissionBehavior.ALLOW)
);
return new PermissionDecision(PermissionBehavior.ASK, "Requires user confirmation",
toolName, commandPrefix, suggested);
}
public boolean isAllowed() {
return behavior == PermissionBehavior.ALLOW;
}
public boolean isDenied() {
return behavior == PermissionBehavior.DENY;
}
public boolean needsAsk() {
return behavior == PermissionBehavior.ASK;
}
}
/** 权限确认选项(用户在 UI 中的选择) */
public enum PermissionChoice {
/** 允许本次执行 */
ALLOW_ONCE,
/** 始终允许此模式 */
ALWAYS_ALLOW,
/** 拒绝本次执行 */
DENY_ONCE,
/** 始终拒绝此模式 */
ALWAYS_DENY
}
}

@ -6,6 +6,9 @@ import com.claudecode.command.CommandRegistry;
import com.claudecode.console.*;
import com.claudecode.core.AgentLoop;
import com.claudecode.core.ConversationPersistence;
import com.claudecode.permission.DangerousPatterns;
import com.claudecode.permission.PermissionTypes.PermissionChoice;
import com.claudecode.permission.PermissionTypes.PermissionDecision;
import com.claudecode.tool.ToolRegistry;
import com.claudecode.tool.impl.AskUserQuestionTool;
import org.jline.reader.*;
@ -430,11 +433,21 @@ public class ReplSession {
/**
* 显示权限确认提示并等待用户输入
* 用于危险操作文件写入bash 执行等前的安全确认
* 支持 4 种选项Y(允许一次) / A(始终允许) / N(拒绝) / D(始终拒绝)
*/
private boolean promptPermission(AgentLoop.PermissionRequest request) {
private PermissionChoice promptPermission(AgentLoop.PermissionRequest request) {
out.println();
// 检查是否为危险命令
PermissionDecision decision = request.decision();
boolean isDangerous = (decision != null && decision.reason() != null
&& decision.reason().startsWith("⚠ DANGEROUS"));
if (isDangerous) {
out.println(AnsiStyle.red(" ⚠ DANGEROUS Operation"));
} else {
out.println(AnsiStyle.yellow(" ⚠ Permission Required"));
}
out.println(" " + "─".repeat(50));
out.println(" " + AnsiStyle.bold("Tool: ") + AnsiStyle.cyan(request.toolName()));
out.println(" " + AnsiStyle.bold("Action: ") + request.activityDescription());
@ -448,30 +461,46 @@ public class ReplSession {
out.println(" " + AnsiStyle.dim("Args: " + argsPreview));
}
// 显示建议规则
String suggestedRule = null;
if (decision != null && decision.commandPrefix() != null) {
suggestedRule = request.toolName() + "(" + decision.commandPrefix() + ":*)";
}
out.println(" " + "─".repeat(50));
out.print(" " + AnsiStyle.bold("Allow execution?") + AnsiStyle.dim(" [Y/n/always] ") + AnsiStyle.BOLD + AnsiStyle.BRIGHT_CYAN + "→ " + AnsiStyle.RESET);
out.println(" " + AnsiStyle.green("[Y]") + " Allow once");
if (suggestedRule != null && !isDangerous) {
out.println(" " + AnsiStyle.green("[A]") + " Always allow " + AnsiStyle.cyan(suggestedRule));
}
out.println(" " + AnsiStyle.red("[N]") + " Deny");
if (suggestedRule != null) {
out.println(" " + AnsiStyle.red("[D]") + " Always deny this pattern");
}
out.print(" " + AnsiStyle.bold("Choice") + AnsiStyle.dim(" [Y/a/n/d] ") + AnsiStyle.BOLD + AnsiStyle.BRIGHT_CYAN + "→ " + AnsiStyle.RESET);
out.flush();
String answer = readLineForPermission();
if (answer == null) return false;
if (answer == null) return PermissionChoice.DENY_ONCE;
answer = answer.strip().toLowerCase();
// "always" → 禁用后续权限确认
if (answer.equals("always") || answer.equals("a")) {
agentLoop.setOnPermissionRequest(null); // 移除权限回调
out.println(AnsiStyle.green(" ✓ All subsequent operations authorized"));
return true;
return switch (answer) {
case "a", "always" -> {
out.println(AnsiStyle.green(" ✓ Rule saved: always allow " +
(suggestedRule != null ? suggestedRule : request.toolName())));
yield PermissionChoice.ALWAYS_ALLOW;
}
// 空字符串或 y/yes → 允许
if (answer.isEmpty() || answer.equals("y") || answer.equals("yes")) {
return true;
case "d" -> {
out.println(AnsiStyle.red(" ✗ Rule saved: always deny " +
(suggestedRule != null ? suggestedRule : request.toolName())));
yield PermissionChoice.ALWAYS_DENY;
}
// 其他输入 → 拒绝
case "n", "no" -> {
out.println(AnsiStyle.red(" ✗ Operation denied"));
return false;
yield PermissionChoice.DENY_ONCE;
}
default -> PermissionChoice.ALLOW_ONCE; // 空字符串、y、yes → 允许
};
}
/** 读取权限确认的用户输入(兼容 JLine 和 Scanner 模式) */

Loading…
Cancel
Save