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:
@@ -0,0 +1,178 @@
|
||||
package com.claudecode.context;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* Skills 技能加载器 —— 对应 claude-code/src/skills/ 模块。
|
||||
* <p>
|
||||
* 从多个来源扫描和加载 .md 格式的技能文件:
|
||||
* <ol>
|
||||
* <li>用户级: ~/.claude/skills/</li>
|
||||
* <li>项目级: ./.claude/skills/</li>
|
||||
* <li>命令目录: ./.claude/commands/ (自动转换为技能)</li>
|
||||
* </ol>
|
||||
* <p>
|
||||
* 每个技能文件支持 YAML frontmatter 元数据:
|
||||
* <pre>
|
||||
* ---
|
||||
* name: verify-tests
|
||||
* description: Run all tests after changes
|
||||
* whenToUse: After modifying code
|
||||
* ---
|
||||
* [技能内容 markdown]
|
||||
* </pre>
|
||||
*/
|
||||
public class SkillLoader {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(SkillLoader.class);
|
||||
|
||||
private final Path projectDir;
|
||||
private final List<Skill> skills = new ArrayList<>();
|
||||
|
||||
public SkillLoader(Path projectDir) {
|
||||
this.projectDir = projectDir;
|
||||
}
|
||||
|
||||
/**
|
||||
* 扫描并加载所有技能文件
|
||||
*/
|
||||
public List<Skill> loadAll() {
|
||||
skills.clear();
|
||||
|
||||
// 1. 用户级技能
|
||||
Path userSkillsDir = Path.of(System.getProperty("user.home"), ".claude", "skills");
|
||||
loadFromDirectory(userSkillsDir, "user");
|
||||
|
||||
// 2. 项目级技能
|
||||
Path projectSkillsDir = projectDir.resolve(".claude").resolve("skills");
|
||||
loadFromDirectory(projectSkillsDir, "project");
|
||||
|
||||
// 3. 命令目录(自动转换为技能)
|
||||
Path commandsDir = projectDir.resolve(".claude").resolve("commands");
|
||||
loadFromDirectory(commandsDir, "command");
|
||||
|
||||
log.debug("共加载 {} 个技能", skills.size());
|
||||
return Collections.unmodifiableList(skills);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从指定目录加载 .md 技能文件
|
||||
*/
|
||||
private void loadFromDirectory(Path dir, String source) {
|
||||
if (!Files.isDirectory(dir)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try (var stream = Files.list(dir)) {
|
||||
stream.filter(p -> p.toString().endsWith(".md"))
|
||||
.sorted()
|
||||
.forEach(p -> {
|
||||
try {
|
||||
Skill skill = parseSkillFile(p, source);
|
||||
skills.add(skill);
|
||||
log.debug("加载技能: {} [{}] from {}", skill.name(), source, p.getFileName());
|
||||
} catch (IOException e) {
|
||||
log.warn("加载技能文件失败: {}: {}", p, e.getMessage());
|
||||
}
|
||||
});
|
||||
} catch (IOException e) {
|
||||
log.debug("扫描技能目录失败: {}: {}", dir, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析单个技能文件,提取 frontmatter 和内容
|
||||
*/
|
||||
private Skill parseSkillFile(Path path, String source) throws IOException {
|
||||
String raw = Files.readString(path, StandardCharsets.UTF_8).strip();
|
||||
String fileName = path.getFileName().toString().replace(".md", "");
|
||||
|
||||
// 尝试提取 YAML frontmatter
|
||||
String name = fileName;
|
||||
String description = "";
|
||||
String whenToUse = "";
|
||||
String content = raw;
|
||||
|
||||
if (raw.startsWith("---")) {
|
||||
int endIdx = raw.indexOf("---", 3);
|
||||
if (endIdx > 0) {
|
||||
String frontmatter = raw.substring(3, endIdx).strip();
|
||||
content = raw.substring(endIdx + 3).strip();
|
||||
|
||||
// 简单的 YAML 解析(key: value 格式)
|
||||
for (String line : frontmatter.split("\n")) {
|
||||
line = line.strip();
|
||||
int colonIdx = line.indexOf(':');
|
||||
if (colonIdx > 0) {
|
||||
String key = line.substring(0, colonIdx).strip();
|
||||
String value = line.substring(colonIdx + 1).strip();
|
||||
// 去掉引号
|
||||
if (value.startsWith("\"") && value.endsWith("\"")) {
|
||||
value = value.substring(1, value.length() - 1);
|
||||
}
|
||||
switch (key) {
|
||||
case "name" -> name = value;
|
||||
case "description" -> description = value;
|
||||
case "whenToUse" -> whenToUse = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return new Skill(name, description, whenToUse, content, source, path);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取已加载的技能列表
|
||||
*/
|
||||
public List<Skill> getSkills() {
|
||||
return Collections.unmodifiableList(skills);
|
||||
}
|
||||
|
||||
/**
|
||||
* 按名称查找技能
|
||||
*/
|
||||
public Optional<Skill> findByName(String name) {
|
||||
return skills.stream()
|
||||
.filter(s -> s.name().equalsIgnoreCase(name))
|
||||
.findFirst();
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建技能上下文摘要(注入系统提示词)
|
||||
*/
|
||||
public String buildSkillsSummary() {
|
||||
if (skills.isEmpty()) {
|
||||
return "";
|
||||
}
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("# Available Skills\n\n");
|
||||
for (Skill skill : skills) {
|
||||
sb.append("- **").append(skill.name()).append("**");
|
||||
if (!skill.description().isEmpty()) {
|
||||
sb.append(": ").append(skill.description());
|
||||
}
|
||||
if (!skill.whenToUse().isEmpty()) {
|
||||
sb.append(" (use when: ").append(skill.whenToUse()).append(")");
|
||||
}
|
||||
sb.append("\n");
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* 技能数据记录
|
||||
*/
|
||||
public record Skill(String name, String description, String whenToUse,
|
||||
String content, String source, Path filePath) {
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user