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>
This commit is contained in:
liuzh
2026-04-02 23:05:37 +08:00
co-authored by Copilot
parent c989d3b451
commit 730551cb3f
16 changed files with 1759 additions and 127 deletions
@@ -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));
}
// 保留最后一轮用户消息和助手回复(如果有)
for (int i = Math.max(1, before - 2); i < before; i++) {
compacted.add(history.get(i));
}
context.agentLoop().replaceHistory(compacted);
int after = compacted.size();
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"));
// 优先使用 AutoCompactManager 中的 FullCompact
FullCompact fullCompact;
AutoCompactManager acm = context.agentLoop().getAutoCompactManager();
if (acm != null) {
fullCompact = acm.getFullCompact();
} else {
sb.append(AnsiStyle.dim(" ⚠ AI summary generation failed, keeping recent conversation only"));
fullCompact = new FullCompact(context.agentLoop().getChatModel());
}
return sb.toString();
}
// 执行全量压缩
List<Message> compacted = fullCompact.compact(new ArrayList<>(history));
/** 调用 AI 生成对话摘要 */
private String generateSummary(CommandContext context, List<Message> history) {
try {
ChatModel chatModel = context.agentLoop().getChatModel();
if (compacted != null) {
context.agentLoop().replaceHistory(compacted);
int after = compacted.size();
// 构建摘要请求的消息列史
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 -> {} // 跳过系统消息和工具响应
}
// 重置熔断器(手动压缩成功说明 AI 摘要功能正常)
if (acm != null) {
acm.resetCircuitBreaker();
}
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;
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");
}
sb.append(AnsiStyle.dim(" 📝 AI summary generated and injected into context"));
return sb.toString();
}
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");
}
}