feat(skill): 补齐版本测试发布与工作台链路

This commit is contained in:
2026-06-01 04:36:09 +08:00
parent 32925bad8e
commit 29f132e48c
26 changed files with 1144 additions and 0 deletions

View File

@@ -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));
}
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
} }

View File

@@ -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;
} }

View File

@@ -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();
}
}

View File

@@ -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();
}
}

View File

@@ -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> {
}

View File

@@ -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> {
}

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -0,0 +1,8 @@
package com.bruce.skill.service;
import com.bruce.skill.vo.SkillWorkspaceVO;
public interface ISkillWorkspaceService {
SkillWorkspaceVO getWorkspace(String skillCode);
}

View File

@@ -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类型不能为空");
}
}
}

View File

@@ -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;
}
}

View File

@@ -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() : "{}";
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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);
}
}

View File

@@ -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());
}
}

View File

@@ -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"));
}
}

View File

@@ -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());
}
}

View 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
View 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 } });
}