fix(skill): sync workspace when builtin skill is updated or version bumped

This commit is contained in:
matevip 2026-04-06 17:28:39 +08:00
parent 3940cfd1ce
commit 9d727c607a
5 changed files with 106 additions and 6 deletions

View File

@ -114,11 +114,19 @@ public class SkillController {
return R.ok(skillRuntimeService.resolveAllSkillsStatus());
}
@Operation(summary = "刷新 active skills 缓存")
@Operation(summary = "刷新 active skills 缓存resync=true 时同步内置技能到 workspace")
@PostMapping("/runtime/refresh")
public R<Map<String, Object>> refreshRuntime() {
public R<Map<String, Object>> refreshRuntime(
@RequestParam(defaultValue = "false") boolean resync) {
List<String> resynced = List.of();
if (resync) {
resynced = workspaceManager.syncBundledSkills();
}
List<ResolvedSkill> skills = skillRuntimeService.refreshActiveSkills();
return R.ok(Map.of("count", skills.size(), "message", "Active skills refreshed"));
return R.ok(Map.of(
"count", skills.size(),
"message", resync ? "Active skills refreshed with workspace resync" : "Active skills refreshed",
"resynced", resynced));
}
// ==================== Workspace API ====================

View File

@ -153,6 +153,9 @@ public class SkillService {
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();

View File

@ -13,7 +13,10 @@ import java.util.List;
* Skill 工作区启动初始化
* <p>
* 1. 确保 workspace root 目录存在
* 2. classpath 下预置技能同步到 workspace仅首次不覆盖
* 2. classpath 下预置技能同步到 workspace
* - 首次创建并同步
* - 后续比对 SKILL.md frontmatter 中的 version 字段
* bundled version 更高时归档旧版本并覆盖升级
* <p>
* Order(195) DatabaseBootstrapRunner(200) 之前执行
*

View File

@ -346,11 +346,28 @@ public class SkillWorkspaceManager {
Path targetDir = resolveConventionPath(skillName);
if (Files.exists(targetDir)) {
log.debug("Bundled skill '{}' already exists at {}, skipping", skillName, targetDir);
// 版本比对classpath version > workspace version 时覆盖升级
String bundledVersion = parseFrontmatterVersion(skillMdResource);
Path workspaceMd = targetDir.resolve("SKILL.md");
String workspaceVersion = Files.exists(workspaceMd)
? parseFrontmatterVersionFromPath(workspaceMd) : null;
if (bundledVersion != null && isNewerVersion(bundledVersion, workspaceVersion)) {
log.info("Bundled skill '{}' version {} > workspace version {}, upgrading",
skillName, bundledVersion, workspaceVersion);
archiveWorkspace(skillName);
syncSingleBundledSkill(resolver, bundledPath, skillName, targetDir);
synced.add(skillName);
eventPublisher.publishEvent(
new SkillWorkspaceEvent(skillName, SkillWorkspaceEvent.Type.CREATED, targetDir));
} else {
log.debug("Bundled skill '{}' workspace version is current (bundled={}, workspace={}), skipping",
skillName, bundledVersion, workspaceVersion);
}
continue;
}
// 同步该技能目录下的所有文件
// 目录不存在首次同步
syncSingleBundledSkill(resolver, bundledPath, skillName, targetDir);
synced.add(skillName);
log.info("Synced bundled skill '{}' → {}", skillName, targetDir);
@ -433,6 +450,74 @@ public class SkillWorkspaceManager {
}
}
// ==================== 版本比对 ====================
private static final java.util.regex.Pattern VERSION_PATTERN =
java.util.regex.Pattern.compile("^version:\\s*[\"']?([^\"'\\s]+)[\"']?", java.util.regex.Pattern.MULTILINE);
/**
* classpath Resource (SKILL.md) frontmatter 中提取 version 字段
*/
private String parseFrontmatterVersion(Resource resource) {
try (InputStream is = resource.getInputStream()) {
String content = new String(is.readAllBytes(), java.nio.charset.StandardCharsets.UTF_8);
return extractVersionFromContent(content);
} catch (IOException e) {
return null;
}
}
/**
* 从文件路径 (SKILL.md) frontmatter 中提取 version 字段
*/
private String parseFrontmatterVersionFromPath(Path path) {
try {
String content = Files.readString(path);
return extractVersionFromContent(content);
} catch (IOException e) {
return null;
}
}
private String extractVersionFromContent(String content) {
// 只读 frontmatter 部分--- 之间
if (!content.startsWith("---")) return null;
int endIdx = content.indexOf("---", 3);
if (endIdx < 0) return null;
String frontmatter = content.substring(0, endIdx);
var matcher = VERSION_PATTERN.matcher(frontmatter);
return matcher.find() ? matcher.group(1) : null;
}
/**
* 语义版本比较newer > older 返回 true
* <p>
* 1.1.0 > 1.0.0, 2.0.0 > 1.9.9, older=null 视为 0.0.0
*/
static boolean isNewerVersion(String newer, String older) {
if (newer == null) return false;
if (older == null) return true;
String[] nParts = newer.split("\\.");
String[] oParts = older.split("\\.");
int len = Math.max(nParts.length, oParts.length);
for (int i = 0; i < len; i++) {
int n = i < nParts.length ? parseSegment(nParts[i]) : 0;
int o = i < oParts.length ? parseSegment(oParts[i]) : 0;
if (n > o) return true;
if (n < o) return false;
}
return false; // equal
}
private static int parseSegment(String s) {
try {
return Integer.parseInt(s.trim());
} catch (NumberFormatException e) {
return 0;
}
}
// ==================== 工具方法 ====================
private String sanitizeName(String name) {

View File

@ -1,5 +1,6 @@
---
name: sql_query
version: "1.1.0"
description: "当用户提出数据查询需求(如'查数'、'查一下订单量'、'有多少用户'、'帮我跑个SQL'等),使用数据源工具发现表结构,生成并执行只读 SQL 查询。"
dependencies:
tools: