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:
@@ -3,12 +3,13 @@ package com.claudecode.command.impl;
|
||||
import com.claudecode.command.CommandContext;
|
||||
import com.claudecode.command.SlashCommand;
|
||||
import com.claudecode.console.AnsiStyle;
|
||||
import com.claudecode.core.TokenTracker;
|
||||
|
||||
/**
|
||||
* /compact 命令 —— 压缩当前对话上下文。
|
||||
* <p>
|
||||
* 对应 claude-code/src/commands/compact.ts。
|
||||
* 当前为简化实现,直接清空历史并提示用户。
|
||||
* 保留系统提示词,用摘要替换详细的对话历史。
|
||||
*/
|
||||
public class CompactCommand implements SlashCommand {
|
||||
|
||||
@@ -24,11 +25,33 @@ public class CompactCommand implements SlashCommand {
|
||||
|
||||
@Override
|
||||
public String execute(String args, CommandContext context) {
|
||||
if (context.agentLoop() != null) {
|
||||
int before = context.agentLoop().getMessageHistory().size();
|
||||
context.agentLoop().reset();
|
||||
return AnsiStyle.green(" ✓ Context compacted: " + before + " messages → 1 (system prompt only)");
|
||||
if (context.agentLoop() == null) {
|
||||
return AnsiStyle.yellow(" ⚠ No active conversation to compact.");
|
||||
}
|
||||
return AnsiStyle.yellow(" ⚠ No active conversation to compact.");
|
||||
|
||||
int before = context.agentLoop().getMessageHistory().size();
|
||||
|
||||
if (before <= 2) {
|
||||
return AnsiStyle.dim(" Context is already minimal (" + before + " messages). Nothing to compact.");
|
||||
}
|
||||
|
||||
// 记录压缩前的 token 使用
|
||||
TokenTracker tracker = context.agentLoop().getTokenTracker();
|
||||
long tokensBefore = tracker.getInputTokens() + tracker.getOutputTokens();
|
||||
|
||||
// 重置历史(保留系统提示词)
|
||||
context.agentLoop().reset();
|
||||
|
||||
int after = context.agentLoop().getMessageHistory().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 used before compact: ").append(TokenTracker.formatTokens(tokensBefore)).append("\n");
|
||||
}
|
||||
sb.append(AnsiStyle.dim(" Conversation history cleared. System prompt retained."));
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
package com.claudecode.command.impl;
|
||||
|
||||
import com.claudecode.command.CommandContext;
|
||||
import com.claudecode.command.SlashCommand;
|
||||
import com.claudecode.console.AnsiStyle;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* /config 命令 —— 查看和设置应用配置。
|
||||
* <p>
|
||||
* 支持查看当前配置、设置单个配置项。
|
||||
* 配置变更仅在当前会话内生效。
|
||||
*/
|
||||
public class ConfigCommand implements SlashCommand {
|
||||
|
||||
/** 支持的配置项及说明 */
|
||||
private static final Map<String, String> CONFIG_KEYS = Map.of(
|
||||
"model", "AI model name (e.g., claude-sonnet-4-20250514)",
|
||||
"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)"
|
||||
);
|
||||
|
||||
@Override
|
||||
public String name() {
|
||||
return "config";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String description() {
|
||||
return "View or set configuration";
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> aliases() {
|
||||
return List.of("cfg");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String execute(String args, CommandContext context) {
|
||||
if (args == null || args.isBlank()) {
|
||||
return showAllConfig(context);
|
||||
}
|
||||
|
||||
String[] parts = args.strip().split("\\s+", 2);
|
||||
String key = parts[0];
|
||||
|
||||
if (parts.length == 1) {
|
||||
// 显示单个配置项
|
||||
return showConfig(key, context);
|
||||
}
|
||||
|
||||
// 设置配置项
|
||||
String value = parts[1];
|
||||
return setConfig(key, value, context);
|
||||
}
|
||||
|
||||
private String showAllConfig(CommandContext context) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("\n");
|
||||
sb.append(AnsiStyle.bold(" ⚙ Configuration\n"));
|
||||
sb.append(" ").append("─".repeat(40)).append("\n\n");
|
||||
|
||||
// 当前活跃配置
|
||||
String model = context.agentLoop().getTokenTracker().getModelName();
|
||||
sb.append(" ").append(AnsiStyle.bold("model: ")).append(AnsiStyle.cyan(model)).append("\n");
|
||||
|
||||
String apiKey = System.getenv("ANTHROPIC_API_KEY");
|
||||
if (apiKey != null && apiKey.length() > 8) {
|
||||
sb.append(" ").append(AnsiStyle.bold("api-key: ")).append(AnsiStyle.dim(
|
||||
apiKey.substring(0, 8) + "..." + apiKey.substring(apiKey.length() - 4))).append("\n");
|
||||
} else {
|
||||
sb.append(" ").append(AnsiStyle.bold("api-key: ")).append(AnsiStyle.yellow("(not set)")).append("\n");
|
||||
}
|
||||
|
||||
String baseUrl = System.getenv().getOrDefault("ANTHROPIC_BASE_URL", "https://api.anthropic.com");
|
||||
sb.append(" ").append(AnsiStyle.bold("base-url: ")).append(AnsiStyle.dim(baseUrl)).append("\n");
|
||||
|
||||
sb.append("\n");
|
||||
sb.append(AnsiStyle.dim(" Available keys:\n"));
|
||||
for (var entry : CONFIG_KEYS.entrySet()) {
|
||||
sb.append(AnsiStyle.dim(" " + entry.getKey() + " — " + entry.getValue())).append("\n");
|
||||
}
|
||||
sb.append("\n");
|
||||
sb.append(AnsiStyle.dim(" Usage: /config <key> <value>"));
|
||||
|
||||
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" -> {
|
||||
context.agentLoop().getTokenTracker().setModel(value);
|
||||
yield AnsiStyle.green(" ✅ Model set to: " + value) + "\n"
|
||||
+ AnsiStyle.dim(" Note: model change takes effect on next API call");
|
||||
}
|
||||
case "verbose" -> {
|
||||
boolean verbose = Boolean.parseBoolean(value);
|
||||
yield AnsiStyle.green(" ✅ Verbose mode: " + (verbose ? "ON" : "OFF"));
|
||||
}
|
||||
default -> {
|
||||
if (!CONFIG_KEYS.containsKey(key)) {
|
||||
yield AnsiStyle.yellow(" ⚠ Unknown config key: " + key);
|
||||
}
|
||||
yield AnsiStyle.yellow(" ⚠ Setting '" + key + "' is not yet supported at runtime") + "\n"
|
||||
+ AnsiStyle.dim(" Set via application.yml or environment variables");
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
package com.claudecode.command.impl;
|
||||
|
||||
import com.claudecode.command.CommandContext;
|
||||
import com.claudecode.command.SlashCommand;
|
||||
import com.claudecode.console.AnsiStyle;
|
||||
import com.claudecode.context.ClaudeMdLoader;
|
||||
import com.claudecode.context.GitContext;
|
||||
import com.claudecode.context.SkillLoader;
|
||||
|
||||
import java.nio.file.Path;
|
||||
|
||||
/**
|
||||
* /context 命令 —— 显示当前上下文信息。
|
||||
* <p>
|
||||
* 展示已加载的 CLAUDE.md、Skills、Git 上下文和 Token 预算使用情况。
|
||||
*/
|
||||
public class ContextCommand implements SlashCommand {
|
||||
|
||||
@Override
|
||||
public String name() {
|
||||
return "context";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String description() {
|
||||
return "Show current context (CLAUDE.md, skills, git)";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String execute(String args, CommandContext context) {
|
||||
Path projectDir = Path.of(System.getProperty("user.dir"));
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("\n");
|
||||
sb.append(AnsiStyle.bold(" 📋 Context Overview\n"));
|
||||
sb.append(" ").append("─".repeat(40)).append("\n\n");
|
||||
|
||||
// CLAUDE.md 状态
|
||||
sb.append(AnsiStyle.bold(" CLAUDE.md:\n"));
|
||||
ClaudeMdLoader loader = new ClaudeMdLoader(projectDir);
|
||||
String claudeMd = loader.load();
|
||||
if (claudeMd.isEmpty()) {
|
||||
sb.append(AnsiStyle.dim(" (none loaded) — run /init to create one\n"));
|
||||
} else {
|
||||
// 显示摘要(前 200 字符)
|
||||
int lines = claudeMd.split("\n").length;
|
||||
sb.append(" ").append(AnsiStyle.green(lines + " lines loaded")).append("\n");
|
||||
String preview = claudeMd.length() > 200
|
||||
? claudeMd.substring(0, 200) + "..."
|
||||
: claudeMd;
|
||||
for (String line : preview.split("\n")) {
|
||||
sb.append(AnsiStyle.dim(" │ " + line)).append("\n");
|
||||
}
|
||||
}
|
||||
sb.append("\n");
|
||||
|
||||
// Skills 状态
|
||||
sb.append(AnsiStyle.bold(" Skills:\n"));
|
||||
SkillLoader skillLoader = new SkillLoader(projectDir);
|
||||
var skills = skillLoader.loadAll();
|
||||
if (skills.isEmpty()) {
|
||||
sb.append(AnsiStyle.dim(" (none loaded) — add .md files to .claude/skills/\n"));
|
||||
} else {
|
||||
for (var skill : skills) {
|
||||
sb.append(" • ").append(AnsiStyle.cyan(skill.name()));
|
||||
if (!skill.description().isEmpty()) {
|
||||
sb.append(AnsiStyle.dim(" — " + skill.description()));
|
||||
}
|
||||
sb.append(AnsiStyle.dim(" [" + skill.source() + "]")).append("\n");
|
||||
}
|
||||
}
|
||||
sb.append("\n");
|
||||
|
||||
// Git 上下文
|
||||
sb.append(AnsiStyle.bold(" Git:\n"));
|
||||
GitContext git = new GitContext(projectDir).collect();
|
||||
if (!git.isGitRepo()) {
|
||||
sb.append(AnsiStyle.dim(" (not a git repository)\n"));
|
||||
} else {
|
||||
sb.append(" Branch: ").append(AnsiStyle.cyan(git.getBranch() != null ? git.getBranch() : "unknown")).append("\n");
|
||||
if (git.getStatus() != null) {
|
||||
long modifiedCount = git.getStatus().lines()
|
||||
.filter(l -> !l.startsWith("##"))
|
||||
.count();
|
||||
if (modifiedCount > 0) {
|
||||
sb.append(" Modified: ").append(AnsiStyle.yellow(modifiedCount + " file(s)")).append("\n");
|
||||
} else {
|
||||
sb.append(" Working tree: ").append(AnsiStyle.green("clean")).append("\n");
|
||||
}
|
||||
}
|
||||
}
|
||||
sb.append("\n");
|
||||
|
||||
// Token 预算
|
||||
sb.append(AnsiStyle.bold(" System Prompt:\n"));
|
||||
String sysPrompt = context.agentLoop().getSystemPrompt();
|
||||
int charCount = sysPrompt.length();
|
||||
int estimatedTokens = charCount / 4; // 粗略估算
|
||||
sb.append(" Size: ").append(charCount).append(" chars (~").append(estimatedTokens).append(" tokens)\n");
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
||||
@@ -3,12 +3,13 @@ package com.claudecode.command.impl;
|
||||
import com.claudecode.command.CommandContext;
|
||||
import com.claudecode.command.SlashCommand;
|
||||
import com.claudecode.console.AnsiStyle;
|
||||
import com.claudecode.core.TokenTracker;
|
||||
|
||||
/**
|
||||
* /cost 命令 —— 显示 Token 使用量和费用估算。
|
||||
* <p>
|
||||
* 对应 claude-code/src/commands/cost.ts。
|
||||
* 当前为占位实现,后续接入实际 Token 统计。
|
||||
* 从 AgentLoop 的 TokenTracker 获取真实 Token 统计。
|
||||
*/
|
||||
public class CostCommand implements SlashCommand {
|
||||
|
||||
@@ -24,21 +25,40 @@ public class CostCommand implements SlashCommand {
|
||||
|
||||
@Override
|
||||
public String execute(String args, CommandContext context) {
|
||||
int msgCount = 0;
|
||||
if (context.agentLoop() != null) {
|
||||
msgCount = context.agentLoop().getMessageHistory().size();
|
||||
}
|
||||
TokenTracker tracker = context.agentLoop().getTokenTracker();
|
||||
int msgCount = context.agentLoop().getMessageHistory().size();
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("\n");
|
||||
sb.append(AnsiStyle.bold(" Token Usage:\n\n"));
|
||||
sb.append(" Messages: ").append(AnsiStyle.cyan(String.valueOf(msgCount))).append("\n");
|
||||
sb.append(" Input tokens: ").append(AnsiStyle.dim("(tracking not yet implemented)")).append("\n");
|
||||
sb.append(" Output tokens:").append(AnsiStyle.dim("(tracking not yet implemented)")).append("\n");
|
||||
sb.append(" Est. cost: ").append(AnsiStyle.dim("(tracking not yet implemented)")).append("\n");
|
||||
sb.append("\n");
|
||||
sb.append(AnsiStyle.dim(" Token tracking will be added in a future update."));
|
||||
sb.append(AnsiStyle.bold(" 💰 Token Usage & Cost\n"));
|
||||
sb.append(" ").append("─".repeat(40)).append("\n\n");
|
||||
|
||||
sb.append(" ").append(AnsiStyle.bold("Model: ")).append(AnsiStyle.cyan(tracker.getModelName())).append("\n");
|
||||
sb.append(" ").append(AnsiStyle.bold("API Calls: ")).append(tracker.getApiCallCount()).append("\n");
|
||||
sb.append(" ").append(AnsiStyle.bold("Messages: ")).append(msgCount).append("\n\n");
|
||||
|
||||
sb.append(" ").append(AnsiStyle.bold("Input tokens: ")).append(formatTokenLine(tracker.getInputTokens())).append("\n");
|
||||
sb.append(" ").append(AnsiStyle.bold("Output tokens:")).append(formatTokenLine(tracker.getOutputTokens())).append("\n");
|
||||
|
||||
if (tracker.getCacheReadTokens() > 0) {
|
||||
sb.append(" ").append(AnsiStyle.bold("Cache read: ")).append(formatTokenLine(tracker.getCacheReadTokens())).append("\n");
|
||||
}
|
||||
if (tracker.getCacheCreationTokens() > 0) {
|
||||
sb.append(" ").append(AnsiStyle.bold("Cache create: ")).append(formatTokenLine(tracker.getCacheCreationTokens())).append("\n");
|
||||
}
|
||||
|
||||
sb.append(" ").append("─".repeat(30)).append("\n");
|
||||
sb.append(" ").append(AnsiStyle.bold("Total: ")).append(TokenTracker.formatTokens(tracker.getTotalTokens())).append(" tokens\n");
|
||||
sb.append(" ").append(AnsiStyle.bold("Est. Cost: ")).append(AnsiStyle.green("$" + String.format("%.4f", tracker.estimateCost()))).append("\n");
|
||||
|
||||
if (tracker.getApiCallCount() == 0) {
|
||||
sb.append("\n").append(AnsiStyle.dim(" No API calls yet. Start a conversation to see usage."));
|
||||
}
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
private String formatTokenLine(long tokens) {
|
||||
return " " + TokenTracker.formatTokens(tokens) + AnsiStyle.dim(" (" + tokens + ")");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
package com.claudecode.command.impl;
|
||||
|
||||
import com.claudecode.command.CommandContext;
|
||||
import com.claudecode.command.SlashCommand;
|
||||
import com.claudecode.console.AnsiStyle;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* /init 命令 —— 初始化项目 CLAUDE.md。
|
||||
* <p>
|
||||
* 对应 claude-code/src/commands/init.ts。
|
||||
* 检测项目类型并生成 CLAUDE.md 模板文件。
|
||||
*/
|
||||
public class InitCommand implements SlashCommand {
|
||||
|
||||
@Override
|
||||
public String name() {
|
||||
return "init";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String description() {
|
||||
return "Initialize CLAUDE.md for the current project";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String execute(String args, CommandContext context) {
|
||||
Path projectDir = Path.of(System.getProperty("user.dir"));
|
||||
Path claudeMdPath = projectDir.resolve("CLAUDE.md");
|
||||
|
||||
// 检查是否已存在
|
||||
if (Files.exists(claudeMdPath)) {
|
||||
return AnsiStyle.yellow(" ⚠ CLAUDE.md already exists at: " + claudeMdPath) + "\n"
|
||||
+ AnsiStyle.dim(" Use a text editor to modify it, or delete and re-run /init.");
|
||||
}
|
||||
|
||||
// 检测项目类型
|
||||
ProjectType type = detectProjectType(projectDir);
|
||||
|
||||
// 生成 CLAUDE.md 内容
|
||||
String content = generateClaudeMd(projectDir, type);
|
||||
|
||||
try {
|
||||
Files.writeString(claudeMdPath, content, StandardCharsets.UTF_8);
|
||||
|
||||
// 如果存在 .claude 目录,也创建 skills 目录
|
||||
Path claudeDir = projectDir.resolve(".claude");
|
||||
Path skillsDir = claudeDir.resolve("skills");
|
||||
if (!Files.exists(skillsDir)) {
|
||||
Files.createDirectories(skillsDir);
|
||||
}
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append(AnsiStyle.green(" ✅ Created CLAUDE.md")).append("\n");
|
||||
sb.append(AnsiStyle.dim(" Path: " + claudeMdPath)).append("\n");
|
||||
sb.append(AnsiStyle.dim(" Project type: " + type.displayName)).append("\n");
|
||||
sb.append(AnsiStyle.dim(" Skills dir: " + skillsDir)).append("\n\n");
|
||||
sb.append(AnsiStyle.dim(" Edit CLAUDE.md to customize instructions for the AI assistant."));
|
||||
return sb.toString();
|
||||
|
||||
} catch (IOException e) {
|
||||
return AnsiStyle.red(" ✗ Failed to create CLAUDE.md: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/** 检测项目类型 */
|
||||
private ProjectType detectProjectType(Path projectDir) {
|
||||
if (Files.exists(projectDir.resolve("pom.xml"))) return ProjectType.MAVEN;
|
||||
if (Files.exists(projectDir.resolve("build.gradle")) || Files.exists(projectDir.resolve("build.gradle.kts")))
|
||||
return ProjectType.GRADLE;
|
||||
if (Files.exists(projectDir.resolve("package.json"))) return ProjectType.NODE;
|
||||
if (Files.exists(projectDir.resolve("pyproject.toml")) || Files.exists(projectDir.resolve("setup.py")))
|
||||
return ProjectType.PYTHON;
|
||||
if (Files.exists(projectDir.resolve("Cargo.toml"))) return ProjectType.RUST;
|
||||
if (Files.exists(projectDir.resolve("go.mod"))) return ProjectType.GO;
|
||||
if (Files.exists(projectDir.resolve("Gemfile"))) return ProjectType.RUBY;
|
||||
return ProjectType.GENERIC;
|
||||
}
|
||||
|
||||
/** 生成 CLAUDE.md 内容 */
|
||||
private String generateClaudeMd(Path projectDir, ProjectType type) {
|
||||
String projectName = projectDir.getFileName().toString();
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("# ").append(projectName).append("\n\n");
|
||||
sb.append("## Project Overview\n\n");
|
||||
sb.append("<!-- Describe your project here -->\n\n");
|
||||
|
||||
sb.append("## Tech Stack\n\n");
|
||||
sb.append(type.techStackHint).append("\n\n");
|
||||
|
||||
sb.append("## Build & Run\n\n");
|
||||
sb.append("```bash\n");
|
||||
sb.append(type.buildCommand).append("\n");
|
||||
sb.append("```\n\n");
|
||||
|
||||
sb.append("## Test\n\n");
|
||||
sb.append("```bash\n");
|
||||
sb.append(type.testCommand).append("\n");
|
||||
sb.append("```\n\n");
|
||||
|
||||
sb.append("## Code Style\n\n");
|
||||
sb.append("- Follow existing patterns in the codebase\n");
|
||||
sb.append("- Write clear, descriptive variable names\n");
|
||||
sb.append("- Add comments for complex business logic\n\n");
|
||||
|
||||
sb.append("## Project Structure\n\n");
|
||||
sb.append("<!-- Describe key directories and files -->\n");
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
enum ProjectType {
|
||||
MAVEN("Maven/Java", "- Java (Maven)\n- Spring Boot (if applicable)", "mvn clean install", "mvn test"),
|
||||
GRADLE("Gradle/Java", "- Java/Kotlin (Gradle)\n- Spring Boot (if applicable)", "gradle build", "gradle test"),
|
||||
NODE("Node.js", "- Node.js\n- TypeScript/JavaScript", "npm install && npm run build", "npm test"),
|
||||
PYTHON("Python", "- Python 3\n- pip/poetry", "pip install -e .", "pytest"),
|
||||
RUST("Rust", "- Rust\n- Cargo", "cargo build", "cargo test"),
|
||||
GO("Go", "- Go", "go build ./...", "go test ./..."),
|
||||
RUBY("Ruby", "- Ruby\n- Bundler", "bundle install", "bundle exec rspec"),
|
||||
GENERIC("Generic", "<!-- List your tech stack -->", "# add build command", "# add test command");
|
||||
|
||||
final String displayName;
|
||||
final String techStackHint;
|
||||
final String buildCommand;
|
||||
final String testCommand;
|
||||
|
||||
ProjectType(String displayName, String techStackHint, String buildCommand, String testCommand) {
|
||||
this.displayName = displayName;
|
||||
this.techStackHint = techStackHint;
|
||||
this.buildCommand = buildCommand;
|
||||
this.testCommand = testCommand;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,12 +5,22 @@ import com.claudecode.command.SlashCommand;
|
||||
import com.claudecode.console.AnsiStyle;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* /model 命令 —— 显示或切换当前 AI 模型。
|
||||
* <p>
|
||||
* 支持查看当前模型信息和切换到其他模型。
|
||||
*/
|
||||
public class ModelCommand implements SlashCommand {
|
||||
|
||||
private static final Map<String, String> AVAILABLE_MODELS = Map.of(
|
||||
"sonnet", "claude-sonnet-4-20250514",
|
||||
"opus", "claude-opus-4-20250514",
|
||||
"haiku", "claude-haiku-4-20250514",
|
||||
"sonnet-3.5", "claude-3-5-sonnet-20241022"
|
||||
);
|
||||
|
||||
@Override
|
||||
public String name() {
|
||||
return "model";
|
||||
@@ -28,18 +38,46 @@ public class ModelCommand implements SlashCommand {
|
||||
|
||||
@Override
|
||||
public String execute(String args, CommandContext context) {
|
||||
// 当前只显示信息,后续可扩展为切换模型
|
||||
if (args == null || args.isBlank()) {
|
||||
return showCurrentModel(context);
|
||||
}
|
||||
|
||||
return switchModel(args.strip(), context);
|
||||
}
|
||||
|
||||
private String showCurrentModel(CommandContext context) {
|
||||
String currentModel = context.agentLoop().getTokenTracker().getModelName();
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("\n");
|
||||
sb.append(AnsiStyle.bold(" Model Configuration:\n\n"));
|
||||
sb.append(" Provider: ").append(AnsiStyle.cyan("Anthropic")).append("\n");
|
||||
sb.append(" Model: ").append(AnsiStyle.cyan(
|
||||
System.getenv().getOrDefault("AI_MODEL", "claude-sonnet-4-20250514"))).append("\n");
|
||||
sb.append(AnsiStyle.bold(" 🤖 Model Configuration\n"));
|
||||
sb.append(" ").append("─".repeat(40)).append("\n\n");
|
||||
sb.append(" Current: ").append(AnsiStyle.cyan(currentModel)).append("\n\n");
|
||||
|
||||
if (args != null && !args.isBlank()) {
|
||||
sb.append("\n");
|
||||
sb.append(AnsiStyle.yellow(" ⚠ Model switching not yet implemented. Set AI_MODEL env variable."));
|
||||
sb.append(AnsiStyle.dim(" Available shortcuts:\n"));
|
||||
for (var entry : AVAILABLE_MODELS.entrySet()) {
|
||||
String marker = entry.getValue().equals(currentModel) ? " ◀" : "";
|
||||
sb.append(AnsiStyle.dim(" " + entry.getKey() + " → " + entry.getValue() + marker)).append("\n");
|
||||
}
|
||||
sb.append("\n");
|
||||
sb.append(AnsiStyle.dim(" Usage: /model <name> (e.g., /model opus, /model claude-3-5-sonnet-20241022)"));
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
private String switchModel(String modelArg, CommandContext context) {
|
||||
// 支持快捷名称
|
||||
String resolvedModel = AVAILABLE_MODELS.getOrDefault(modelArg.toLowerCase(), modelArg);
|
||||
|
||||
String oldModel = context.agentLoop().getTokenTracker().getModelName();
|
||||
context.agentLoop().getTokenTracker().setModel(resolvedModel);
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append(AnsiStyle.green(" ✅ Model switched")).append("\n");
|
||||
sb.append(" From: ").append(AnsiStyle.dim(oldModel)).append("\n");
|
||||
sb.append(" To: ").append(AnsiStyle.cyan(resolvedModel)).append("\n");
|
||||
sb.append(AnsiStyle.dim(" Note: Changes take effect on next API call."));
|
||||
sb.append("\n").append(AnsiStyle.dim(" Note: Runtime model switch only updates pricing. Actual API model is set in application.yml."));
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
package com.claudecode.command.impl;
|
||||
|
||||
import com.claudecode.command.CommandContext;
|
||||
import com.claudecode.command.SlashCommand;
|
||||
import com.claudecode.console.AnsiStyle;
|
||||
import com.claudecode.core.TokenTracker;
|
||||
|
||||
import java.lang.management.ManagementFactory;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
|
||||
/**
|
||||
* /status 命令 —— 显示会话状态仪表板。
|
||||
* <p>
|
||||
* 展示当前模型、Token 用量、工具数、消息数、内存和运行时间等信息。
|
||||
*/
|
||||
public class StatusCommand implements SlashCommand {
|
||||
|
||||
private final Instant startTime = Instant.now();
|
||||
|
||||
@Override
|
||||
public String name() {
|
||||
return "status";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String description() {
|
||||
return "Show session status dashboard";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String execute(String args, CommandContext context) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("\n");
|
||||
sb.append(AnsiStyle.bold(" 📊 Session Status\n"));
|
||||
sb.append(" ").append("─".repeat(40)).append("\n\n");
|
||||
|
||||
// 模型信息
|
||||
TokenTracker tracker = context.agentLoop().getTokenTracker();
|
||||
sb.append(" ").append(AnsiStyle.bold("Model: ")).append(AnsiStyle.cyan(tracker.getModelName())).append("\n");
|
||||
|
||||
// Token 使用
|
||||
sb.append(" ").append(AnsiStyle.bold("Tokens: "))
|
||||
.append("↑ ").append(TokenTracker.formatTokens(tracker.getInputTokens()))
|
||||
.append(" input, ↓ ").append(TokenTracker.formatTokens(tracker.getOutputTokens()))
|
||||
.append(" output")
|
||||
.append(AnsiStyle.dim(" ($" + String.format("%.4f", tracker.estimateCost()) + ")"))
|
||||
.append("\n");
|
||||
|
||||
// API 调用次数
|
||||
sb.append(" ").append(AnsiStyle.bold("API Calls:")).append(" ").append(tracker.getApiCallCount()).append("\n");
|
||||
|
||||
// 消息历史
|
||||
int msgCount = context.agentLoop().getMessageHistory().size();
|
||||
sb.append(" ").append(AnsiStyle.bold("Messages: ")).append(msgCount).append("\n");
|
||||
|
||||
// 工具数
|
||||
sb.append(" ").append(AnsiStyle.bold("Tools: ")).append(context.toolRegistry().size()).append(" registered\n");
|
||||
|
||||
// 工作目录
|
||||
sb.append(" ").append(AnsiStyle.bold("Work Dir: ")).append(AnsiStyle.dim(System.getProperty("user.dir"))).append("\n");
|
||||
|
||||
// JVM 内存
|
||||
Runtime rt = Runtime.getRuntime();
|
||||
long usedMB = (rt.totalMemory() - rt.freeMemory()) / (1024 * 1024);
|
||||
long maxMB = rt.maxMemory() / (1024 * 1024);
|
||||
sb.append(" ").append(AnsiStyle.bold("Memory: ")).append(usedMB).append("MB / ").append(maxMB).append("MB\n");
|
||||
|
||||
// 运行时间
|
||||
Duration uptime = Duration.between(startTime, Instant.now());
|
||||
sb.append(" ").append(AnsiStyle.bold("Uptime: ")).append(formatDuration(uptime)).append("\n");
|
||||
|
||||
// Java 版本
|
||||
sb.append(" ").append(AnsiStyle.bold("JDK: ")).append(System.getProperty("java.version")).append("\n");
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
private String formatDuration(Duration d) {
|
||||
long hours = d.toHours();
|
||||
long minutes = d.toMinutesPart();
|
||||
long seconds = d.toSecondsPart();
|
||||
if (hours > 0) return String.format("%dh %dm %ds", hours, minutes, seconds);
|
||||
if (minutes > 0) return String.format("%dm %ds", minutes, seconds);
|
||||
return String.format("%ds", seconds);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user