feat: 重构标准 Skill 管理与发布链路

- 统一标准 ZIP 导入导出与通用资源模型

- 收口分类范围权限和创建人查询

- 完善发布快照、审批幂等与数据库清理迁移
This commit is contained in:
2026-08-14 18:53:26 +08:00
parent 9be9bd7665
commit 77d66e1b42
91 changed files with 1660 additions and 10957 deletions

View File

@@ -153,8 +153,8 @@ public class SkillFileServiceImplTransactionTest {
SkillResource resource = savedResource.get();
assertTrue(resource.getIsText());
assertEquals("SCRIPT", resource.getKind());
assertEquals("PYTHON", resource.getLanguage());
assertEquals("SCRIPT", result.getType());
assertEquals("PYTHON", result.getLanguage());
assertEquals("print('ok')\n", resource.getTextContent());
assertNull(resource.getContentRef());
assertTrue(result.getIsText());
@@ -177,7 +177,7 @@ public class SkillFileServiceImplTransactionTest {
}
/**
* 验证二进制资源重命名到 scripts 后转为文本,并释放原内容引用。
* 验证二进制资源改为文本扩展名后转为文本,并释放原内容引用。
*/
@Test
public void binaryRenameToScriptConvertsAndReleasesContent() {
@@ -197,8 +197,8 @@ public class SkillFileServiceImplTransactionTest {
assertTrue(resource.getIsText());
assertEquals("scripts/tool.py", resource.getNormalizedPath());
assertEquals("SCRIPT", resource.getKind());
assertEquals("PYTHON", resource.getLanguage());
assertEquals("SCRIPT", result.getType());
assertEquals("PYTHON", result.getLanguage());
assertNull(resource.getContentRef());
assertEquals("print('ok')\n", resource.getTextContent());
assertTrue(result.getIsText());
@@ -206,45 +206,82 @@ public class SkillFileServiceImplTransactionTest {
}
/**
* 验证文本资源重命名到 assets 后转为二进制内容引用
* 验证文本资源移动到 assets 后仍按扩展名保留文本表示
*/
@Test
public void textRenameToAssetConvertsToBinaryRepresentation() {
public void textRenameToAssetKeepsTextRepresentation() {
String sourceHash = "c".repeat(64);
SkillResource resource = resource(
"references/guide.md", true, "# Guide\n", null, sourceHash, 8L);
when(skillResourceService.list(any(QueryWrapper.class)))
.thenReturn(List.of(), List.of(resource), List.of(resource), List.of(resource));
when(skillResourceService.update(eq(resource), any(QueryWrapper.class))).thenReturn(true);
when(contentStore.put(any(byte[].class))).thenReturn(NEW_CONTENT_REF);
SkillFileRenameRequest request = renameRequest(
"references/guide.md", "assets/guide.md", sourceHash);
SkillFileContent result = service.renameFile(request);
assertFalse(resource.getIsText());
assertEquals("ASSET", resource.getKind());
assertEquals(NEW_CONTENT_REF, resource.getContentRef());
assertNull(resource.getTextContent());
assertFalse(result.getIsText());
verify(contentStore).put("# Guide\n".getBytes(StandardCharsets.UTF_8));
assertTrue(resource.getIsText());
assertEquals("ASSET", result.getType());
assertNull(resource.getContentRef());
assertEquals("# Guide\n", resource.getTextContent());
assertTrue(result.getIsText());
verify(contentStore, never()).put(any(byte[].class));
}
/**
* 验证 assets 路径不能通过文本创建入口形成非规范表示
* 验证 assets 目录允许创建可编辑文本资源
*/
@Test
public void createTextAssetIsRejected() {
public void createTextAssetUsesCanonicalTextRepresentation() {
AtomicReference<SkillResource> savedResource = new AtomicReference<>();
when(skillResourceService.list(any(QueryWrapper.class)))
.thenAnswer(invocation -> savedResource.get() == null
? List.of() : List.of(savedResource.get()));
when(skillResourceService.save(any(SkillResource.class))).thenAnswer(invocation -> {
SkillResource resource = invocation.getArgument(0);
resource.setId(BigInteger.valueOf(11));
savedResource.set(resource);
return true;
});
SkillFileSaveRequest request = new SkillFileSaveRequest();
request.setSkillId(SKILL_ID);
request.setPath("assets/readme.txt");
request.setContent("text");
BusinessException exception = assertThrows(BusinessException.class,
() -> service.createTextFile(request));
SkillFileContent result = service.createTextFile(request);
assertTrue(exception.getMessage().contains("二进制文件管理"));
verify(skillResourceService, never()).save(any(SkillResource.class));
assertEquals("ASSET", result.getType());
assertTrue(result.getIsText());
assertEquals("text", savedResource.get().getTextContent());
assertNull(savedResource.get().getContentRef());
}
/**
* 验证 scripts 目录中的不透明文件按二进制资源无损保存。
*/
@Test
public void binaryScriptUploadUsesContentStore() {
AtomicReference<SkillResource> savedResource = new AtomicReference<>();
when(skillResourceService.list(any(QueryWrapper.class)))
.thenAnswer(invocation -> savedResource.get() == null
? List.of() : List.of(savedResource.get()));
when(skillResourceService.save(any(SkillResource.class))).thenAnswer(invocation -> {
SkillResource resource = invocation.getArgument(0);
resource.setId(BigInteger.valueOf(12));
savedResource.set(resource);
return true;
});
SkillFileContent result = service.uploadResource(
SKILL_ID, "scripts/helper.bin", new TestMultipartFile(
"helper.bin", new byte[]{0, (byte) 0xFF, 1}));
assertEquals("SCRIPT", result.getType());
assertFalse(result.getIsText());
assertEquals(NEW_CONTENT_REF, savedResource.get().getContentRef());
assertNull(savedResource.get().getTextContent());
verify(contentStore).put(any(MultipartFile.class), anyString());
}
/**
@@ -269,9 +306,9 @@ public class SkillFileServiceImplTransactionTest {
SkillFileContent result = service.createTextFile(request);
assertEquals("SCRIPT", savedResource.get().getKind());
assertEquals("SCRIPT", result.getType());
assertTrue(savedResource.get().getIsText());
assertNull(savedResource.get().getLanguage());
assertNull(result.getLanguage());
assertEquals("text/plain", savedResource.get().getMediaType());
assertEquals("puts 'ok'\n", result.getContent());
}
@@ -284,8 +321,6 @@ public class SkillFileServiceImplTransactionTest {
String sourceHash = "d".repeat(64);
SkillResource resource = resource(
"references/data.json", true, "{}", null, sourceHash, 2L);
resource.setKind("REFERENCE");
resource.setLanguage(null);
resource.setMediaType("application/json");
when(skillResourceService.list(any(QueryWrapper.class)))
.thenReturn(List.of(resource), List.of(resource));
@@ -298,9 +333,9 @@ public class SkillFileServiceImplTransactionTest {
SkillFileContent result = service.saveContent(request);
assertEquals("REFERENCE", resource.getKind());
assertEquals("REFERENCE", result.getType());
assertEquals("application/json", resource.getMediaType());
assertNull(resource.getLanguage());
assertEquals("JSON", result.getLanguage());
assertEquals("application/json", result.getMediaType());
assertEquals("{\"ok\":true}\n", result.getContent());
}
@@ -319,10 +354,8 @@ public class SkillFileServiceImplTransactionTest {
persisted.setTenantId(BigInteger.ONE);
persisted.setCategoryId(BigInteger.valueOf(3));
persisted.setDisplayName("演示 Skill");
persisted.setEnabled(false);
persisted.setVisibilityScope("DEPT");
persisted.setSkillContent(oldContent);
persisted.getMetadataJson().put("owner", "qa");
AtomicReference<Skill> updateRef = new AtomicReference<>();
when(skillService.getOne(any(QueryWrapper.class))).thenReturn(persisted);
when(skillService.updateDraftIfContentMatches(any(Skill.class), eq(oldHash)))
@@ -344,10 +377,7 @@ public class SkillFileServiceImplTransactionTest {
assertEquals(newContent, update.getSkillContent());
assertEquals(persisted.getCategoryId(), update.getCategoryId());
assertEquals(persisted.getDisplayName(), update.getDisplayName());
assertEquals(persisted.getEnabled(), update.getEnabled());
assertEquals(persisted.getVisibilityScope(), update.getVisibilityScope());
assertNotSame(persisted.getMetadataJson(), update.getMetadataJson());
assertEquals(persisted.getMetadataJson(), update.getMetadataJson());
}
/**