refactor: 精简标准 Skill 包模型

- 统一使用 SKILL.md 与通用资源表达

- 删除仓储、专用资源类型和低价值兼容接口

- 保留安全 ZIP 编解码、校验和内容存储能力
This commit is contained in:
2026-08-14 18:51:43 +08:00
parent b313523aba
commit a34ca9271e
28 changed files with 366 additions and 2295 deletions

View File

@@ -4,7 +4,6 @@ import com.easyagents.skill.exception.SkillPackageException;
import com.easyagents.skill.exception.SkillValidationException;
import com.easyagents.skill.model.Skill;
import com.easyagents.skill.model.SkillDocument;
import com.easyagents.skill.model.SkillMetadata;
import com.easyagents.skill.model.SkillPackage;
import com.easyagents.skill.model.SkillPackageLayout;
import com.easyagents.skill.model.SkillPackageLimits;
@@ -22,7 +21,6 @@ import com.easyagents.skill.validation.SkillValidationIssue;
import com.easyagents.skill.validation.SkillValidationMode;
import com.easyagents.skill.validation.SkillValidationReport;
import com.easyagents.skill.validation.SkillValidationSeverity;
import com.easyagents.skill.validation.SkillValidator;
import com.easyagents.skill.validation.defaults.DefaultSkillValidator;
import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;
import org.apache.commons.compress.archivers.zip.ZipFile;
@@ -62,7 +60,7 @@ import java.util.zip.ZipOutputStream;
/**
* 标准 Skill ZIP 的安全双向流式 Codec。
*/
public class ZipSkillPackageCodec implements SkillPackageCodec, AutoCloseable {
public class ZipSkillPackageCodec implements AutoCloseable {
private static final String DEFAULT_MEDIA_TYPE = "application/octet-stream";
private static final int CENTRAL_DIRECTORY_HEADER_SIZE = 46;
@@ -79,7 +77,6 @@ public class ZipSkillPackageCodec implements SkillPackageCodec, AutoCloseable {
private static final int UINT16_MAX = 0xFFFF;
private final SkillContentStore contentStore;
private final SkillValidator additionalValidator;
private final boolean ownsContentStore;
/**
@@ -88,7 +85,7 @@ public class ZipSkillPackageCodec implements SkillPackageCodec, AutoCloseable {
* <p>该实例拥有默认内容存储,使用完毕后应调用 {@link #close()} 清理临时内容。</p>
*/
public ZipSkillPackageCodec() {
this(new TemporaryFileSkillContentStore(), null, true);
this(new TemporaryFileSkillContentStore(), true);
}
/**
@@ -97,36 +94,17 @@ public class ZipSkillPackageCodec implements SkillPackageCodec, AutoCloseable {
* @param contentStore 二进制内容存储
*/
public ZipSkillPackageCodec(SkillContentStore contentStore) {
this(contentStore, null, false);
this(contentStore, false);
}
/**
* 创建 ZIP Codec。
*
* @param contentStore 二进制内容存储
* @param validator Skill 聚合校验器
*/
public ZipSkillPackageCodec(SkillContentStore contentStore, SkillValidator validator) {
this(contentStore, requireValidator(validator), false);
}
private ZipSkillPackageCodec(SkillContentStore contentStore, SkillValidator validator,
boolean ownsContentStore) {
private ZipSkillPackageCodec(SkillContentStore contentStore, boolean ownsContentStore) {
if (contentStore == null) {
throw new SkillPackageException("Skill content store is required.");
}
this.contentStore = contentStore;
this.additionalValidator = validator;
this.ownsContentStore = ownsContentStore;
}
private static SkillValidator requireValidator(SkillValidator validator) {
if (validator == null) {
throw new SkillPackageException("Skill validator is required.");
}
return validator;
}
/**
* 关闭 Codec 自有的默认临时内容存储。
*
@@ -139,19 +117,6 @@ public class ZipSkillPackageCodec implements SkillPackageCodec, AutoCloseable {
}
}
/**
* 使用兼容入口导入 ZIP。
*
* @param inputStream ZIP 输入流
* @return Skill 列表
* @deprecated 请使用 {@link #decode(InputStream, SkillPackageReadOptions)}。
*/
@Deprecated
@Override
public List<Skill> importZip(InputStream inputStream) {
return decode(inputStream, SkillPackageReadOptions.defaults()).getSkillPackage().getSkills();
}
/**
* 安全解码 Skill ZIP。
*
@@ -159,7 +124,6 @@ public class ZipSkillPackageCodec implements SkillPackageCodec, AutoCloseable {
* @param options 读取选项
* @return 解码结果
*/
@Override
public SkillPackageReadResult decode(InputStream inputStream, SkillPackageReadOptions options) {
if (inputStream == null) {
throw packageError("ZIP_INPUT_REQUIRED", null, "ZIP input stream is required.");
@@ -221,7 +185,6 @@ public class ZipSkillPackageCodec implements SkillPackageCodec, AutoCloseable {
* @param options 写出选项
* @return 编码结果
*/
@Override
public SkillPackageWriteResult encode(SkillPackage skillPackage, OutputStream outputStream,
SkillPackageWriteOptions options) {
if (skillPackage == null || skillPackage.getSkills() == null || skillPackage.getSkills().isEmpty()) {
@@ -370,9 +333,7 @@ public class ZipSkillPackageCodec implements SkillPackageCodec, AutoCloseable {
throw validationPackageError(e,
layout == SkillPackageLayout.ROOT_SKILL ? null : group.root);
}
Map<String, Object> frontmatter = document.getFrontmatter().getValues();
String name = scalar(frontmatter.get("name"));
String description = scalar(frontmatter.get("description"));
String name = scalar(document.getFrontmatter().get("name"));
List<SkillResource> resources = new ArrayList<>();
for (ArchiveFile file : group.files) {
@@ -385,14 +346,9 @@ public class ZipSkillPackageCodec implements SkillPackageCodec, AutoCloseable {
resources.sort(Comparator.comparing(SkillResource::getPath));
Skill skill = new Skill();
skill.setId(null);
skill.setPackageRoot(layout == SkillPackageLayout.ROOT_SKILL ? name : group.root);
skill.setName(name);
skill.setDescription(description);
skill.setMetadata(new SkillMetadata(frontmatter));
skill.setDocument(document);
skill.setResources(resources);
SkillResources.refreshLegacyViews(skill);
return skill;
}
@@ -400,7 +356,7 @@ public class ZipSkillPackageCodec implements SkillPackageCodec, AutoCloseable {
StageTracker stages) throws IOException {
SkillResourceKind kind = SkillResources.classify(file.relativePath);
String mediaType = detectMediaType(file.relativePath);
boolean text = SkillResources.isText(file.relativePath, kind, mediaType);
boolean text = SkillResources.isText(file.relativePath, mediaType);
long singleLimit = text ? limits.getMaxTextFileBytes() : limits.getMaxBinaryFileBytes();
if (file.entry.getSize() > singleLimit) {
throw packageError(text ? "TEXT_FILE_SIZE_LIMIT" : "BINARY_FILE_SIZE_LIMIT", file.fullPath,
@@ -489,24 +445,16 @@ public class ZipSkillPackageCodec implements SkillPackageCodec, AutoCloseable {
}
/**
* 执行 Codec 不可绕过的标准安全校验,并追加调用方业务校验
* 执行 Codec 不可绕过的标准安全校验。
*
* @param skill Skill 聚合
* @param limits 当前读写操作限额
* @param mode 标准校验模式
* @return 合并后的结构化报告
* @return 结构化报告
*/
private SkillValidationReport validateForCodec(Skill skill, SkillPackageLimits limits,
SkillValidationMode mode) {
SkillValidationReport report = new DefaultSkillValidator(limits)
.validateReport(skill, limits, mode);
if (additionalValidator != null) {
SkillValidationReport additionalReport = additionalValidator.getClass() == DefaultSkillValidator.class
? additionalValidator.validateReport(skill, null, mode)
: additionalValidator.validateReport(skill, limits, mode);
report.merge(additionalReport);
}
return report;
return new DefaultSkillValidator(limits).validateReport(skill, limits, mode);
}
private List<OutputFile> prepareOutput(List<Skill> skills, SkillPackageLimits limits) {
@@ -557,7 +505,7 @@ public class ZipSkillPackageCodec implements SkillPackageCodec, AutoCloseable {
files.add(OutputFile.text(skillPath, skillContent));
totalSize = checkedOutputTotal(totalSize, skillSize, limits, skillPath);
List<SkillResource> resources = SkillResources.canonicalResources(skill);
List<SkillResource> resources = new ArrayList<>(skill.getResources());
resources.sort(Comparator.comparing(SkillResource::getPath));
for (SkillResource resource : resources) {
String path = skill.getName() + "/" + SkillPaths.normalize(resource.getPath());
@@ -1533,22 +1481,27 @@ public class ZipSkillPackageCodec implements SkillPackageCodec, AutoCloseable {
}
SkillPackageException rollbackFailure = null;
for (int index = records.size() - 1; index >= 0; index--) {
StageRecord record = records.get(index);
if (record.cleaned) {
continue;
}
try {
contentStore.rollback(records.get(index).stage);
contentStore.rollback(record.stage);
record.cleaned = true;
} catch (RuntimeException cleanupError) {
if (rollbackFailure == null) {
rollbackFailure = new SkillPackageException(
"SKILL_CONTENT_ROLLBACK_ERROR", records.get(index).path,
"SKILL_CONTENT_ROLLBACK_ERROR", record.path,
"Failed to rollback staged Skill package content.", cleanupError);
} else {
rollbackFailure.addSuppressed(cleanupError);
}
}
}
finalized = true;
if (rollbackFailure != null) {
throw rollbackFailure;
}
finalized = true;
}
private void cleanupAfterFailure(Throwable primary) {
@@ -1557,17 +1510,21 @@ public class ZipSkillPackageCodec implements SkillPackageCodec, AutoCloseable {
}
for (int index = records.size() - 1; index >= 0; index--) {
StageRecord record = records.get(index);
if (record.cleaned) {
continue;
}
try {
if (record.committedRef == null) {
contentStore.rollback(record.stage);
} else {
contentStore.release(record.committedRef);
}
record.cleaned = true;
} catch (RuntimeException cleanupError) {
primary.addSuppressed(cleanupError);
}
}
finalized = true;
finalized = records.stream().allMatch(record -> record.cleaned);
}
}
@@ -1577,6 +1534,7 @@ public class ZipSkillPackageCodec implements SkillPackageCodec, AutoCloseable {
private final SkillResource resource;
private final String path;
private String committedRef;
private boolean cleaned;
private StageRecord(SkillContentStage stage, SkillResource resource, String path) {
this.stage = stage;