package vip.mate.skill.service;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import vip.mate.exception.MateClawException;
import vip.mate.skill.model.SkillEntity;
import vip.mate.skill.repository.SkillMapper;
import vip.mate.skill.workspace.SkillWorkspaceManager;
import vip.mate.skill.workspace.SkillWorkspaceProperties;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
/**
* 技能业务服务
*
* 负责技能的 CRUD 管理、启用/禁用控制,以及与 Agent 运行时的集成。
* Skill 在 MateClaw 中的定位是"可扩展的能力模块",分为三种类型:
*
* - builtin — 系统内置技能(不可删除),通常对应预定义的 systemPrompt 片段
* - mcp — 通过 MCP 协议连接外部工具服务器
* - dynamic — 用户自定义的动态技能(可包含脚本或配置)
*
*
* @author MateClaw Team
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class SkillService {
private final SkillMapper skillMapper;
private final SkillWorkspaceManager workspaceManager;
private final SkillWorkspaceProperties workspaceProperties;
private vip.mate.skill.runtime.SkillRuntimeService runtimeService;
/**
* 延迟注入 SkillRuntimeService 避免循环依赖
*/
public void setRuntimeService(vip.mate.skill.runtime.SkillRuntimeService runtimeService) {
this.runtimeService = runtimeService;
}
// ==================== CRUD ====================
/**
* 获取所有技能列表(管理页面使用)
* 排序:内置优先,然后按创建时间倒序
*/
public List listSkills() {
return skillMapper.selectList(new LambdaQueryWrapper()
.orderByDesc(SkillEntity::getBuiltin)
.orderByDesc(SkillEntity::getCreateTime));
}
/**
* 获取已启用的技能列表(Agent 运行时使用)
*/
public List listEnabledSkills() {
return skillMapper.selectList(new LambdaQueryWrapper()
.eq(SkillEntity::getEnabled, true)
.orderByAsc(SkillEntity::getName));
}
/**
* 按类型获取技能列表
*/
public List listSkillsByType(String skillType) {
return skillMapper.selectList(new LambdaQueryWrapper()
.eq(SkillEntity::getSkillType, skillType)
.orderByDesc(SkillEntity::getCreateTime));
}
/**
* 获取技能详情
*/
public SkillEntity getSkill(Long id) {
SkillEntity skill = skillMapper.selectById(id);
if (skill == null) {
throw new MateClawException("err.skill.not_found", "技能不存在: " + id);
}
return skill;
}
/**
* 创建技能
* 默认类型为 dynamic(用户自定义),非内置
*/
public SkillEntity createSkill(SkillEntity skill) {
// 验证名称不为空
if (skill.getName() == null || skill.getName().isBlank()) {
throw new MateClawException("err.skill.name_required", "技能名称不能为空");
}
// 检查名称唯一性
Long count = skillMapper.selectCount(new LambdaQueryWrapper()
.eq(SkillEntity::getName, skill.getName()));
if (count > 0) {
throw new MateClawException("err.skill.name_exists", "技能名称已存在: " + skill.getName());
}
// 设置默认值
skill.setBuiltin(false);
if (skill.getEnabled() == null) {
skill.setEnabled(true);
}
// 前端只识别 builtin/mcp/dynamic,用户新建默认为 dynamic
if (skill.getSkillType() == null || skill.getSkillType().isBlank()) {
skill.setSkillType("dynamic");
}
// 默认版本号
if (skill.getVersion() == null || skill.getVersion().isBlank()) {
skill.setVersion("1.0.0");
}
skillMapper.insert(skill);
log.info("Created skill: {} (type={})", skill.getName(), skill.getSkillType());
// 自动初始化工作区目录
if (workspaceProperties.isAutoInit() && !hasExplicitSkillDir(skill)) {
workspaceManager.initWorkspace(skill.getName(), skill.getSkillContent());
}
// 刷新 runtime cache
if (runtimeService != null) {
runtimeService.refreshActiveSkills();
}
return skill;
}
/**
* 更新技能
* 内置技能只允许修改 enabled、configJson、description
*/
public SkillEntity updateSkill(SkillEntity skill) {
SkillEntity existing = getSkill(skill.getId());
if (Boolean.TRUE.equals(existing.getBuiltin())) {
// 内置技能:只允许修改有限字段
existing.setEnabled(skill.getEnabled() != null ? skill.getEnabled() : existing.getEnabled());
existing.setConfigJson(skill.getConfigJson());
existing.setDescription(skill.getDescription() != null ? skill.getDescription() : existing.getDescription());
// builtin skill 也允许更新 skillContent(用于维护 fallback 内容)
if (skill.getSkillContent() != null) {
existing.setSkillContent(skill.getSkillContent());
}
skillMapper.updateById(existing);
log.info("Updated builtin skill (limited): {}", existing.getName());
// builtin skill 也同步 workspace SKILL.md
syncSkillContentToWorkspace(existing);
// 刷新 runtime cache
if (runtimeService != null) {
runtimeService.refreshActiveSkills();
}
return existing;
}
// 非内置技能:允许修改所有字段,但不允许改为 builtin
skill.setBuiltin(false);
skillMapper.updateById(skill);
log.info("Updated skill: {}", skill.getName());
// 若 skillContent 变更且约定工作区存在,同步 SKILL.md
syncSkillContentToWorkspace(skill);
// 刷新 runtime cache
if (runtimeService != null) {
runtimeService.refreshActiveSkills();
}
return skill;
}
/**
* 删除技能
* 内置技能不可删除
*/
public void deleteSkill(Long id) {
SkillEntity skill = getSkill(id);
if (Boolean.TRUE.equals(skill.getBuiltin())) {
throw new MateClawException("err.skill.builtin_readonly", "内置技能不可删除: " + skill.getName());
}
skillMapper.deleteById(id);
log.info("Deleted skill: {}", skill.getName());
// 归档工作区目录
if ("archive".equals(workspaceProperties.getDeletePolicy())) {
workspaceManager.archiveWorkspace(skill.getName());
}
// 刷新 runtime cache
if (runtimeService != null) {
runtimeService.refreshActiveSkills();
}
}
/**
* 启用/禁用技能
*/
public SkillEntity toggleSkill(Long id, boolean enabled) {
SkillEntity skill = getSkill(id);
skill.setEnabled(enabled);
skillMapper.updateById(skill);
log.info("Skill {} {}", skill.getName(), enabled ? "enabled" : "disabled");
// 刷新 runtime cache
if (runtimeService != null) {
runtimeService.refreshActiveSkills();
}
return skill;
}
// ==================== Agent 运行时集成 ====================
/**
* Token 预算上限(字符数近似值,1 token ≈ 2 个中文字 / 4 个英文字符)
* 默认 6000 字符 ≈ ~2000 tokens,为对话上下文预留足够空间
*/
private static final int DEFAULT_SKILL_PROMPT_BUDGET = 6000;
/**
* 构建技能 Prompt 增强片段(带 Token 预算控制)
*
* 优化策略(对比旧版全量注入):
*
* - 分层注入:先注入「技能目录」(名称+描述),再按预算注入「技能详情」(skillContent)
* - Token 预算控制:总字符数超过预算时,截断详情部分,只保留目录
* - 优先级:builtin 技能优先注入详情,其次按名称排序
* - 不再将 sourceCode 全量注入(旧版会爆 token),改用 skillContent(SKILL.md 协议)
*
*
* @return systemPrompt 增强片段,可直接拼接到 Agent 的 systemPrompt 末尾
*/
public String buildSkillPromptEnhancement() {
return buildSkillPromptEnhancement(DEFAULT_SKILL_PROMPT_BUDGET);
}
/**
* 构建技能 Prompt 增强片段(可指定 Token 预算)
*
* @param charBudget 最大字符预算(超出时自动截断详情)
*/
public String buildSkillPromptEnhancement(int charBudget) {
List enabledSkills = listEnabledSkills();
if (enabledSkills.isEmpty()) {
return "";
}
// --- 第一层:技能目录(始终注入,消耗很少的 token) ---
StringBuilder catalog = new StringBuilder();
catalog.append("\n\n## Available Skills\n");
catalog.append("以下技能已启用,你可以在对话中根据用户需求灵活运用:\n\n");
for (SkillEntity skill : enabledSkills) {
catalog.append("- **").append(skill.getName()).append("**");
if (skill.getIcon() != null && !skill.getIcon().isBlank()) {
catalog.append(" ").append(skill.getIcon());
}
if (skill.getDescription() != null && !skill.getDescription().isBlank()) {
// 截取描述前 200 字符作为摘要
String desc = skill.getDescription();
if (desc.length() > 200) {
desc = desc.substring(0, 200) + "...";
}
catalog.append(" — ").append(desc);
}
catalog.append("\n");
}
int remaining = charBudget - catalog.length();
if (remaining <= 200) {
// 预算不足,只返回目录
return catalog.toString();
}
// --- 第二层:技能详情(按优先级注入,受预算控制) ---
// 排序优先级:builtin > 其他,然后按名称
List sorted = enabledSkills.stream()
.sorted((a, b) -> {
int builtinCmp = Boolean.compare(
Boolean.TRUE.equals(b.getBuiltin()),
Boolean.TRUE.equals(a.getBuiltin()));
return builtinCmp != 0 ? builtinCmp : a.getName().compareTo(b.getName());
})
.toList();
StringBuilder details = new StringBuilder();
details.append("\n### Skill Details\n");
int detailLen = details.length();
for (SkillEntity skill : sorted) {
String content = resolveSkillContent(skill);
if (content == null || content.isBlank()) {
continue;
}
// 每个技能的详情块
StringBuilder block = new StringBuilder();
block.append("\n#### ").append(skill.getName()).append("\n");
block.append(content).append("\n");
// 检查预算
if (detailLen + block.length() > remaining) {
// 预算不足,尝试截断当前 skill 内容
int maxContentLen = remaining - detailLen - 60; // 留 60 字符给标题和截断提示
if (maxContentLen > 200) {
block.setLength(0);
block.append("\n#### ").append(skill.getName()).append("\n");
block.append(content, 0, Math.min(content.length(), maxContentLen));
block.append("\n...(truncated)\n");
details.append(block);
}
break; // 预算用尽,停止注入
}
details.append(block);
detailLen += block.length();
}
return catalog.toString() + details;
}
/**
* 获取技能的可注入内容
*
* 优先级:skillContent(SKILL.md 协议) > description
* 不再使用 sourceCode(可能包含大量代码,容易爆 token)
*/
private String resolveSkillContent(SkillEntity skill) {
// 优先使用 SKILL.md 内容(执行协议)
if (skill.getSkillContent() != null && !skill.getSkillContent().isBlank()) {
return skill.getSkillContent();
}
// 回退到 description(兼容旧数据)
return skill.getDescription();
}
/**
* 获取已启用技能的摘要信息(用于 Agent 状态展示)
*/
public Map> getEnabledSkillSummary() {
return listEnabledSkills().stream()
.collect(Collectors.groupingBy(
SkillEntity::getSkillType,
Collectors.mapping(SkillEntity::getName, Collectors.toList())
));
}
// ==================== Workspace 集成辅助方法 ====================
/**
* 同步 skillContent 到工作区 SKILL.md
*/
private void syncSkillContentToWorkspace(SkillEntity skill) {
if (skill.getSkillContent() == null || skill.getSkillContent().isBlank()) {
return;
}
if (workspaceManager.conventionWorkspaceExists(skill.getName())) {
Path workspaceDir = workspaceManager.resolveConventionPath(skill.getName());
Path skillMd = workspaceDir.resolve("SKILL.md");
try {
Files.writeString(skillMd, skill.getSkillContent());
log.debug("Synced skillContent to workspace SKILL.md: {}", skillMd);
} catch (Exception e) {
log.warn("Failed to sync skillContent to workspace: {}", e.getMessage());
}
}
}
/**
* 检查 skill 是否有显式配置的 skillDir
*/
private boolean hasExplicitSkillDir(SkillEntity skill) {
String configJson = skill.getConfigJson();
if (configJson == null || configJson.isBlank()) {
return false;
}
return configJson.contains("skillDir") || configJson.contains("path") || configJson.contains("directory");
}
}