feat: Phase4 命令系统与上下文增强

新增基础设施:
- TokenTracker: 真实Token使用量追踪和费用估算(按模型定价)
- SkillLoader: .claude/skills/ 扫描加载,支持YAML frontmatter元数据
- GitContext: Git分支/状态/最近提交收集,注入系统提示词

新增命令 (4个):
- /init: 项目CLAUDE.md初始化向导,自动检测项目类型(Maven/Gradle/Node/Python等)
- /status: 会话状态仪表板(模型、Token、内存、运行时间等)
- /context: 上下文概览(CLAUDE.md、Skills、Git、系统提示词大小)
- /config: 配置查看/设置(支持model快捷切换和API key显示)

增强命令 (3个):
- /cost: 从占位→真实Token统计,显示input/output/cache tokens和费用
- /model: 支持快捷名称切换(sonnet/opus/haiku),显示可用模型列表
- /compact: 增强显示压缩前后消息数和Token使用量

系统提示词增强:
- 集成Skills摘要和Git上下文到系统提示词
- 新增Shell环境信息和更详细的Guidelines

命令总数: 6 → 10, 工具总数: 11

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
liuzh
2026-04-01 20:59:22 +08:00
co-authored by Copilot
parent e09c3de91e
commit 7a6c2fcc02
13 changed files with 1043 additions and 37 deletions
@@ -41,6 +41,7 @@ public class AgentLoop {
private final ToolRegistry toolRegistry;
private final ToolContext toolContext;
private final String systemPrompt;
private final TokenTracker tokenTracker;
/** 消息历史 —— 自行管理,不依赖 Spring AI ChatMemory */
private final List<Message> messageHistory = new ArrayList<>();
@@ -53,10 +54,16 @@ public class AgentLoop {
public AgentLoop(ChatModel chatModel, ToolRegistry toolRegistry,
ToolContext toolContext, String systemPrompt) {
this(chatModel, toolRegistry, toolContext, systemPrompt, new TokenTracker());
}
public AgentLoop(ChatModel chatModel, ToolRegistry toolRegistry,
ToolContext toolContext, String systemPrompt, TokenTracker tokenTracker) {
this.chatModel = chatModel;
this.toolRegistry = toolRegistry;
this.toolContext = toolContext;
this.systemPrompt = systemPrompt;
this.tokenTracker = tokenTracker;
// 添加系统提示词到消息历史
this.messageHistory.add(new SystemMessage(systemPrompt));
}
@@ -94,6 +101,15 @@ public class AgentLoop {
Prompt prompt = new Prompt(List.copyOf(messageHistory), options);
ChatResponse response = chatModel.call(prompt);
// 记录 Token 使用量
if (response.getMetadata() != null && response.getMetadata().getUsage() != null) {
var usage = response.getMetadata().getUsage();
tokenTracker.recordUsage(
usage.getPromptTokens(),
usage.getCompletionTokens()
);
}
AssistantMessage assistant = response.getResult().getOutput();
messageHistory.add(assistant);
@@ -169,6 +185,16 @@ public class AgentLoop {
return Collections.unmodifiableList(messageHistory);
}
/** 获取 Token 追踪器 */
public TokenTracker getTokenTracker() {
return tokenTracker;
}
/** 获取系统提示词 */
public String getSystemPrompt() {
return systemPrompt;
}
/** 重置历史(保留系统提示词) */
public void reset() {
messageHistory.clear();
@@ -0,0 +1,91 @@
package com.claudecode.core;
import java.util.concurrent.atomic.AtomicLong;
/**
* Token 使用量追踪器 —— 记录 API 调用的 token 消耗。
* <p>
* 从 ChatResponse 的 usage 元数据中提取 token 统计信息,
* 支持按会话累计和费用估算。
*/
public class TokenTracker {
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);
/** 模型定价(每百万 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";
/** 记录一次 API 调用的 token 使用 */
public void recordUsage(long inputTokens, long outputTokens) {
totalInputTokens.addAndGet(inputTokens);
totalOutputTokens.addAndGet(outputTokens);
apiCallCount.incrementAndGet();
}
/** 记录一次包含缓存的 API 调用 */
public void recordUsage(long inputTokens, long outputTokens, long cacheRead, long cacheCreation) {
totalInputTokens.addAndGet(inputTokens);
totalOutputTokens.addAndGet(outputTokens);
totalCacheReadTokens.addAndGet(cacheRead);
totalCacheCreationTokens.addAndGet(cacheCreation);
apiCallCount.incrementAndGet();
}
/** 设置模型和对应定价 */
public void setModel(String model) {
this.modelName = model;
// 根据模型设置定价
if (model.contains("opus")) {
inputPricePerMillion = 15.0;
outputPricePerMillion = 75.0;
cacheReadPricePerMillion = 1.5;
} else if (model.contains("sonnet")) {
inputPricePerMillion = 3.0;
outputPricePerMillion = 15.0;
cacheReadPricePerMillion = 0.3;
} else if (model.contains("haiku")) {
inputPricePerMillion = 0.25;
outputPricePerMillion = 1.25;
cacheReadPricePerMillion = 0.03;
}
}
public long getInputTokens() { return totalInputTokens.get(); }
public long getOutputTokens() { return totalOutputTokens.get(); }
public long getCacheReadTokens() { return totalCacheReadTokens.get(); }
public long getCacheCreationTokens() { return totalCacheCreationTokens.get(); }
public long getTotalTokens() { return totalInputTokens.get() + totalOutputTokens.get(); }
public long getApiCallCount() { return apiCallCount.get(); }
public String getModelName() { return modelName; }
/** 估算当前会话费用(美元) */
public double estimateCost() {
double inputCost = totalInputTokens.get() * inputPricePerMillion / 1_000_000.0;
double outputCost = totalOutputTokens.get() * outputPricePerMillion / 1_000_000.0;
double cacheCost = totalCacheReadTokens.get() * cacheReadPricePerMillion / 1_000_000.0;
return inputCost + outputCost + cacheCost;
}
/** 重置统计 */
public void reset() {
totalInputTokens.set(0);
totalOutputTokens.set(0);
totalCacheReadTokens.set(0);
totalCacheCreationTokens.set(0);
apiCallCount.set(0);
}
/** 格式化 token 数量(带千位分隔) */
public static String formatTokens(long tokens) {
if (tokens < 1000) return String.valueOf(tokens);
if (tokens < 1_000_000) return String.format("%.1fK", tokens / 1000.0);
return String.format("%.2fM", tokens / 1_000_000.0);
}
}