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:
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user