From 29f132e48c27bd9e5edee713aeed8ebd1e83dc46 Mon Sep 17 00:00:00 2001 From: bruce Date: Mon, 1 Jun 2026 04:36:09 +0800 Subject: [PATCH] =?UTF-8?q?feat(skill):=20=E8=A1=A5=E9=BD=90=E7=89=88?= =?UTF-8?q?=E6=9C=AC=E6=B5=8B=E8=AF=95=E5=8F=91=E5=B8=83=E4=B8=8E=E5=B7=A5?= =?UTF-8?q?=E4=BD=9C=E5=8F=B0=E9=93=BE=E8=B7=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/SkillWorkspaceController.java | 57 ++++++ .../skill/dto/SkillDefinitionSaveDTO.java | 14 ++ .../bruce/skill/dto/SkillVersionSaveDTO.java | 17 ++ .../bruce/skill/entity/SkillDefinition.java | 4 + .../com/bruce/skill/entity/SkillVersion.java | 4 + .../skill/factory/SkillDefinitionFactory.java | 52 +++++ .../skill/factory/SkillVersionFactory.java | 55 ++++++ .../skill/mapper/SkillDefinitionMapper.java | 9 + .../skill/mapper/SkillVersionMapper.java | 9 + .../service/ISkillDefinitionService.java | 17 ++ .../com/bruce/skill/service/ISkillRunner.java | 12 ++ .../skill/service/ISkillVersionService.java | 21 +++ .../skill/service/ISkillWorkspaceService.java | 8 + .../impl/SkillDefinitionServiceImpl.java | 94 ++++++++++ .../skill/service/impl/SkillRunnerImpl.java | 27 +++ .../service/impl/SkillVersionServiceImpl.java | 177 ++++++++++++++++++ .../impl/SkillWorkspaceServiceImpl.java | 59 ++++++ .../com/bruce/skill/vo/SkillDefinitionVO.java | 14 ++ .../com/bruce/skill/vo/SkillVersionVO.java | 20 ++ .../com/bruce/skill/vo/SkillWorkspaceVO.java | 19 ++ .../skill/SkillComponentStructureTests.java | 64 +++++++ .../skill/factory/SkillFactoryTests.java | 68 +++++++ .../version/SkillVersionServiceTests.java | 127 +++++++++++++ .../workspace/SkillWorkspaceServiceTests.java | 69 +++++++ frontend/src/api/__tests__/skill.spec.ts | 65 +++++++ frontend/src/api/skill.ts | 62 ++++++ 26 files changed, 1144 insertions(+) create mode 100644 common-agent-skill/src/main/java/com/bruce/skill/controller/SkillWorkspaceController.java create mode 100644 common-agent-skill/src/main/java/com/bruce/skill/dto/SkillDefinitionSaveDTO.java create mode 100644 common-agent-skill/src/main/java/com/bruce/skill/dto/SkillVersionSaveDTO.java create mode 100644 common-agent-skill/src/main/java/com/bruce/skill/factory/SkillDefinitionFactory.java create mode 100644 common-agent-skill/src/main/java/com/bruce/skill/factory/SkillVersionFactory.java create mode 100644 common-agent-skill/src/main/java/com/bruce/skill/mapper/SkillDefinitionMapper.java create mode 100644 common-agent-skill/src/main/java/com/bruce/skill/mapper/SkillVersionMapper.java create mode 100644 common-agent-skill/src/main/java/com/bruce/skill/service/ISkillDefinitionService.java create mode 100644 common-agent-skill/src/main/java/com/bruce/skill/service/ISkillRunner.java create mode 100644 common-agent-skill/src/main/java/com/bruce/skill/service/ISkillVersionService.java create mode 100644 common-agent-skill/src/main/java/com/bruce/skill/service/ISkillWorkspaceService.java create mode 100644 common-agent-skill/src/main/java/com/bruce/skill/service/impl/SkillDefinitionServiceImpl.java create mode 100644 common-agent-skill/src/main/java/com/bruce/skill/service/impl/SkillRunnerImpl.java create mode 100644 common-agent-skill/src/main/java/com/bruce/skill/service/impl/SkillVersionServiceImpl.java create mode 100644 common-agent-skill/src/main/java/com/bruce/skill/service/impl/SkillWorkspaceServiceImpl.java create mode 100644 common-agent-skill/src/main/java/com/bruce/skill/vo/SkillDefinitionVO.java create mode 100644 common-agent-skill/src/main/java/com/bruce/skill/vo/SkillVersionVO.java create mode 100644 common-agent-skill/src/main/java/com/bruce/skill/vo/SkillWorkspaceVO.java create mode 100644 common-agent-skill/src/test/java/com/bruce/skill/SkillComponentStructureTests.java create mode 100644 common-agent-skill/src/test/java/com/bruce/skill/factory/SkillFactoryTests.java create mode 100644 common-agent-skill/src/test/java/com/bruce/skill/version/SkillVersionServiceTests.java create mode 100644 common-agent-skill/src/test/java/com/bruce/skill/workspace/SkillWorkspaceServiceTests.java create mode 100644 frontend/src/api/__tests__/skill.spec.ts create mode 100644 frontend/src/api/skill.ts diff --git a/common-agent-skill/src/main/java/com/bruce/skill/controller/SkillWorkspaceController.java b/common-agent-skill/src/main/java/com/bruce/skill/controller/SkillWorkspaceController.java new file mode 100644 index 0000000..8950b81 --- /dev/null +++ b/common-agent-skill/src/main/java/com/bruce/skill/controller/SkillWorkspaceController.java @@ -0,0 +1,57 @@ +package com.bruce.skill.controller; + +import com.bruce.common.domain.model.RequestResult; +import com.bruce.skill.dto.SkillVersionSaveDTO; +import com.bruce.skill.service.ISkillVersionService; +import com.bruce.skill.service.ISkillWorkspaceService; +import com.bruce.skill.vo.SkillVersionVO; +import com.bruce.skill.vo.SkillWorkspaceVO; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +/** + * Skill 工作台接口,首轮对齐前端原型中的详情、草稿、测试、发布和归档能力。 + */ +@RestController +@RequestMapping("/api/skills") +@RequiredArgsConstructor +public class SkillWorkspaceController { + + private final ISkillWorkspaceService skillWorkspaceService; + private final ISkillVersionService skillVersionService; + + @GetMapping("/{skillCode}") + public RequestResult detail(@PathVariable("skillCode") String skillCode) { + return RequestResult.success(skillWorkspaceService.getWorkspace(skillCode)); + } + + @PostMapping("/{skillCode}/draft") + public RequestResult saveDraft(@PathVariable("skillCode") String skillCode, + @RequestBody SkillVersionSaveDTO request) { + return RequestResult.success(skillVersionService.saveDraft(skillCode, request)); + } + + @PostMapping("/{skillCode}/test") + public RequestResult test(@PathVariable("skillCode") String skillCode, + @RequestBody SkillVersionSaveDTO request) { + return RequestResult.success(skillVersionService.test(skillCode, request)); + } + + @PostMapping("/{skillCode}/publish") + public RequestResult publish(@PathVariable("skillCode") String skillCode, + @RequestBody SkillVersionSaveDTO request) { + return RequestResult.success(skillVersionService.publish(skillCode, request)); + } + + @PostMapping("/{skillCode}/archive") + public RequestResult archive(@PathVariable("skillCode") String skillCode, + @RequestParam("versionNo") Integer versionNo) { + return RequestResult.success(skillVersionService.archive(skillCode, versionNo)); + } +} diff --git a/common-agent-skill/src/main/java/com/bruce/skill/dto/SkillDefinitionSaveDTO.java b/common-agent-skill/src/main/java/com/bruce/skill/dto/SkillDefinitionSaveDTO.java new file mode 100644 index 0000000..fbeedef --- /dev/null +++ b/common-agent-skill/src/main/java/com/bruce/skill/dto/SkillDefinitionSaveDTO.java @@ -0,0 +1,14 @@ +package com.bruce.skill.dto; + +import lombok.Data; + +@Data +public class SkillDefinitionSaveDTO { + private Long id; + private String skillCode; + private String skillName; + private String skillType; + private String description; + private String status; + private String remark; +} diff --git a/common-agent-skill/src/main/java/com/bruce/skill/dto/SkillVersionSaveDTO.java b/common-agent-skill/src/main/java/com/bruce/skill/dto/SkillVersionSaveDTO.java new file mode 100644 index 0000000..dc58a27 --- /dev/null +++ b/common-agent-skill/src/main/java/com/bruce/skill/dto/SkillVersionSaveDTO.java @@ -0,0 +1,17 @@ +package com.bruce.skill.dto; + +import lombok.Data; + +@Data +public class SkillVersionSaveDTO { + private Long id; + private Long skillId; + private Integer versionNo; + private String promptText; + private String codeText; + private String configJson; + private String variableSchemaJson; + private String testResultJson; + private String publishStatus; + private String remark; +} diff --git a/common-agent-skill/src/main/java/com/bruce/skill/entity/SkillDefinition.java b/common-agent-skill/src/main/java/com/bruce/skill/entity/SkillDefinition.java index 4758219..b498daa 100644 --- a/common-agent-skill/src/main/java/com/bruce/skill/entity/SkillDefinition.java +++ b/common-agent-skill/src/main/java/com/bruce/skill/entity/SkillDefinition.java @@ -13,6 +13,8 @@ import lombok.EqualsAndHashCode; @TableName("skill_definition") public class SkillDefinition extends BaseEntity { + private static final long serialVersionUID = 1L; + private String skillCode; private String skillName; @@ -22,4 +24,6 @@ public class SkillDefinition extends BaseEntity { private String description; private String status; + + private String remark; } diff --git a/common-agent-skill/src/main/java/com/bruce/skill/entity/SkillVersion.java b/common-agent-skill/src/main/java/com/bruce/skill/entity/SkillVersion.java index 2065a00..db41062 100644 --- a/common-agent-skill/src/main/java/com/bruce/skill/entity/SkillVersion.java +++ b/common-agent-skill/src/main/java/com/bruce/skill/entity/SkillVersion.java @@ -15,6 +15,8 @@ import lombok.EqualsAndHashCode; @TableName("skill_version") public class SkillVersion extends BaseEntity { + private static final long serialVersionUID = 1L; + private Long skillId; private Integer versionNo; @@ -35,4 +37,6 @@ public class SkillVersion extends BaseEntity { private String publishStatus; private java.time.LocalDateTime publishedTime; + + private String remark; } diff --git a/common-agent-skill/src/main/java/com/bruce/skill/factory/SkillDefinitionFactory.java b/common-agent-skill/src/main/java/com/bruce/skill/factory/SkillDefinitionFactory.java new file mode 100644 index 0000000..02c43a4 --- /dev/null +++ b/common-agent-skill/src/main/java/com/bruce/skill/factory/SkillDefinitionFactory.java @@ -0,0 +1,52 @@ +package com.bruce.skill.factory; + +import com.bruce.skill.dto.SkillDefinitionSaveDTO; +import com.bruce.skill.entity.SkillDefinition; +import com.bruce.skill.vo.SkillDefinitionVO; +import org.springframework.beans.BeanUtils; +import org.springframework.stereotype.Component; +import org.springframework.util.StringUtils; + +import java.util.List; + +/** + * Skill 定义工厂,统一处理定义保存对象、实体和返回对象转换。 + */ +@Component +public class SkillDefinitionFactory { + + public SkillDefinition toEntity(SkillDefinitionSaveDTO request) { + if (request == null) { + return null; + } + SkillDefinition entity = new SkillDefinition(); + entity.setId(request.getId()); + entity.setSkillCode(trimToNull(request.getSkillCode())); + entity.setSkillName(trimToNull(request.getSkillName())); + entity.setSkillType(trimToNull(request.getSkillType())); + entity.setDescription(trimToNull(request.getDescription())); + entity.setStatus(trimToNull(request.getStatus())); + entity.setRemark(trimToNull(request.getRemark())); + return entity; + } + + public SkillDefinitionVO toVO(SkillDefinition entity) { + if (entity == null) { + return null; + } + SkillDefinitionVO vo = new SkillDefinitionVO(); + BeanUtils.copyProperties(entity, vo); + return vo; + } + + public List toVOList(List entities) { + return entities == null ? List.of() : entities.stream().map(this::toVO).toList(); + } + + private String trimToNull(String value) { + if (!StringUtils.hasText(value)) { + return null; + } + return value.trim(); + } +} diff --git a/common-agent-skill/src/main/java/com/bruce/skill/factory/SkillVersionFactory.java b/common-agent-skill/src/main/java/com/bruce/skill/factory/SkillVersionFactory.java new file mode 100644 index 0000000..b8837f1 --- /dev/null +++ b/common-agent-skill/src/main/java/com/bruce/skill/factory/SkillVersionFactory.java @@ -0,0 +1,55 @@ +package com.bruce.skill.factory; + +import com.bruce.skill.dto.SkillVersionSaveDTO; +import com.bruce.skill.entity.SkillVersion; +import com.bruce.skill.vo.SkillVersionVO; +import org.springframework.beans.BeanUtils; +import org.springframework.stereotype.Component; +import org.springframework.util.StringUtils; + +import java.util.List; + +/** + * Skill 版本工厂,统一处理版本草稿、测试和发布对象转换。 + */ +@Component +public class SkillVersionFactory { + + public SkillVersion toEntity(SkillVersionSaveDTO request) { + if (request == null) { + return null; + } + SkillVersion entity = new SkillVersion(); + entity.setId(request.getId()); + entity.setSkillId(request.getSkillId()); + entity.setVersionNo(request.getVersionNo()); + entity.setPromptText(trimToNull(request.getPromptText())); + entity.setCodeText(trimToNull(request.getCodeText())); + entity.setConfigJson(trimToNull(request.getConfigJson())); + entity.setVariableSchemaJson(trimToNull(request.getVariableSchemaJson())); + entity.setTestResultJson(trimToNull(request.getTestResultJson())); + entity.setPublishStatus(trimToNull(request.getPublishStatus())); + entity.setRemark(trimToNull(request.getRemark())); + return entity; + } + + public SkillVersionVO toVO(SkillVersion entity) { + if (entity == null) { + return null; + } + SkillVersionVO vo = new SkillVersionVO(); + BeanUtils.copyProperties(entity, vo); + return vo; + } + + public List toVOList(List entities) { + return entities == null ? List.of() : entities.stream().map(this::toVO).toList(); + } + + private String trimToNull(String value) { + if (!StringUtils.hasText(value)) { + return null; + } + return value.trim(); + } +} diff --git a/common-agent-skill/src/main/java/com/bruce/skill/mapper/SkillDefinitionMapper.java b/common-agent-skill/src/main/java/com/bruce/skill/mapper/SkillDefinitionMapper.java new file mode 100644 index 0000000..eedb31e --- /dev/null +++ b/common-agent-skill/src/main/java/com/bruce/skill/mapper/SkillDefinitionMapper.java @@ -0,0 +1,9 @@ +package com.bruce.skill.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.bruce.skill.entity.SkillDefinition; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface SkillDefinitionMapper extends BaseMapper { +} diff --git a/common-agent-skill/src/main/java/com/bruce/skill/mapper/SkillVersionMapper.java b/common-agent-skill/src/main/java/com/bruce/skill/mapper/SkillVersionMapper.java new file mode 100644 index 0000000..32030da --- /dev/null +++ b/common-agent-skill/src/main/java/com/bruce/skill/mapper/SkillVersionMapper.java @@ -0,0 +1,9 @@ +package com.bruce.skill.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.bruce.skill.entity.SkillVersion; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface SkillVersionMapper extends BaseMapper { +} diff --git a/common-agent-skill/src/main/java/com/bruce/skill/service/ISkillDefinitionService.java b/common-agent-skill/src/main/java/com/bruce/skill/service/ISkillDefinitionService.java new file mode 100644 index 0000000..9a01868 --- /dev/null +++ b/common-agent-skill/src/main/java/com/bruce/skill/service/ISkillDefinitionService.java @@ -0,0 +1,17 @@ +package com.bruce.skill.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.bruce.skill.dto.SkillDefinitionSaveDTO; +import com.bruce.skill.entity.SkillDefinition; +import com.bruce.skill.vo.SkillDefinitionVO; + +import java.util.List; + +public interface ISkillDefinitionService extends IService { + + SkillDefinition getByCode(String skillCode); + + List listDefinitions(); + + boolean saveDefinition(SkillDefinitionSaveDTO request); +} diff --git a/common-agent-skill/src/main/java/com/bruce/skill/service/ISkillRunner.java b/common-agent-skill/src/main/java/com/bruce/skill/service/ISkillRunner.java new file mode 100644 index 0000000..3362438 --- /dev/null +++ b/common-agent-skill/src/main/java/com/bruce/skill/service/ISkillRunner.java @@ -0,0 +1,12 @@ +package com.bruce.skill.service; + +import com.bruce.skill.entity.SkillDefinition; +import com.bruce.skill.entity.SkillVersion; + +public interface ISkillRunner { + + /** + * 执行 Skill 草稿测试,首轮使用受控 mock 结果支撑工作台联调。 + */ + String runTest(SkillDefinition definition, SkillVersion version); +} diff --git a/common-agent-skill/src/main/java/com/bruce/skill/service/ISkillVersionService.java b/common-agent-skill/src/main/java/com/bruce/skill/service/ISkillVersionService.java new file mode 100644 index 0000000..f9ea4fa --- /dev/null +++ b/common-agent-skill/src/main/java/com/bruce/skill/service/ISkillVersionService.java @@ -0,0 +1,21 @@ +package com.bruce.skill.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.bruce.skill.dto.SkillVersionSaveDTO; +import com.bruce.skill.entity.SkillVersion; +import com.bruce.skill.vo.SkillVersionVO; + +import java.util.List; + +public interface ISkillVersionService extends IService { + + boolean saveDraft(String skillCode, SkillVersionSaveDTO request); + + SkillVersionVO test(String skillCode, SkillVersionSaveDTO request); + + boolean publish(String skillCode, SkillVersionSaveDTO request); + + boolean archive(String skillCode, Integer versionNo); + + List listBySkillId(Long skillId); +} diff --git a/common-agent-skill/src/main/java/com/bruce/skill/service/ISkillWorkspaceService.java b/common-agent-skill/src/main/java/com/bruce/skill/service/ISkillWorkspaceService.java new file mode 100644 index 0000000..8e43914 --- /dev/null +++ b/common-agent-skill/src/main/java/com/bruce/skill/service/ISkillWorkspaceService.java @@ -0,0 +1,8 @@ +package com.bruce.skill.service; + +import com.bruce.skill.vo.SkillWorkspaceVO; + +public interface ISkillWorkspaceService { + + SkillWorkspaceVO getWorkspace(String skillCode); +} diff --git a/common-agent-skill/src/main/java/com/bruce/skill/service/impl/SkillDefinitionServiceImpl.java b/common-agent-skill/src/main/java/com/bruce/skill/service/impl/SkillDefinitionServiceImpl.java new file mode 100644 index 0000000..becd453 --- /dev/null +++ b/common-agent-skill/src/main/java/com/bruce/skill/service/impl/SkillDefinitionServiceImpl.java @@ -0,0 +1,94 @@ +package com.bruce.skill.service.impl; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.bruce.skill.dto.SkillDefinitionSaveDTO; +import com.bruce.skill.entity.SkillDefinition; +import com.bruce.skill.factory.SkillDefinitionFactory; +import com.bruce.skill.mapper.SkillDefinitionMapper; +import com.bruce.skill.service.ISkillDefinitionService; +import com.bruce.skill.vo.SkillDefinitionVO; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.util.StringUtils; + +import java.util.List; + +@Slf4j +@Service +@RequiredArgsConstructor +public class SkillDefinitionServiceImpl extends ServiceImpl implements ISkillDefinitionService { + + private final SkillDefinitionFactory skillDefinitionFactory; + + @Override + public SkillDefinition getByCode(String skillCode) { + if (!StringUtils.hasText(skillCode)) { + throw new IllegalArgumentException("Skill编码不能为空"); + } + SkillDefinition result = lambdaQuery() + .eq(SkillDefinition::getSkillCode, skillCode.trim()) + .one(); + log.info("按编码查询Skill定义完成,skillCode={}, found={}", skillCode.trim(), result != null); + return result; + } + + @Override + public List listDefinitions() { + List result = skillDefinitionFactory.toVOList(lambdaQuery() + .orderByAsc(SkillDefinition::getSkillCode) + .list()); + log.info("查询Skill定义列表完成,count={}", result.size()); + return result; + } + + @Override + public boolean saveDefinition(SkillDefinitionSaveDTO request) { + validateRequest(request); + SkillDefinition duplicate = lambdaQuery() + .eq(SkillDefinition::getSkillCode, request.getSkillCode().trim()) + .ne(request.getId() != null, SkillDefinition::getId, request.getId()) + .one(); + if (duplicate != null) { + throw new IllegalArgumentException("Skill编码已存在: " + request.getSkillCode().trim()); + } + SkillDefinition requestEntity = skillDefinitionFactory.toEntity(request); + SkillDefinition entity = request.getId() == null ? new SkillDefinition() : getById(request.getId()); + if (request.getId() != null && entity == null) { + throw new IllegalArgumentException("Skill定义不存在,ID: " + request.getId()); + } + if (entity == null) { + entity = requestEntity; + if (!StringUtils.hasText(entity.getStatus())) { + entity.setStatus("DRAFT"); + } + boolean result = save(entity); + log.info("新增Skill定义完成,skillCode={}, result={}", entity.getSkillCode(), result); + return result; + } + entity.setSkillCode(requestEntity.getSkillCode()); + entity.setSkillName(requestEntity.getSkillName()); + entity.setSkillType(requestEntity.getSkillType()); + entity.setDescription(requestEntity.getDescription()); + entity.setStatus(StringUtils.hasText(requestEntity.getStatus()) ? requestEntity.getStatus() : entity.getStatus()); + entity.setRemark(requestEntity.getRemark()); + boolean result = updateById(entity); + log.info("更新Skill定义完成,skillId={}, skillCode={}, result={}", entity.getId(), entity.getSkillCode(), result); + return result; + } + + private void validateRequest(SkillDefinitionSaveDTO request) { + if (request == null) { + throw new IllegalArgumentException("Skill定义保存请求不能为空"); + } + if (!StringUtils.hasText(request.getSkillCode())) { + throw new IllegalArgumentException("Skill编码不能为空"); + } + if (!StringUtils.hasText(request.getSkillName())) { + throw new IllegalArgumentException("Skill名称不能为空"); + } + if (!StringUtils.hasText(request.getSkillType())) { + throw new IllegalArgumentException("Skill类型不能为空"); + } + } +} diff --git a/common-agent-skill/src/main/java/com/bruce/skill/service/impl/SkillRunnerImpl.java b/common-agent-skill/src/main/java/com/bruce/skill/service/impl/SkillRunnerImpl.java new file mode 100644 index 0000000..7e0231e --- /dev/null +++ b/common-agent-skill/src/main/java/com/bruce/skill/service/impl/SkillRunnerImpl.java @@ -0,0 +1,27 @@ +package com.bruce.skill.service.impl; + +import com.bruce.skill.entity.SkillDefinition; +import com.bruce.skill.entity.SkillVersion; +import com.bruce.skill.service.ISkillRunner; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +/** + * Skill 测试执行器首轮采用 mock 执行,先保证工作台编辑、测试、保存链路打通。 + */ +@Slf4j +@Service +public class SkillRunnerImpl implements ISkillRunner { + + @Override + public String runTest(SkillDefinition definition, SkillVersion version) { + log.info("Skill测试执行开始,skillId={}, skillCode={}, versionNo={}", + definition.getId(), definition.getSkillCode(), version.getVersionNo()); + String result = """ + {"quality_score":0.86,"summary":"建议补充日志留存周期引用,并明确私有化部署边界","skillCode":"%s","versionNo":%d} + """.formatted(definition.getSkillCode(), version.getVersionNo()); + log.info("Skill测试执行结束,skillId={}, skillCode={}, versionNo={}", + definition.getId(), definition.getSkillCode(), version.getVersionNo()); + return result; + } +} diff --git a/common-agent-skill/src/main/java/com/bruce/skill/service/impl/SkillVersionServiceImpl.java b/common-agent-skill/src/main/java/com/bruce/skill/service/impl/SkillVersionServiceImpl.java new file mode 100644 index 0000000..458ce42 --- /dev/null +++ b/common-agent-skill/src/main/java/com/bruce/skill/service/impl/SkillVersionServiceImpl.java @@ -0,0 +1,177 @@ +package com.bruce.skill.service.impl; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.bruce.skill.dto.SkillVersionSaveDTO; +import com.bruce.skill.entity.SkillDefinition; +import com.bruce.skill.entity.SkillVersion; +import com.bruce.skill.factory.SkillVersionFactory; +import com.bruce.skill.mapper.SkillVersionMapper; +import com.bruce.skill.service.ISkillDefinitionService; +import com.bruce.skill.service.ISkillRunner; +import com.bruce.skill.service.ISkillVersionService; +import com.bruce.skill.vo.SkillVersionVO; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.util.StringUtils; + +import java.time.LocalDateTime; +import java.util.List; + +@Slf4j +@Service +@RequiredArgsConstructor +public class SkillVersionServiceImpl extends ServiceImpl implements ISkillVersionService { + + private final ISkillDefinitionService skillDefinitionService; + private final ISkillRunner skillRunner; + private final SkillVersionFactory skillVersionFactory; + + @Override + public boolean saveDraft(String skillCode, SkillVersionSaveDTO request) { + SkillVersion entity = buildDraftEntity(skillCode, request, false); + boolean result = entity.getId() == null ? save(entity) : updateById(entity); + log.info("保存Skill草稿完成,skillId={}, versionNo={}, result={}", entity.getSkillId(), entity.getVersionNo(), result); + return result; + } + + @Override + public SkillVersionVO test(String skillCode, SkillVersionSaveDTO request) { + SkillDefinition definition = loadDefinitionByCode(skillCode); + SkillVersion entity = buildDraftEntity(skillCode, request, false); + entity.setTestResultJson(skillRunner.runTest(definition, entity)); + boolean result = entity.getId() == null ? save(entity) : updateById(entity); + log.info("测试Skill草稿完成,skillId={}, versionNo={}, persistResult={}", entity.getSkillId(), entity.getVersionNo(), result); + return skillVersionFactory.toVO(entity); + } + + @Override + public boolean publish(String skillCode, SkillVersionSaveDTO request) { + SkillVersion entity = buildDraftEntity(skillCode, request, true); + entity.setPublishStatus("PUBLISHED"); + entity.setPublishedTime(LocalDateTime.now()); + boolean result = entity.getId() == null ? save(entity) : updateById(entity); + + SkillDefinition definition = loadDefinitionByCode(skillCode); + definition.setStatus("PUBLISHED"); + skillDefinitionService.updateById(definition); + log.info("发布Skill版本完成,skillId={}, versionNo={}, result={}", entity.getSkillId(), entity.getVersionNo(), result); + return result; + } + + @Override + public boolean archive(String skillCode, Integer versionNo) { + SkillDefinition definition = loadDefinitionByCode(skillCode); + if (versionNo == null) { + throw new IllegalArgumentException("版本号不能为空"); + } + SkillVersion version = findBySkillAndVersionNo(definition.getId(), versionNo); + if (version == null) { + throw new IllegalArgumentException("Skill版本不存在,skillCode=%s, versionNo=%d".formatted(skillCode, versionNo)); + } + version.setPublishStatus("ARCHIVED"); + boolean result = updateById(version); + log.info("归档Skill版本完成,skillId={}, versionNo={}, result={}", definition.getId(), versionNo, result); + return result; + } + + @Override + public List listBySkillId(Long skillId) { + if (skillId == null) { + throw new IllegalArgumentException("Skill ID不能为空"); + } + List result = skillVersionFactory.toVOList(lambdaQuery() + .eq(SkillVersion::getSkillId, skillId) + .orderByDesc(SkillVersion::getVersionNo) + .list()); + log.info("查询Skill版本列表完成,skillId={}, count={}", skillId, result.size()); + return result; + } + + public SkillDefinition loadDefinitionByCode(String skillCode) { + SkillDefinition definition = skillDefinitionService.getByCode(skillCode); + if (definition == null) { + throw new IllegalArgumentException("Skill定义不存在,skillCode: " + skillCode); + } + return definition; + } + + public SkillVersion findBySkillAndVersionNo(Long skillId, Integer versionNo) { + if (skillId == null || versionNo == null) { + return null; + } + return lambdaQuery() + .eq(SkillVersion::getSkillId, skillId) + .eq(SkillVersion::getVersionNo, versionNo) + .one(); + } + + private SkillVersion buildDraftEntity(String skillCode, SkillVersionSaveDTO request, boolean publishing) { + SkillDefinition definition = loadDefinitionByCode(skillCode); + validateRequest(request, publishing); + SkillVersion duplicate = findBySkillAndVersionNo(definition.getId(), request.getVersionNo()); + if (duplicate != null && request.getId() == null && publishing) { + throw new IllegalArgumentException("Skill版本号已存在: " + request.getVersionNo()); + } + + SkillVersion requestEntity = skillVersionFactory.toEntity(request); + SkillVersion entity = request.getId() == null ? duplicate : getById(request.getId()); + if (request.getId() != null && entity == null) { + throw new IllegalArgumentException("Skill版本不存在,ID: " + request.getId()); + } + if (entity == null) { + entity = new SkillVersion(); + } + entity.setId(request.getId() == null ? entity.getId() : request.getId()); + entity.setSkillId(definition.getId()); + entity.setVersionNo(requestEntity.getVersionNo()); + entity.setPromptText(requestEntity.getPromptText()); + entity.setCodeText(requestEntity.getCodeText()); + entity.setConfigJson(defaultJson(requestEntity.getConfigJson())); + entity.setVariableSchemaJson(defaultJson(requestEntity.getVariableSchemaJson())); + entity.setTestResultJson(defaultJson(requestEntity.getTestResultJson())); + entity.setPublishStatus(StringUtils.hasText(requestEntity.getPublishStatus()) ? requestEntity.getPublishStatus() : "DRAFT"); + entity.setRemark(requestEntity.getRemark()); + return entity; + } + + private void validateRequest(SkillVersionSaveDTO request, boolean publishing) { + if (request == null) { + throw new IllegalArgumentException("Skill版本请求不能为空"); + } + if (request.getVersionNo() == null) { + throw new IllegalArgumentException("版本号不能为空"); + } + if (!StringUtils.hasText(request.getPromptText()) + && !StringUtils.hasText(request.getCodeText()) + && !hasMeaningfulJson(request.getConfigJson())) { + throw new IllegalArgumentException("Prompt、Code、Config 至少填写一项"); + } + validateJson(request.getConfigJson(), "configJson"); + validateJson(request.getVariableSchemaJson(), "variableSchemaJson"); + validateJson(request.getTestResultJson(), "testResultJson"); + if (publishing && !StringUtils.hasText(request.getTestResultJson())) { + log.info("发布Skill时未显式传测试结果,后续以当前草稿快照为准,versionNo={}", request.getVersionNo()); + } + } + + private void validateJson(String json, String fieldName) { + if (!StringUtils.hasText(json)) { + return; + } + String normalized = json.trim(); + boolean isObject = normalized.startsWith("{") && normalized.endsWith("}"); + boolean isArray = normalized.startsWith("[") && normalized.endsWith("]"); + if (!isObject && !isArray) { + throw new IllegalArgumentException(fieldName + "必须是合法JSON"); + } + } + + private boolean hasMeaningfulJson(String json) { + return StringUtils.hasText(json) && !"{}".equals(json.trim()) && !"[]".equals(json.trim()); + } + + private String defaultJson(String json) { + return StringUtils.hasText(json) ? json.trim() : "{}"; + } +} diff --git a/common-agent-skill/src/main/java/com/bruce/skill/service/impl/SkillWorkspaceServiceImpl.java b/common-agent-skill/src/main/java/com/bruce/skill/service/impl/SkillWorkspaceServiceImpl.java new file mode 100644 index 0000000..0d7fab4 --- /dev/null +++ b/common-agent-skill/src/main/java/com/bruce/skill/service/impl/SkillWorkspaceServiceImpl.java @@ -0,0 +1,59 @@ +package com.bruce.skill.service.impl; + +import com.bruce.skill.entity.SkillDefinition; +import com.bruce.skill.service.ISkillDefinitionService; +import com.bruce.skill.service.ISkillVersionService; +import com.bruce.skill.service.ISkillWorkspaceService; +import com.bruce.skill.vo.SkillDefinitionVO; +import com.bruce.skill.vo.SkillVersionVO; +import com.bruce.skill.vo.SkillWorkspaceVO; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Slf4j +@Service +@RequiredArgsConstructor +public class SkillWorkspaceServiceImpl implements ISkillWorkspaceService { + + private final ISkillDefinitionService skillDefinitionService; + private final ISkillVersionService skillVersionService; + + @Override + public SkillWorkspaceVO getWorkspace(String skillCode) { + log.info("Skill工作台查询开始,skillCode={}", skillCode); + SkillDefinition definition = skillDefinitionService.getByCode(skillCode); + if (definition == null) { + throw new IllegalArgumentException("Skill定义不存在,skillCode: " + skillCode); + } + List skills = skillDefinitionService.listDefinitions(); + List versions = skillVersionService.listBySkillId(definition.getId()); + + SkillWorkspaceVO workspace = new SkillWorkspaceVO(); + workspace.setSkillId(definition.getId()); + workspace.setSkillCode(definition.getSkillCode()); + workspace.setSkillName(definition.getSkillName()); + workspace.setSkillType(definition.getSkillType()); + workspace.setDescription(definition.getDescription()); + workspace.setStatus(definition.getStatus()); + workspace.setSkills(skills); + workspace.setVersions(versions); + + SkillVersionVO publishedVersion = versions.stream() + .filter(item -> "PUBLISHED".equals(item.getPublishStatus())) + .findFirst() + .orElse(null); + if (publishedVersion != null) { + workspace.setPublishedVersionNo(publishedVersion.getVersionNo()); + } + SkillVersionVO latestVersion = versions.isEmpty() ? null : versions.getFirst(); + if (latestVersion != null) { + workspace.setLatestTestResultJson(latestVersion.getTestResultJson()); + } + log.info("Skill工作台查询结束,skillCode={}, versionCount={}, publishedVersionNo={}", + skillCode, versions.size(), workspace.getPublishedVersionNo()); + return workspace; + } +} diff --git a/common-agent-skill/src/main/java/com/bruce/skill/vo/SkillDefinitionVO.java b/common-agent-skill/src/main/java/com/bruce/skill/vo/SkillDefinitionVO.java new file mode 100644 index 0000000..7158744 --- /dev/null +++ b/common-agent-skill/src/main/java/com/bruce/skill/vo/SkillDefinitionVO.java @@ -0,0 +1,14 @@ +package com.bruce.skill.vo; + +import lombok.Data; + +@Data +public class SkillDefinitionVO { + private Long id; + private String skillCode; + private String skillName; + private String skillType; + private String description; + private String status; + private String remark; +} diff --git a/common-agent-skill/src/main/java/com/bruce/skill/vo/SkillVersionVO.java b/common-agent-skill/src/main/java/com/bruce/skill/vo/SkillVersionVO.java new file mode 100644 index 0000000..0219afc --- /dev/null +++ b/common-agent-skill/src/main/java/com/bruce/skill/vo/SkillVersionVO.java @@ -0,0 +1,20 @@ +package com.bruce.skill.vo; + +import java.time.LocalDateTime; + +import lombok.Data; + +@Data +public class SkillVersionVO { + private Long id; + private Long skillId; + private Integer versionNo; + private String promptText; + private String codeText; + private String configJson; + private String variableSchemaJson; + private String testResultJson; + private String publishStatus; + private LocalDateTime publishedTime; + private String remark; +} diff --git a/common-agent-skill/src/main/java/com/bruce/skill/vo/SkillWorkspaceVO.java b/common-agent-skill/src/main/java/com/bruce/skill/vo/SkillWorkspaceVO.java new file mode 100644 index 0000000..a18ce91 --- /dev/null +++ b/common-agent-skill/src/main/java/com/bruce/skill/vo/SkillWorkspaceVO.java @@ -0,0 +1,19 @@ +package com.bruce.skill.vo; + +import lombok.Data; + +import java.util.List; + +@Data +public class SkillWorkspaceVO { + private Long skillId; + private String skillCode; + private String skillName; + private String skillType; + private String description; + private String status; + private Integer publishedVersionNo; + private String latestTestResultJson; + private List skills; + private List versions; +} diff --git a/common-agent-skill/src/test/java/com/bruce/skill/SkillComponentStructureTests.java b/common-agent-skill/src/test/java/com/bruce/skill/SkillComponentStructureTests.java new file mode 100644 index 0000000..b48b111 --- /dev/null +++ b/common-agent-skill/src/test/java/com/bruce/skill/SkillComponentStructureTests.java @@ -0,0 +1,64 @@ +package com.bruce.skill; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.baomidou.mybatisplus.extension.service.IService; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.bruce.common.domain.model.RequestResult; +import com.bruce.skill.controller.SkillWorkspaceController; +import com.bruce.skill.dto.SkillVersionSaveDTO; +import com.bruce.skill.entity.SkillDefinition; +import com.bruce.skill.entity.SkillVersion; +import com.bruce.skill.mapper.SkillDefinitionMapper; +import com.bruce.skill.mapper.SkillVersionMapper; +import com.bruce.skill.service.ISkillDefinitionService; +import com.bruce.skill.service.ISkillVersionService; +import com.bruce.skill.service.ISkillWorkspaceService; +import com.bruce.skill.service.impl.SkillDefinitionServiceImpl; +import com.bruce.skill.service.impl.SkillVersionServiceImpl; +import com.bruce.skill.vo.SkillDefinitionVO; +import com.bruce.skill.vo.SkillWorkspaceVO; +import org.junit.jupiter.api.Test; + +import java.lang.reflect.Method; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class SkillComponentStructureTests { + + @Test + void skillComponentsShouldReuseMybatisPlusBaseTypes() { + assertTrue(BaseMapper.class.isAssignableFrom(SkillDefinitionMapper.class)); + assertTrue(BaseMapper.class.isAssignableFrom(SkillVersionMapper.class)); + assertTrue(IService.class.isAssignableFrom(ISkillDefinitionService.class)); + assertTrue(IService.class.isAssignableFrom(ISkillVersionService.class)); + assertTrue(ServiceImpl.class.isAssignableFrom(SkillDefinitionServiceImpl.class)); + assertTrue(ServiceImpl.class.isAssignableFrom(SkillVersionServiceImpl.class)); + } + + @Test + void skillControllerShouldExposeRequestResultMethods() throws NoSuchMethodException { + Method detailMethod = SkillWorkspaceController.class.getMethod("detail", String.class); + Method saveDraftMethod = SkillWorkspaceController.class.getMethod("saveDraft", String.class, SkillVersionSaveDTO.class); + Method testMethod = SkillWorkspaceController.class.getMethod("test", String.class, SkillVersionSaveDTO.class); + Method publishMethod = SkillWorkspaceController.class.getMethod("publish", String.class, SkillVersionSaveDTO.class); + Method archiveMethod = SkillWorkspaceController.class.getMethod("archive", String.class, Integer.class); + + Method definitionDetailMethod = ISkillDefinitionService.class.getMethod("getByCode", String.class); + Method definitionListMethod = ISkillDefinitionService.class.getMethod("listDefinitions"); + Method workspaceMethod = ISkillWorkspaceService.class.getMethod("getWorkspace", String.class); + + assertEquals(RequestResult.class, detailMethod.getReturnType()); + assertEquals(RequestResult.class, saveDraftMethod.getReturnType()); + assertEquals(RequestResult.class, testMethod.getReturnType()); + assertEquals(RequestResult.class, publishMethod.getReturnType()); + assertEquals(RequestResult.class, archiveMethod.getReturnType()); + + assertEquals(SkillDefinition.class, definitionDetailMethod.getReturnType()); + assertEquals(List.class, definitionListMethod.getReturnType()); + assertEquals(SkillWorkspaceVO.class, workspaceMethod.getReturnType()); + assertEquals(SkillDefinitionVO.class, SkillDefinitionVO.class); + assertEquals(SkillVersion.class, SkillVersion.class); + } +} diff --git a/common-agent-skill/src/test/java/com/bruce/skill/factory/SkillFactoryTests.java b/common-agent-skill/src/test/java/com/bruce/skill/factory/SkillFactoryTests.java new file mode 100644 index 0000000..78a1234 --- /dev/null +++ b/common-agent-skill/src/test/java/com/bruce/skill/factory/SkillFactoryTests.java @@ -0,0 +1,68 @@ +package com.bruce.skill.factory; + +import com.bruce.skill.dto.SkillDefinitionSaveDTO; +import com.bruce.skill.dto.SkillVersionSaveDTO; +import com.bruce.skill.entity.SkillDefinition; +import com.bruce.skill.entity.SkillVersion; +import com.bruce.skill.vo.SkillDefinitionVO; +import com.bruce.skill.vo.SkillVersionVO; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +class SkillFactoryTests { + + private final SkillDefinitionFactory skillDefinitionFactory = new SkillDefinitionFactory(); + private final SkillVersionFactory skillVersionFactory = new SkillVersionFactory(); + + @Test + void definitionFactoryShouldTrimRequestAndBuildVo() { + SkillDefinitionSaveDTO request = new SkillDefinitionSaveDTO(); + request.setId(11L); + request.setSkillCode(" skill-citation "); + request.setSkillName(" 引用审校 Skill "); + request.setSkillType(" MIXED "); + request.setDescription(" 检查答案与引用是否一致 "); + request.setStatus(" DRAFT "); + request.setRemark(" 默认技能 "); + + SkillDefinition entity = skillDefinitionFactory.toEntity(request); + assertEquals("skill-citation", entity.getSkillCode()); + assertEquals("引用审校 Skill", entity.getSkillName()); + assertEquals("MIXED", entity.getSkillType()); + assertEquals("检查答案与引用是否一致", entity.getDescription()); + assertEquals("DRAFT", entity.getStatus()); + assertEquals("默认技能", entity.getRemark()); + + SkillDefinitionVO vo = skillDefinitionFactory.toVO(entity); + assertEquals(11L, vo.getId()); + assertEquals("skill-citation", vo.getSkillCode()); + } + + @Test + void versionFactoryShouldPreserveJsonAndPublishStatus() { + SkillVersionSaveDTO request = new SkillVersionSaveDTO(); + request.setSkillId(1001L); + request.setVersionNo(4); + request.setPromptText(" 你是回答审校器 "); + request.setCodeText(" return input; "); + request.setConfigJson(" {\"timeout\":3000} "); + request.setVariableSchemaJson(" {\"type\":\"object\"} "); + request.setPublishStatus(" DRAFT "); + request.setRemark(" 新草稿 "); + + SkillVersion entity = skillVersionFactory.toEntity(request); + assertNotNull(entity); + assertEquals("你是回答审校器", entity.getPromptText()); + assertEquals("return input;", entity.getCodeText()); + assertEquals("{\"timeout\":3000}", entity.getConfigJson()); + assertEquals("{\"type\":\"object\"}", entity.getVariableSchemaJson()); + assertEquals("DRAFT", entity.getPublishStatus()); + + SkillVersionVO vo = skillVersionFactory.toVO(entity); + assertEquals(1001L, vo.getSkillId()); + assertEquals(4, vo.getVersionNo()); + assertEquals("DRAFT", vo.getPublishStatus()); + } +} diff --git a/common-agent-skill/src/test/java/com/bruce/skill/version/SkillVersionServiceTests.java b/common-agent-skill/src/test/java/com/bruce/skill/version/SkillVersionServiceTests.java new file mode 100644 index 0000000..d891c33 --- /dev/null +++ b/common-agent-skill/src/test/java/com/bruce/skill/version/SkillVersionServiceTests.java @@ -0,0 +1,127 @@ +package com.bruce.skill.version; + +import com.bruce.skill.dto.SkillVersionSaveDTO; +import com.bruce.skill.entity.SkillDefinition; +import com.bruce.skill.entity.SkillVersion; +import com.bruce.skill.factory.SkillVersionFactory; +import com.bruce.skill.service.ISkillDefinitionService; +import com.bruce.skill.service.ISkillRunner; +import com.bruce.skill.service.impl.SkillVersionServiceImpl; +import com.bruce.skill.vo.SkillVersionVO; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Spy; +import org.mockito.junit.jupiter.MockitoExtension; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.verify; + +@ExtendWith(MockitoExtension.class) +class SkillVersionServiceTests { + + @Mock + private ISkillDefinitionService skillDefinitionService; + + @Mock + private ISkillRunner skillRunner; + + @Spy + private SkillVersionFactory skillVersionFactory; + + @Spy + @InjectMocks + private SkillVersionServiceImpl skillVersionService; + + @Test + void saveDraftShouldRejectEmptySkillContent() { + SkillDefinition definition = new SkillDefinition(); + definition.setId(1001L); + definition.setSkillCode("skill-citation"); + doReturn(definition).when(skillVersionService).loadDefinitionByCode("skill-citation"); + + SkillVersionSaveDTO request = new SkillVersionSaveDTO(); + request.setVersionNo(4); + request.setConfigJson("{}"); + request.setVariableSchemaJson("{\"type\":\"object\"}"); + + assertThrows(IllegalArgumentException.class, () -> skillVersionService.saveDraft("skill-citation", request)); + } + + @Test + void publishShouldPersistPublishedVersion() { + SkillDefinition definition = new SkillDefinition(); + definition.setId(1001L); + definition.setSkillCode("skill-citation"); + definition.setStatus("DRAFT"); + doReturn(definition).when(skillVersionService).loadDefinitionByCode("skill-citation"); + doReturn(null).when(skillVersionService).findBySkillAndVersionNo(1001L, 4); + doAnswer(invocation -> true).when(skillVersionService).save(any(SkillVersion.class)); + + SkillVersionSaveDTO request = new SkillVersionSaveDTO(); + request.setVersionNo(4); + request.setPromptText("你是回答审校器"); + request.setConfigJson("{\"timeout\":3000}"); + request.setVariableSchemaJson("{\"type\":\"object\"}"); + request.setTestResultJson("{\"quality_score\":0.86}"); + request.setRemark("首次发布"); + + boolean result = skillVersionService.publish("skill-citation", request); + assertTrue(result); + + ArgumentCaptor captor = ArgumentCaptor.forClass(SkillVersion.class); + verify(skillVersionService).save(captor.capture()); + assertEquals("PUBLISHED", captor.getValue().getPublishStatus()); + assertEquals(1001L, captor.getValue().getSkillId()); + } + + @Test + void archiveShouldUpdatePublishStatus() { + SkillDefinition definition = new SkillDefinition(); + definition.setId(1001L); + definition.setSkillCode("skill-citation"); + doReturn(definition).when(skillVersionService).loadDefinitionByCode("skill-citation"); + + SkillVersion version = new SkillVersion(); + version.setId(3001L); + version.setSkillId(1001L); + version.setVersionNo(3); + version.setPublishStatus("PUBLISHED"); + doReturn(version).when(skillVersionService).findBySkillAndVersionNo(1001L, 3); + doAnswer(invocation -> true).when(skillVersionService).updateById(any(SkillVersion.class)); + + boolean result = skillVersionService.archive("skill-citation", 3); + + assertTrue(result); + assertEquals("ARCHIVED", version.getPublishStatus()); + } + + @Test + void testDraftShouldReturnTestResultAndPersistDraftSnapshot() { + SkillDefinition definition = new SkillDefinition(); + definition.setId(1001L); + definition.setSkillCode("skill-citation"); + doReturn(definition).when(skillVersionService).loadDefinitionByCode("skill-citation"); + doReturn(null).when(skillVersionService).findBySkillAndVersionNo(1001L, 5); + doAnswer(invocation -> true).when(skillVersionService).save(any(SkillVersion.class)); + + SkillVersionSaveDTO request = new SkillVersionSaveDTO(); + request.setVersionNo(5); + request.setPromptText("你是回答审校器"); + request.setConfigJson("{\"timeout\":3000}"); + request.setVariableSchemaJson("{\"type\":\"object\"}"); + doReturn("{\"quality_score\":0.86}").when(skillRunner).runTest(any(SkillDefinition.class), any(SkillVersion.class)); + + SkillVersionVO result = skillVersionService.test("skill-citation", request); + + assertEquals("DRAFT", result.getPublishStatus()); + assertTrue(result.getTestResultJson().contains("quality_score")); + } +} diff --git a/common-agent-skill/src/test/java/com/bruce/skill/workspace/SkillWorkspaceServiceTests.java b/common-agent-skill/src/test/java/com/bruce/skill/workspace/SkillWorkspaceServiceTests.java new file mode 100644 index 0000000..85a0990 --- /dev/null +++ b/common-agent-skill/src/test/java/com/bruce/skill/workspace/SkillWorkspaceServiceTests.java @@ -0,0 +1,69 @@ +package com.bruce.skill.workspace; + +import com.bruce.skill.entity.SkillDefinition; +import com.bruce.skill.service.ISkillDefinitionService; +import com.bruce.skill.service.ISkillVersionService; +import com.bruce.skill.service.impl.SkillWorkspaceServiceImpl; +import com.bruce.skill.vo.SkillDefinitionVO; +import com.bruce.skill.vo.SkillVersionVO; +import com.bruce.skill.vo.SkillWorkspaceVO; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class SkillWorkspaceServiceTests { + + @Mock + private ISkillDefinitionService skillDefinitionService; + + @Mock + private ISkillVersionService skillVersionService; + + @InjectMocks + private SkillWorkspaceServiceImpl skillWorkspaceService; + + @Test + void getWorkspaceShouldAggregateDefinitionAndVersions() { + SkillDefinition definition = new SkillDefinition(); + definition.setId(1001L); + definition.setSkillCode("skill-citation"); + definition.setSkillName("引用审校 Skill"); + definition.setSkillType("MIXED"); + definition.setStatus("PUBLISHED"); + definition.setDescription("检查答案与引用是否一致"); + + SkillDefinitionVO item = new SkillDefinitionVO(); + item.setId(1001L); + item.setSkillCode("skill-citation"); + item.setSkillName("引用审校 Skill"); + item.setStatus("PUBLISHED"); + + SkillVersionVO version = new SkillVersionVO(); + version.setId(2001L); + version.setSkillId(1001L); + version.setVersionNo(4); + version.setPublishStatus("PUBLISHED"); + version.setTestResultJson("{\"quality_score\":0.86}"); + + when(skillDefinitionService.getByCode("skill-citation")).thenReturn(definition); + when(skillDefinitionService.listDefinitions()).thenReturn(List.of(item)); + when(skillVersionService.listBySkillId(1001L)).thenReturn(List.of(version)); + + SkillWorkspaceVO workspace = skillWorkspaceService.getWorkspace("skill-citation"); + + assertNotNull(workspace); + assertEquals("skill-citation", workspace.getSkillCode()); + assertEquals(1, workspace.getVersions().size()); + assertEquals(Integer.valueOf(4), workspace.getPublishedVersionNo()); + assertEquals("PUBLISHED", workspace.getStatus()); + } +} diff --git a/frontend/src/api/__tests__/skill.spec.ts b/frontend/src/api/__tests__/skill.spec.ts new file mode 100644 index 0000000..4ef51b8 --- /dev/null +++ b/frontend/src/api/__tests__/skill.spec.ts @@ -0,0 +1,65 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +import { + archiveSkillVersion, + getSkillWorkspace, + publishSkillDraft, + saveSkillDraft, + testSkillDraft, +} from '../skill'; +import { get, post } from '../request'; + +vi.mock('../request', () => ({ + get: vi.fn(), + post: vi.fn(), +})); + +describe('skill api', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('maps skill endpoints correctly', () => { + getSkillWorkspace('skill-citation'); + saveSkillDraft('skill-citation', { + versionNo: 4, + promptText: '你是回答审校器', + configJson: '{"timeout":3000}', + variableSchemaJson: '{"type":"object"}', + }); + testSkillDraft('skill-citation', { + versionNo: 4, + promptText: '你是回答审校器', + configJson: '{"timeout":3000}', + variableSchemaJson: '{"type":"object"}', + }); + publishSkillDraft('skill-citation', { + versionNo: 4, + promptText: '你是回答审校器', + configJson: '{"timeout":3000}', + variableSchemaJson: '{"type":"object"}', + }); + archiveSkillVersion('skill-citation', 3); + + expect(get).toHaveBeenCalledWith('/skills/skill-citation'); + expect(post).toHaveBeenCalledWith('/skills/skill-citation/draft', { + versionNo: 4, + promptText: '你是回答审校器', + configJson: '{"timeout":3000}', + variableSchemaJson: '{"type":"object"}', + }); + expect(post).toHaveBeenCalledWith('/skills/skill-citation/test', { + versionNo: 4, + promptText: '你是回答审校器', + configJson: '{"timeout":3000}', + variableSchemaJson: '{"type":"object"}', + }); + expect(post).toHaveBeenCalledWith('/skills/skill-citation/publish', { + versionNo: 4, + promptText: '你是回答审校器', + configJson: '{"timeout":3000}', + variableSchemaJson: '{"type":"object"}', + }); + expect(post).toHaveBeenCalledWith('/skills/skill-citation/archive', undefined, { params: { versionNo: 3 } }); + }); +}); diff --git a/frontend/src/api/skill.ts b/frontend/src/api/skill.ts new file mode 100644 index 0000000..f59d397 --- /dev/null +++ b/frontend/src/api/skill.ts @@ -0,0 +1,62 @@ +import { get, post } from './request'; + +export interface SkillVersionDraft { + id?: string; + skillId?: string; + versionNo: number; + promptText?: string; + codeText?: string; + configJson?: string; + variableSchemaJson?: string; + testResultJson?: string; + publishStatus?: string; + remark?: string; +} + +export interface SkillDefinitionRecord { + id?: string; + skillCode: string; + skillName: string; + skillType: string; + description?: string; + status?: string; + remark?: string; +} + +export interface SkillVersionRecord extends SkillVersionDraft { + skillId: string; + publishedTime?: string; +} + +export interface SkillWorkspace { + skillId: string; + skillCode: string; + skillName: string; + skillType: string; + description?: string; + status?: string; + publishedVersionNo?: number; + latestTestResultJson?: string; + skills: SkillDefinitionRecord[]; + versions: SkillVersionRecord[]; +} + +export function getSkillWorkspace(skillCode: string) { + return get(`/skills/${skillCode}`); +} + +export function saveSkillDraft(skillCode: string, data: SkillVersionDraft) { + return post(`/skills/${skillCode}/draft`, data); +} + +export function testSkillDraft(skillCode: string, data: SkillVersionDraft) { + return post(`/skills/${skillCode}/test`, data); +} + +export function publishSkillDraft(skillCode: string, data: SkillVersionDraft) { + return post(`/skills/${skillCode}/publish`, data); +} + +export function archiveSkillVersion(skillCode: string, versionNo: number) { + return post(`/skills/${skillCode}/archive`, undefined, { params: { versionNo } }); +}