feat(skill): 补齐版本测试发布与工作台链路
This commit is contained in:
@@ -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<SkillWorkspaceVO> detail(@PathVariable("skillCode") String skillCode) {
|
||||||
|
return RequestResult.success(skillWorkspaceService.getWorkspace(skillCode));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/{skillCode}/draft")
|
||||||
|
public RequestResult<Boolean> saveDraft(@PathVariable("skillCode") String skillCode,
|
||||||
|
@RequestBody SkillVersionSaveDTO request) {
|
||||||
|
return RequestResult.success(skillVersionService.saveDraft(skillCode, request));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/{skillCode}/test")
|
||||||
|
public RequestResult<SkillVersionVO> test(@PathVariable("skillCode") String skillCode,
|
||||||
|
@RequestBody SkillVersionSaveDTO request) {
|
||||||
|
return RequestResult.success(skillVersionService.test(skillCode, request));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/{skillCode}/publish")
|
||||||
|
public RequestResult<Boolean> publish(@PathVariable("skillCode") String skillCode,
|
||||||
|
@RequestBody SkillVersionSaveDTO request) {
|
||||||
|
return RequestResult.success(skillVersionService.publish(skillCode, request));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/{skillCode}/archive")
|
||||||
|
public RequestResult<Boolean> archive(@PathVariable("skillCode") String skillCode,
|
||||||
|
@RequestParam("versionNo") Integer versionNo) {
|
||||||
|
return RequestResult.success(skillVersionService.archive(skillCode, versionNo));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -13,6 +13,8 @@ import lombok.EqualsAndHashCode;
|
|||||||
@TableName("skill_definition")
|
@TableName("skill_definition")
|
||||||
public class SkillDefinition extends BaseEntity {
|
public class SkillDefinition extends BaseEntity {
|
||||||
|
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
private String skillCode;
|
private String skillCode;
|
||||||
|
|
||||||
private String skillName;
|
private String skillName;
|
||||||
@@ -22,4 +24,6 @@ public class SkillDefinition extends BaseEntity {
|
|||||||
private String description;
|
private String description;
|
||||||
|
|
||||||
private String status;
|
private String status;
|
||||||
|
|
||||||
|
private String remark;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ import lombok.EqualsAndHashCode;
|
|||||||
@TableName("skill_version")
|
@TableName("skill_version")
|
||||||
public class SkillVersion extends BaseEntity {
|
public class SkillVersion extends BaseEntity {
|
||||||
|
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
private Long skillId;
|
private Long skillId;
|
||||||
|
|
||||||
private Integer versionNo;
|
private Integer versionNo;
|
||||||
@@ -35,4 +37,6 @@ public class SkillVersion extends BaseEntity {
|
|||||||
private String publishStatus;
|
private String publishStatus;
|
||||||
|
|
||||||
private java.time.LocalDateTime publishedTime;
|
private java.time.LocalDateTime publishedTime;
|
||||||
|
|
||||||
|
private String remark;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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<SkillDefinitionVO> toVOList(List<SkillDefinition> 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<SkillVersionVO> toVOList(List<SkillVersion> 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<SkillDefinition> {
|
||||||
|
}
|
||||||
@@ -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<SkillVersion> {
|
||||||
|
}
|
||||||
@@ -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> {
|
||||||
|
|
||||||
|
SkillDefinition getByCode(String skillCode);
|
||||||
|
|
||||||
|
List<SkillDefinitionVO> listDefinitions();
|
||||||
|
|
||||||
|
boolean saveDefinition(SkillDefinitionSaveDTO request);
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -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<SkillVersion> {
|
||||||
|
|
||||||
|
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<SkillVersionVO> listBySkillId(Long skillId);
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
package com.bruce.skill.service;
|
||||||
|
|
||||||
|
import com.bruce.skill.vo.SkillWorkspaceVO;
|
||||||
|
|
||||||
|
public interface ISkillWorkspaceService {
|
||||||
|
|
||||||
|
SkillWorkspaceVO getWorkspace(String skillCode);
|
||||||
|
}
|
||||||
@@ -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<SkillDefinitionMapper, SkillDefinition> 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<SkillDefinitionVO> listDefinitions() {
|
||||||
|
List<SkillDefinitionVO> 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类型不能为空");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<SkillVersionMapper, SkillVersion> 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<SkillVersionVO> listBySkillId(Long skillId) {
|
||||||
|
if (skillId == null) {
|
||||||
|
throw new IllegalArgumentException("Skill ID不能为空");
|
||||||
|
}
|
||||||
|
List<SkillVersionVO> 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() : "{}";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<SkillDefinitionVO> skills = skillDefinitionService.listDefinitions();
|
||||||
|
List<SkillVersionVO> 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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<SkillDefinitionVO> skills;
|
||||||
|
private List<SkillVersionVO> versions;
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<SkillVersion> 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"));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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());
|
||||||
|
}
|
||||||
|
}
|
||||||
65
frontend/src/api/__tests__/skill.spec.ts
Normal file
65
frontend/src/api/__tests__/skill.spec.ts
Normal file
@@ -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 } });
|
||||||
|
});
|
||||||
|
});
|
||||||
62
frontend/src/api/skill.ts
Normal file
62
frontend/src/api/skill.ts
Normal file
@@ -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<SkillWorkspace>(`/skills/${skillCode}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function saveSkillDraft(skillCode: string, data: SkillVersionDraft) {
|
||||||
|
return post<boolean, SkillVersionDraft>(`/skills/${skillCode}/draft`, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function testSkillDraft(skillCode: string, data: SkillVersionDraft) {
|
||||||
|
return post<SkillVersionRecord, SkillVersionDraft>(`/skills/${skillCode}/test`, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function publishSkillDraft(skillCode: string, data: SkillVersionDraft) {
|
||||||
|
return post<boolean, SkillVersionDraft>(`/skills/${skillCode}/publish`, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function archiveSkillVersion(skillCode: string, versionNo: number) {
|
||||||
|
return post<boolean>(`/skills/${skillCode}/archive`, undefined, { params: { versionNo } });
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user