| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269 |
- package com.agent.management.service.impl;
- import com.agent.management.common.exception.BusinessException;
- import com.agent.management.config.SkillProperties;
- import com.agent.management.engine.hermes.HermesSkillSyncService;
- import com.agent.management.model.vo.SkillVersionVO;
- import com.agent.management.service.SkillVersionService;
- import com.fasterxml.jackson.databind.ObjectMapper;
- import com.fasterxml.jackson.databind.SerializationFeature;
- import lombok.RequiredArgsConstructor;
- import lombok.extern.slf4j.Slf4j;
- import org.apache.commons.io.FileUtils;
- import org.springframework.stereotype.Service;
- import java.io.File;
- import java.io.IOException;
- import java.nio.charset.StandardCharsets;
- import java.nio.file.*;
- import java.time.LocalDateTime;
- import java.util.*;
- import java.util.stream.Stream;
- @Slf4j
- @Service
- @RequiredArgsConstructor
- public class SkillVersionServiceImpl implements SkillVersionService {
- private final SkillProperties skillProperties;
- private final HermesSkillSyncService hermesSkillSyncService;
- private static final String VERSIONS_DIR = "_versions";
- private static final String VERSION_META_FILE = "_version.json";
- private static final ObjectMapper jsonMapper = new ObjectMapper()
- .enable(SerializationFeature.INDENT_OUTPUT);
- // ==================== 公有方法 ====================
- @Override
- public int createSnapshot(String folderName, String message) {
- Path skillDir = getSkillDir(folderName);
- Path versionsDir = skillDir.resolve(VERSIONS_DIR);
- VersionMeta meta = loadMeta(skillDir);
- // 版本号始终取历史最大值 + 1,保证单调递增(避免回滚后再保存版本号冲突)
- int maxVersion = meta.versions.stream()
- .mapToInt(e -> e.number)
- .max().orElse(0);
- int newVersion = maxVersion + 1;
- // 创建 v{n} 目录并复制工作区文件
- Path snapshotDir = versionsDir.resolve("v" + newVersion);
- copyWorkingDir(skillDir, snapshotDir);
- // 更新元数据
- VersionEntry entry = new VersionEntry();
- entry.number = newVersion;
- entry.createdAt = LocalDateTime.now().toString();
- entry.message = message != null ? message : "";
- meta.versions.add(entry);
- meta.currentVersion = newVersion;
- saveMeta(skillDir, meta);
- log.info("Skill [{}] 创建快照 v{}", folderName, newVersion);
- return newVersion;
- }
- @Override
- public int getCurrentVersion(String folderName) {
- Path skillDir = getSkillDir(folderName);
- return loadMeta(skillDir).currentVersion;
- }
- @Override
- public List<SkillVersionVO> listVersions(String folderName) {
- Path skillDir = getSkillDir(folderName);
- VersionMeta meta = loadMeta(skillDir);
- List<SkillVersionVO> result = new ArrayList<>();
- for (VersionEntry entry : meta.versions) {
- SkillVersionVO vo = new SkillVersionVO();
- vo.setNumber(entry.number);
- vo.setCreatedAt(LocalDateTime.parse(entry.createdAt));
- vo.setMessage(entry.message);
- vo.setCurrent(entry.number == meta.currentVersion);
- result.add(vo);
- }
- return result;
- }
- @Override
- public String getVersionContent(String folderName, int version) {
- Path skillDir = getSkillDir(folderName);
- validateVersionExists(skillDir, version);
- Path skillMd = skillDir.resolve(VERSIONS_DIR)
- .resolve("v" + version)
- .resolve("SKILL.md");
- if (!Files.exists(skillMd)) {
- throw new BusinessException(404, "版本 v" + version + " 无 SKILL.md");
- }
- try {
- return Files.readString(skillMd, StandardCharsets.UTF_8);
- } catch (IOException e) {
- throw new BusinessException("读取版本内容失败");
- }
- }
- @Override
- public void rollback(String folderName, int version) {
- Path skillDir = getSkillDir(folderName);
- VersionMeta meta = loadMeta(skillDir);
- validateVersionExists(skillDir, version);
- // 将快照内容恢复到工作区(排除 _versions 和 _version.json)
- Path snapshotDir = skillDir.resolve(VERSIONS_DIR).resolve("v" + version);
- restoreToWorkingDir(snapshotDir, skillDir);
- // 仅更新 currentVersion,不创建新快照
- meta.currentVersion = version;
- saveMeta(skillDir, meta);
- log.info("Skill [{}] 回滚到 v{}", folderName, version);
- hermesSkillSyncService.syncAfterWrite();
- }
- @Override
- public void updateVersionMessage(String folderName, int version, String message) {
- Path skillDir = getSkillDir(folderName);
- VersionMeta meta = loadMeta(skillDir);
- VersionEntry entry = meta.versions.stream()
- .filter(e -> e.number == version)
- .findFirst()
- .orElseThrow(() -> new BusinessException(404, "版本不存在: v" + version));
- entry.message = message != null ? message : "";
- saveMeta(skillDir, meta);
- }
- // ==================== 私有方法 ====================
- private Path getSkillDir(String folderName) {
- Path dir = Paths.get(skillProperties.getBasePath()).toAbsolutePath().resolve(folderName);
- if (!Files.exists(dir) || !Files.isDirectory(dir)) {
- throw new BusinessException(404, "Skill 不存在: " + folderName);
- }
- return dir;
- }
- /**
- * 复制工作区到快照目录(排除 _versions/ 和 _version.json)
- */
- private void copyWorkingDir(Path skillDir, Path snapshotDir) {
- try {
- Files.createDirectories(snapshotDir);
- try (Stream<Path> paths = Files.list(skillDir)) {
- paths.filter(p -> {
- String name = p.getFileName().toString();
- return !name.equals(VERSIONS_DIR) && !name.equals(VERSION_META_FILE);
- }).forEach(p -> {
- Path target = snapshotDir.resolve(p.getFileName().toString());
- try {
- if (Files.isDirectory(p)) {
- FileUtils.copyDirectory(p.toFile(), target.toFile());
- } else {
- Files.copy(p, target, StandardCopyOption.REPLACE_EXISTING);
- }
- } catch (IOException e) {
- log.error("复制文件失败: {} -> {}", p, target, e);
- throw new BusinessException("创建快照失败");
- }
- });
- }
- } catch (IOException e) {
- throw new BusinessException("创建快照失败");
- }
- }
- /**
- * 将快照内容恢复到工作区(先清空工作区文件,再从快照复制)
- */
- private void restoreToWorkingDir(Path snapshotDir, Path skillDir) {
- try {
- // 删除工作区中现有的业务文件(排除 _versions/ 和 _version.json)
- try (Stream<Path> paths = Files.list(skillDir)) {
- paths.filter(p -> {
- String name = p.getFileName().toString();
- return !name.equals(VERSIONS_DIR) && !name.equals(VERSION_META_FILE);
- }).forEach(p -> {
- try {
- if (Files.isDirectory(p)) {
- FileUtils.deleteDirectory(p.toFile());
- } else {
- Files.delete(p);
- }
- } catch (IOException e) {
- log.error("删除工作区文件失败: {}", p, e);
- }
- });
- }
- // 从快照复制到工作区
- try (Stream<Path> paths = Files.list(snapshotDir)) {
- paths.forEach(p -> {
- Path target = skillDir.resolve(p.getFileName().toString());
- try {
- if (Files.isDirectory(p)) {
- FileUtils.copyDirectory(p.toFile(), target.toFile());
- } else {
- Files.copy(p, target, StandardCopyOption.REPLACE_EXISTING);
- }
- } catch (IOException e) {
- log.error("恢复文件失败: {} -> {}", p, target, e);
- throw new BusinessException("回滚失败");
- }
- });
- }
- } catch (IOException e) {
- throw new BusinessException("回滚失败");
- }
- }
- private void validateVersionExists(Path skillDir, int version) {
- Path vDir = skillDir.resolve(VERSIONS_DIR).resolve("v" + version);
- if (!Files.exists(vDir)) {
- throw new BusinessException(404, "版本不存在: v" + version);
- }
- }
- // ==================== 元数据读写 ====================
- private VersionMeta loadMeta(Path skillDir) {
- Path metaFile = skillDir.resolve(VERSION_META_FILE);
- if (!Files.exists(metaFile)) {
- return new VersionMeta();
- }
- try {
- return jsonMapper.readValue(metaFile.toFile(), VersionMeta.class);
- } catch (IOException e) {
- log.warn("解析版本元数据失败,使用默认值: {}", metaFile, e);
- return new VersionMeta();
- }
- }
- private void saveMeta(Path skillDir, VersionMeta meta) {
- Path metaFile = skillDir.resolve(VERSION_META_FILE);
- try {
- jsonMapper.writeValue(metaFile.toFile(), meta);
- } catch (IOException e) {
- throw new BusinessException("保存版本元数据失败");
- }
- }
- // ==================== 内部数据结构 ====================
- /** _version.json 的数据结构 */
- @lombok.Data
- public static class VersionMeta {
- public int currentVersion = 0;
- public List<VersionEntry> versions = new ArrayList<>();
- }
- @lombok.Data
- public static class VersionEntry {
- public int number;
- public String createdAt;
- public String message;
- }
- }
|