diff --git a/easy-agents-skill/README.md b/easy-agents-skill/README.md index 08f0cd8..3c916ab 100644 --- a/easy-agents-skill/README.md +++ b/easy-agents-skill/README.md @@ -14,6 +14,13 @@ Codec 支持根目录单 Skill、单目录 Skill 和多目录 Skill 三种输入 `SKILL.md` 使用 YAML frontmatter 与 Markdown 正文。未知字段、嵌套 Map/List、布尔值和数字会保留;语义校验通过结构化 issue 返回路径、行列、错误码与修复建议。 +模块内的可移植模型只有两层: + +- `SkillDocument`:`SKILL.md` 原文、frontmatter 和 Markdown 正文 +- `SkillResource`:除 `SKILL.md` 外的任意安全相对路径文件 + +Skill 的数据库主键、分类、权限、发布和审批属于上层技能库,不进入标准包模型。 + ## 推荐调用方式 无参 `ZipSkillPackageCodec` 使用实例级临时文件存储,适合一次性导入导出。它拥有临时目录,必须关闭: @@ -41,10 +48,10 @@ codec.encode(result.getSkillPackage(), outputStream, writeOptions); 校验通过 `SkillValidationMode` 区分两个明确上下文: -- `DRAFT_IMPORT`:ZIP 导入和兼容预检使用;历史下划线名称保留为 warning,允许先进入草稿修复。 +- `DRAFT_IMPORT`:ZIP 导入和草稿编辑使用;可修复的命名问题返回 warning。 - `STANDARD`:正式新建、发布校验和标准 ZIP 导出使用;下划线名称等互操作问题作为 error。 -`SkillFactory.createStrict`、`SkillFactory.createWithResourcesStrict`、`DefaultSkillValidator.validate` 与 `ZipSkillPackageCodec.encode` 执行 `STANDARD` 校验。为保持旧调用兼容,`SkillFactory.create` 仍可构建导入草稿,原有 `validateReport(skill)` 与 `validateReport(skill, limits)` 继续使用 `DRAFT_IMPORT`;新调用方需要显式上下文时使用三参数 `validateReport`。 +`DefaultSkillValidator.validate` 与 `ZipSkillPackageCodec.encode` 执行 `STANDARD` 校验。`SkillFactory.create` 构建草稿;需要指定校验上下文时调用三参数 `validateReport`。 ## 安全边界 @@ -59,14 +66,14 @@ codec.encode(result.getSkillPackage(), outputStream, writeOptions); 限额通过 `SkillPackageLimits` 配置,并由 `SkillPackageReadOptions`、`SkillPackageWriteOptions` 传入单次操作。 -## 从旧接口迁移 +## 编解码结果 -`importZip(InputStream)` 为兼容入口,现已废弃。新调用方应使用 `decode`,以获得: +`decode` 返回: - 包布局 `SkillPackageLayout` - 标准化 `SkillPackage` - 包哈希 - 聚合校验报告 -- `STRICT` 或 `REPORT_ONLY` 读取模式 +- `COMMIT_ON_VALID` 或 `REPORT_ONLY` 读取模式 -写出统一使用 `encode`。自定义校验器是附加业务校验,不能替代 Codec 内置的标准安全校验。 +写出统一使用 `encode`。Codec 始终执行内置标准与安全校验。 diff --git a/easy-agents-skill/src/main/java/com/easyagents/skill/codec/SkillPackageCodec.java b/easy-agents-skill/src/main/java/com/easyagents/skill/codec/SkillPackageCodec.java deleted file mode 100644 index 6da742d..0000000 --- a/easy-agents-skill/src/main/java/com/easyagents/skill/codec/SkillPackageCodec.java +++ /dev/null @@ -1,59 +0,0 @@ -package com.easyagents.skill.codec; - -import com.easyagents.skill.exception.SkillPackageException; -import com.easyagents.skill.model.Skill; -import com.easyagents.skill.model.SkillPackage; -import com.easyagents.skill.model.SkillPackageLayout; -import com.easyagents.skill.validation.SkillValidationReport; - -import java.io.InputStream; -import java.io.OutputStream; -import java.util.List; - -/** - * Skill 包双向流式编解码接口。 - */ -public interface SkillPackageCodec { - - /** - * 从 ZIP 输入流导入 Skill。 - * - * @param inputStream ZIP 输入流 - * @return Skill 列表 - * @throws SkillPackageException ZIP 结构、内容或安全校验失败 - * @deprecated 请使用 {@link #decode(InputStream, SkillPackageReadOptions)} 获取包形态、hash 和诊断。 - */ - @Deprecated - List importZip(InputStream inputStream); - - /** - * 解码 Skill ZIP。 - * - *

兼容默认实现委托旧导入接口;正式 Codec 应覆盖。

- * - * @param inputStream ZIP 输入流 - * @param options 读取选项 - * @return 解码结果 - * @throws SkillPackageException ZIP 结构、内容、安全校验或资源存储失败 - */ - default SkillPackageReadResult decode(InputStream inputStream, SkillPackageReadOptions options) { - List skills = importZip(inputStream); - SkillPackageLayout layout = skills.size() > 1 - ? SkillPackageLayout.MULTI_DIRECTORY : SkillPackageLayout.SINGLE_DIRECTORY; - return new SkillPackageReadResult(new SkillPackage(layout, skills), new SkillValidationReport(), null); - } - - /** - * 将 Skill 包编码为标准 ZIP。 - * - * @param skillPackage Skill 包 - * @param outputStream 输出流,不由本方法关闭 - * @param options 写出选项 - * @return 编码结果 - * @throws SkillPackageException Skill 包不合法、资源不可读或 ZIP 写出失败 - */ - default SkillPackageWriteResult encode(SkillPackage skillPackage, OutputStream outputStream, - SkillPackageWriteOptions options) { - throw new SkillPackageException("This SkillPackageCodec does not support encoding."); - } -} diff --git a/easy-agents-skill/src/main/java/com/easyagents/skill/codec/SkillPackageWriteResult.java b/easy-agents-skill/src/main/java/com/easyagents/skill/codec/SkillPackageWriteResult.java index 35e0b96..f3bdca2 100644 --- a/easy-agents-skill/src/main/java/com/easyagents/skill/codec/SkillPackageWriteResult.java +++ b/easy-agents-skill/src/main/java/com/easyagents/skill/codec/SkillPackageWriteResult.java @@ -12,17 +12,6 @@ public final class SkillPackageWriteResult { private final int entryCount; private final SkillPackageLayout layout; - /** - * 创建编码结果。 - * - * @param packageHash 输出 ZIP SHA-256 - * @param size 输出字节数 - * @param entryCount 输出 entry 数 - */ - public SkillPackageWriteResult(String packageHash, long size, int entryCount) { - this(packageHash, size, entryCount, SkillPackageLayout.SINGLE_DIRECTORY); - } - /** * 创建带包形态的编码结果。 * diff --git a/easy-agents-skill/src/main/java/com/easyagents/skill/codec/ZipSkillPackageCodec.java b/easy-agents-skill/src/main/java/com/easyagents/skill/codec/ZipSkillPackageCodec.java index e7abef7..8f14d13 100644 --- a/easy-agents-skill/src/main/java/com/easyagents/skill/codec/ZipSkillPackageCodec.java +++ b/easy-agents-skill/src/main/java/com/easyagents/skill/codec/ZipSkillPackageCodec.java @@ -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 { *

该实例拥有默认内容存储,使用完毕后应调用 {@link #close()} 清理临时内容。

*/ 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 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 frontmatter = document.getFrontmatter().getValues(); - String name = scalar(frontmatter.get("name")); - String description = scalar(frontmatter.get("description")); + String name = scalar(document.getFrontmatter().get("name")); List 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 prepareOutput(List 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 resources = SkillResources.canonicalResources(skill); + List 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; diff --git a/easy-agents-skill/src/main/java/com/easyagents/skill/factory/SkillFactory.java b/easy-agents-skill/src/main/java/com/easyagents/skill/factory/SkillFactory.java index 207816e..af4b250 100644 --- a/easy-agents-skill/src/main/java/com/easyagents/skill/factory/SkillFactory.java +++ b/easy-agents-skill/src/main/java/com/easyagents/skill/factory/SkillFactory.java @@ -2,8 +2,6 @@ package com.easyagents.skill.factory; import com.easyagents.skill.model.*; import com.easyagents.skill.util.SkillFrontmatter; -import com.easyagents.skill.util.SkillResources; -import com.easyagents.skill.validation.defaults.DefaultSkillValidator; import java.util.List; import java.util.Map; @@ -19,92 +17,37 @@ public final class SkillFactory { /** * 基于 SKILL.md 内容创建 Skill。 * - * @param id Skill ID * @param skillContent SKILL.md 原始内容 * @return Skill 聚合 */ - public static Skill create(String id, String skillContent) { - return create(id, skillContent, null, null, null); - } - - /** - * 基于 SKILL.md 内容创建并严格校验正式标准 Skill。 - * - * @param id Skill ID - * @param skillContent SKILL.md 原始内容 - * @return 通过正式标准校验的 Skill 聚合 - */ - public static Skill createStrict(String id, String skillContent) { - Skill skill = create(id, skillContent); - new DefaultSkillValidator().validate(skill); + public static Skill create(String skillContent) { + SkillDocument document = SkillFrontmatter.parseDocument(skillContent); + Map values = document.getFrontmatter().getValues(); + requiredScalar(values, "name"); + requiredScalar(values, "description"); + Skill skill = new Skill(); + skill.setDocument(document); return skill; } /** * 基于 SKILL.md 内容和通用资源创建 Skill。 * - * @param id 仓储 ID,可为空 * @param skillContent SKILL.md 原始内容 * @param resources 通用资源列表 * @return Skill 聚合 */ - public static Skill createWithResources(String id, String skillContent, List resources) { - Skill skill = create(id, skillContent); + public static Skill createWithResources(String skillContent, List resources) { + Skill skill = create(skillContent); skill.setResources(resources); - SkillResources.refreshLegacyViews(skill); return skill; } - /** - * 基于 SKILL.md 和通用资源创建并严格校验正式标准 Skill。 - * - * @param id 仓储 ID,可为空 - * @param skillContent SKILL.md 原始内容 - * @param resources 通用资源列表 - * @return 通过正式标准校验的 Skill 聚合 - */ - public static Skill createWithResourcesStrict(String id, String skillContent, - List resources) { - Skill skill = createWithResources(id, skillContent, resources); - new DefaultSkillValidator().validate(skill); - return skill; - } - - /** - * 基于 SKILL.md 内容和资源列表创建 Skill。 - * - * @param id Skill ID - * @param skillContent SKILL.md 原始内容 - * @param references reference 文档列表 - * @param scripts script 脚本列表 - * @param assets asset 资产列表 - * @return Skill 聚合 - */ - public static Skill create(String id, String skillContent, List references, - List scripts, List assets) { - SkillDocument document = SkillFrontmatter.parseDocument(skillContent); - Map values = document.getFrontmatter().getValues(); - String name = requiredScalar(values, "name"); - String description = requiredScalar(values, "description"); - Skill skill = new Skill(); - skill.setId(id); - skill.setName(name); - skill.setDescription(description); - skill.setMetadata(new SkillMetadata(values)); - skill.setDocument(document); - skill.setReferences(references); - skill.setScripts(scripts); - skill.setAssets(assets); - skill.setResources(SkillResources.canonicalResources(skill)); - return skill; - } - - private static String requiredScalar(Map values, String key) { + private static void requiredScalar(Map values, String key) { Object value = values.get(key); if (!(value instanceof String text) || text.isBlank()) { throw new com.easyagents.skill.exception.SkillValidationException( "SKILL.md frontmatter " + key + " must be a non-blank string."); } - return text; } } diff --git a/easy-agents-skill/src/main/java/com/easyagents/skill/model/Skill.java b/easy-agents-skill/src/main/java/com/easyagents/skill/model/Skill.java index 0cd7f9a..94145a3 100644 --- a/easy-agents-skill/src/main/java/com/easyagents/skill/model/Skill.java +++ b/easy-agents-skill/src/main/java/com/easyagents/skill/model/Skill.java @@ -1,7 +1,5 @@ package com.easyagents.skill.model; -import com.easyagents.skill.util.SkillResources; - import java.io.Serializable; import java.util.ArrayList; import java.util.List; @@ -13,36 +11,10 @@ public class Skill implements Serializable { private static final long serialVersionUID = 1L; - private String id; private String packageRoot; - private String name; - private String description; - private SkillMetadata metadata = new SkillMetadata(); private String skillContent; private SkillDocument document; private List resources = new ArrayList<>(); - private boolean resourcesInitialized; - private List references = new ArrayList<>(); - private List scripts = new ArrayList<>(); - private List assets = new ArrayList<>(); - - /** - * 获取 Skill ID。 - * - * @return Skill ID - */ - public String getId() { - return id; - } - - /** - * 设置 Skill ID。 - * - * @param id Skill ID - */ - public void setId(String id) { - this.id = id; - } /** * 获取导入包中的顶层目录名;该值不是仓储 ID。 @@ -68,16 +40,7 @@ public class Skill implements Serializable { * @return 名称 */ public String getName() { - return name; - } - - /** - * 设置名称。 - * - * @param name 名称 - */ - public void setName(String name) { - this.name = name; + return frontmatterString("name"); } /** @@ -86,34 +49,7 @@ public class Skill implements Serializable { * @return 描述 */ public String getDescription() { - return description; - } - - /** - * 设置描述。 - * - * @param description 描述 - */ - public void setDescription(String description) { - this.description = description; - } - - /** - * 获取元数据。 - * - * @return 元数据 - */ - public SkillMetadata getMetadata() { - return metadata; - } - - /** - * 设置元数据。 - * - * @param metadata 元数据 - */ - public void setMetadata(SkillMetadata metadata) { - this.metadata = metadata == null ? new SkillMetadata() : metadata; + return frontmatterString("description"); } /** @@ -152,12 +88,6 @@ public class Skill implements Serializable { public void setDocument(SkillDocument document) { this.document = document; this.skillContent = document == null ? null : document.render(); - if (document != null) { - java.util.Map values = document.getFrontmatter().getValues(); - this.metadata = new SkillMetadata(values); - this.name = values.get("name") instanceof String text ? text : null; - this.description = values.get("description") instanceof String text ? text : null; - } } /** @@ -166,12 +96,6 @@ public class Skill implements Serializable { * @return 通用资源列表 */ public List getResources() { - if (!resourcesInitialized) { - resources = resources == null || resources.isEmpty() - ? new ArrayList<>(SkillResources.fromLegacyViews(this)) - : new ArrayList<>(resources); - resourcesInitialized = true; - } return resources; } @@ -182,81 +106,13 @@ public class Skill implements Serializable { */ public void setResources(List resources) { this.resources = resources == null ? new ArrayList<>() : new ArrayList<>(resources); - this.resourcesInitialized = true; } - /** - * 判断正式通用资源列表是否已被显式初始化。 - * - *

该标记用于区分“尚未迁移的旧资源视图”和“调用方明确设置的空资源列表”, - * 避免删除最后一个正式资源后又从旧兼容视图恢复该资源。

- * - * @return 已显式设置通用资源列表时为 true - */ - public boolean isResourcesInitialized() { - return resourcesInitialized; - } - - /** - * 获取参考文档列表。 - * - * @return 参考文档列表 - */ - public List getReferences() { - return references; - } - - /** - * 设置参考文档列表。 - * - * @param references 参考文档列表 - */ - public void setReferences(List references) { - this.references = references == null ? new ArrayList<>() : new ArrayList<>(references); - } - - /** - * 获取脚本列表。 - * - * @return 脚本列表 - */ - public List getScripts() { - return scripts; - } - - /** - * 设置脚本列表。 - * - * @param scripts 脚本列表 - */ - public void setScripts(List scripts) { - this.scripts = scripts == null ? new ArrayList<>() : new ArrayList<>(scripts); - } - - /** - * 获取资产列表。 - * - * @return 资产列表 - */ - public List getAssets() { - return assets; - } - - /** - * 设置资产列表。 - * - * @param assets 资产列表 - */ - public void setAssets(List assets) { - this.assets = assets == null ? new ArrayList<>() : new ArrayList<>(assets); - } - - /** - * 转换为轻量描述。 - * - * @return Skill 描述 - */ - public SkillDescriptor toDescriptor() { - return new SkillDescriptor(id, name, description, metadata); + private String frontmatterString(String key) { + if (document == null) { + return null; + } + Object value = document.getFrontmatter().get(key); + return value instanceof String text ? text : null; } } diff --git a/easy-agents-skill/src/main/java/com/easyagents/skill/model/SkillAsset.java b/easy-agents-skill/src/main/java/com/easyagents/skill/model/SkillAsset.java deleted file mode 100644 index 796d7c7..0000000 --- a/easy-agents-skill/src/main/java/com/easyagents/skill/model/SkillAsset.java +++ /dev/null @@ -1,148 +0,0 @@ -package com.easyagents.skill.model; - -import java.io.Serializable; - -/** - * Skill 静态资产兼容视图。 - * - * @deprecated 请使用 {@link SkillResource}。 - */ -@Deprecated -public class SkillAsset implements Serializable { - - private static final long serialVersionUID = 1L; - - private String path; - private String name; - private String mediaType; - private String contentRef; - private String contentHash; - private long size; - private SkillMetadata metadata = new SkillMetadata(); - - /** - * 获取逻辑路径。 - * - * @return 逻辑路径 - */ - public String getPath() { - return path; - } - - /** - * 设置逻辑路径。 - * - * @param path 逻辑路径 - */ - public void setPath(String path) { - this.path = path; - } - - /** - * 获取名称。 - * - * @return 名称 - */ - public String getName() { - return name; - } - - /** - * 设置名称。 - * - * @param name 名称 - */ - public void setName(String name) { - this.name = name; - } - - /** - * 获取媒体类型。 - * - * @return 媒体类型 - */ - public String getMediaType() { - return mediaType; - } - - /** - * 设置媒体类型。 - * - * @param mediaType 媒体类型 - */ - public void setMediaType(String mediaType) { - this.mediaType = mediaType; - } - - /** - * 获取内容引用。 - * - * @return 内容引用 - */ - public String getContentRef() { - return contentRef; - } - - /** - * 设置内容引用。 - * - * @param contentRef 内容引用 - */ - public void setContentRef(String contentRef) { - this.contentRef = contentRef; - } - - /** - * 获取内容 hash。 - * - * @return 内容 hash - */ - public String getContentHash() { - return contentHash; - } - - /** - * 设置内容 hash。 - * - * @param contentHash 内容 hash - */ - public void setContentHash(String contentHash) { - this.contentHash = contentHash; - } - - /** - * 获取文件大小。 - * - * @return 文件大小 - */ - public long getSize() { - return size; - } - - /** - * 设置文件大小。 - * - * @param size 文件大小 - */ - public void setSize(long size) { - this.size = size; - } - - /** - * 获取元数据。 - * - * @return 元数据 - */ - public SkillMetadata getMetadata() { - return metadata; - } - - /** - * 设置元数据。 - * - * @param metadata 元数据 - */ - public void setMetadata(SkillMetadata metadata) { - this.metadata = metadata == null ? new SkillMetadata() : metadata; - } -} diff --git a/easy-agents-skill/src/main/java/com/easyagents/skill/model/SkillDescriptor.java b/easy-agents-skill/src/main/java/com/easyagents/skill/model/SkillDescriptor.java deleted file mode 100644 index a25df10..0000000 --- a/easy-agents-skill/src/main/java/com/easyagents/skill/model/SkillDescriptor.java +++ /dev/null @@ -1,111 +0,0 @@ -package com.easyagents.skill.model; - -import java.io.Serial; -import java.io.Serializable; - -/** - * Skill 轻量描述。 - */ -public class SkillDescriptor implements Serializable { - - @Serial - private static final long serialVersionUID = 1L; - - private String id; - private String name; - private String description; - private SkillMetadata metadata = new SkillMetadata(); - - /** - * 创建空 Skill 描述。 - */ - public SkillDescriptor() { - } - - /** - * 创建 Skill 描述。 - * - * @param id Skill ID - * @param name Skill 名称 - * @param description Skill 描述 - * @param metadata 元数据 - */ - public SkillDescriptor(String id, String name, String description, SkillMetadata metadata) { - this.id = id; - this.name = name; - this.description = description; - setMetadata(metadata); - } - - /** - * 获取 Skill ID。 - * - * @return Skill ID - */ - public String getId() { - return id; - } - - /** - * 设置 Skill ID。 - * - * @param id Skill ID - */ - public void setId(String id) { - this.id = id; - } - - /** - * 获取名称。 - * - * @return 名称 - */ - public String getName() { - return name; - } - - /** - * 设置名称。 - * - * @param name 名称 - */ - public void setName(String name) { - this.name = name; - } - - /** - * 获取描述。 - * - * @return 描述 - */ - public String getDescription() { - return description; - } - - /** - * 设置描述。 - * - * @param description 描述 - */ - public void setDescription(String description) { - this.description = description; - } - - /** - * 获取元数据。 - * - * @return 元数据 - */ - public SkillMetadata getMetadata() { - return metadata; - } - - /** - * 设置元数据。 - * - * @param metadata 元数据 - */ - public void setMetadata(SkillMetadata metadata) { - this.metadata = metadata == null ? new SkillMetadata() : metadata; - } -} diff --git a/easy-agents-skill/src/main/java/com/easyagents/skill/model/SkillReference.java b/easy-agents-skill/src/main/java/com/easyagents/skill/model/SkillReference.java deleted file mode 100644 index 80657d1..0000000 --- a/easy-agents-skill/src/main/java/com/easyagents/skill/model/SkillReference.java +++ /dev/null @@ -1,129 +0,0 @@ -package com.easyagents.skill.model; - -import java.io.Serializable; - -/** - * Skill Markdown 参考文档兼容视图。 - * - * @deprecated 请使用 {@link SkillResource}。 - */ -@Deprecated -public class SkillReference implements Serializable { - - private static final long serialVersionUID = 1L; - - private String path; - private String name; - private String content; - private String contentHash; - private long size; - private SkillMetadata metadata = new SkillMetadata(); - - /** - * 获取逻辑路径。 - * - * @return 逻辑路径 - */ - public String getPath() { - return path; - } - - /** - * 设置逻辑路径。 - * - * @param path 逻辑路径 - */ - public void setPath(String path) { - this.path = path; - } - - /** - * 获取名称。 - * - * @return 名称 - */ - public String getName() { - return name; - } - - /** - * 设置名称。 - * - * @param name 名称 - */ - public void setName(String name) { - this.name = name; - } - - /** - * 获取 Markdown 内容。 - * - * @return Markdown 内容 - */ - public String getContent() { - return content; - } - - /** - * 设置 Markdown 内容。 - * - * @param content Markdown 内容 - */ - public void setContent(String content) { - this.content = content; - } - - /** - * 获取内容 hash。 - * - * @return 内容 hash - */ - public String getContentHash() { - return contentHash; - } - - /** - * 设置内容 hash。 - * - * @param contentHash 内容 hash - */ - public void setContentHash(String contentHash) { - this.contentHash = contentHash; - } - - /** - * 获取文件大小。 - * - * @return 文件大小 - */ - public long getSize() { - return size; - } - - /** - * 设置文件大小。 - * - * @param size 文件大小 - */ - public void setSize(long size) { - this.size = size; - } - - /** - * 获取元数据。 - * - * @return 元数据 - */ - public SkillMetadata getMetadata() { - return metadata; - } - - /** - * 设置元数据。 - * - * @param metadata 元数据 - */ - public void setMetadata(SkillMetadata metadata) { - this.metadata = metadata == null ? new SkillMetadata() : metadata; - } -} diff --git a/easy-agents-skill/src/main/java/com/easyagents/skill/model/SkillResource.java b/easy-agents-skill/src/main/java/com/easyagents/skill/model/SkillResource.java index cc950fe..9a7a5ae 100644 --- a/easy-agents-skill/src/main/java/com/easyagents/skill/model/SkillResource.java +++ b/easy-agents-skill/src/main/java/com/easyagents/skill/model/SkillResource.java @@ -18,7 +18,6 @@ public class SkillResource implements Serializable { private String contentRef; private String contentHash; private long size; - private SkillMetadata metadata = new SkillMetadata(); /** * 获取 Skill 根目录相对路径。 @@ -146,24 +145,6 @@ public class SkillResource implements Serializable { this.size = size; } - /** - * 获取资源扩展元数据。 - * - * @return 扩展元数据 - */ - public SkillMetadata getMetadata() { - return metadata; - } - - /** - * 设置资源扩展元数据。 - * - * @param metadata 扩展元数据 - */ - public void setMetadata(SkillMetadata metadata) { - this.metadata = metadata == null ? new SkillMetadata() : metadata; - } - /** * 判断资源是否以内联文本保存。 * diff --git a/easy-agents-skill/src/main/java/com/easyagents/skill/model/SkillScript.java b/easy-agents-skill/src/main/java/com/easyagents/skill/model/SkillScript.java deleted file mode 100644 index 4679906..0000000 --- a/easy-agents-skill/src/main/java/com/easyagents/skill/model/SkillScript.java +++ /dev/null @@ -1,129 +0,0 @@ -package com.easyagents.skill.model; - -import java.io.Serializable; - -/** - * Skill 脚本源码兼容视图。 - * - * @deprecated 请使用 {@link SkillResource}。 - */ -@Deprecated -public class SkillScript implements Serializable { - - private static final long serialVersionUID = 1L; - - private String path; - private SkillScriptLanguage language = SkillScriptLanguage.UNKNOWN; - private String content; - private String contentHash; - private long size; - private SkillMetadata metadata = new SkillMetadata(); - - /** - * 获取逻辑路径。 - * - * @return 逻辑路径 - */ - public String getPath() { - return path; - } - - /** - * 设置逻辑路径。 - * - * @param path 逻辑路径 - */ - public void setPath(String path) { - this.path = path; - } - - /** - * 获取脚本语言。 - * - * @return 脚本语言 - */ - public SkillScriptLanguage getLanguage() { - return language; - } - - /** - * 设置脚本语言。 - * - * @param language 脚本语言 - */ - public void setLanguage(SkillScriptLanguage language) { - this.language = language == null ? SkillScriptLanguage.UNKNOWN : language; - } - - /** - * 获取脚本源码。 - * - * @return 脚本源码 - */ - public String getContent() { - return content; - } - - /** - * 设置脚本源码。 - * - * @param content 脚本源码 - */ - public void setContent(String content) { - this.content = content; - } - - /** - * 获取内容 hash。 - * - * @return 内容 hash - */ - public String getContentHash() { - return contentHash; - } - - /** - * 设置内容 hash。 - * - * @param contentHash 内容 hash - */ - public void setContentHash(String contentHash) { - this.contentHash = contentHash; - } - - /** - * 获取文件大小。 - * - * @return 文件大小 - */ - public long getSize() { - return size; - } - - /** - * 设置文件大小。 - * - * @param size 文件大小 - */ - public void setSize(long size) { - this.size = size; - } - - /** - * 获取元数据。 - * - * @return 元数据 - */ - public SkillMetadata getMetadata() { - return metadata; - } - - /** - * 设置元数据。 - * - * @param metadata 元数据 - */ - public void setMetadata(SkillMetadata metadata) { - this.metadata = metadata == null ? new SkillMetadata() : metadata; - } -} diff --git a/easy-agents-skill/src/main/java/com/easyagents/skill/model/SkillScriptLanguage.java b/easy-agents-skill/src/main/java/com/easyagents/skill/model/SkillScriptLanguage.java deleted file mode 100644 index c9ce329..0000000 --- a/easy-agents-skill/src/main/java/com/easyagents/skill/model/SkillScriptLanguage.java +++ /dev/null @@ -1,50 +0,0 @@ -package com.easyagents.skill.model; - -/** - * Skill 脚本语言。 - */ -public enum SkillScriptLanguage { - - /** - * Python 脚本。 - */ - PYTHON, - - /** - * JavaScript 脚本。 - */ - JAVASCRIPT, - - /** - * Shell 脚本。 - */ - SHELL, - - /** - * 未知脚本语言。 - */ - UNKNOWN; - - /** - * 按脚本路径识别语言。 - * - * @param path 脚本逻辑路径 - * @return 脚本语言 - */ - public static SkillScriptLanguage fromPath(String path) { - if (path == null) { - return UNKNOWN; - } - String lower = path.toLowerCase(); - if (lower.endsWith(".py")) { - return PYTHON; - } - if (lower.endsWith(".js")) { - return JAVASCRIPT; - } - if (lower.endsWith(".sh")) { - return SHELL; - } - return UNKNOWN; - } -} diff --git a/easy-agents-skill/src/main/java/com/easyagents/skill/repository/SkillRepository.java b/easy-agents-skill/src/main/java/com/easyagents/skill/repository/SkillRepository.java deleted file mode 100644 index 03552ee..0000000 --- a/easy-agents-skill/src/main/java/com/easyagents/skill/repository/SkillRepository.java +++ /dev/null @@ -1,58 +0,0 @@ -package com.easyagents.skill.repository; - -import com.easyagents.skill.model.Skill; -import com.easyagents.skill.model.SkillDescriptor; - -import java.util.List; -import java.util.Optional; - -/** - * Skill 聚合存储接口。 - */ -public interface SkillRepository { - - /** - * 保存 Skill。 - * - * @param skill Skill 聚合 - */ - void save(Skill skill); - - /** - * 获取完整 Skill。 - * - * @param skillId Skill ID - * @return Skill 聚合 - */ - Optional get(String skillId); - - /** - * 获取 Skill 描述。 - * - * @param skillId Skill ID - * @return Skill 描述 - */ - Optional getDescriptor(String skillId); - - /** - * 列出 Skill 描述。 - * - * @return Skill 描述列表 - */ - List listDescriptors(); - - /** - * 删除 Skill。 - * - * @param skillId Skill ID - */ - void delete(String skillId); - - /** - * 判断 Skill 是否存在。 - * - * @param skillId Skill ID - * @return 存在时为 true - */ - boolean exists(String skillId); -} diff --git a/easy-agents-skill/src/main/java/com/easyagents/skill/repository/memory/InMemorySkillRepository.java b/easy-agents-skill/src/main/java/com/easyagents/skill/repository/memory/InMemorySkillRepository.java deleted file mode 100644 index 867e1b4..0000000 --- a/easy-agents-skill/src/main/java/com/easyagents/skill/repository/memory/InMemorySkillRepository.java +++ /dev/null @@ -1,214 +0,0 @@ -package com.easyagents.skill.repository.memory; - -import com.easyagents.skill.exception.SkillValidationException; -import com.easyagents.skill.model.*; -import com.easyagents.skill.repository.SkillRepository; - -import java.util.ArrayList; -import java.util.List; -import java.util.Optional; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ConcurrentMap; - -/** - * 基于内存的 Skill 聚合仓储实现。 - */ -public class InMemorySkillRepository implements SkillRepository { - - private final ConcurrentMap skills = new ConcurrentHashMap<>(); - - /** - * 保存 Skill 聚合。 - * - * @param skill Skill 聚合 - */ - @Override - public void save(Skill skill) { - if (skill == null || isBlank(skill.getId())) { - throw new SkillValidationException("Skill id is required."); - } - skills.put(skill.getId(), copySkill(skill)); - } - - /** - * 获取完整 Skill。 - * - * @param skillId Skill ID - * @return Skill 聚合 - */ - @Override - public Optional get(String skillId) { - Skill skill = skills.get(skillId); - return skill == null ? Optional.empty() : Optional.of(copySkill(skill)); - } - - /** - * 获取 Skill 描述。 - * - * @param skillId Skill ID - * @return Skill 描述 - */ - @Override - public Optional getDescriptor(String skillId) { - Skill skill = skills.get(skillId); - return skill == null ? Optional.empty() : Optional.of(copyDescriptor(skill.toDescriptor())); - } - - /** - * 列出 Skill 描述。 - * - * @return Skill 描述列表 - */ - @Override - public List listDescriptors() { - List descriptors = new ArrayList<>(); - for (Skill skill : skills.values()) { - descriptors.add(copyDescriptor(skill.toDescriptor())); - } - return descriptors; - } - - /** - * 删除 Skill。 - * - * @param skillId Skill ID - */ - @Override - public void delete(String skillId) { - skills.remove(skillId); - } - - /** - * 判断 Skill 是否存在。 - * - * @param skillId Skill ID - * @return 存在时为 true - */ - @Override - public boolean exists(String skillId) { - return skills.containsKey(skillId); - } - - private static Skill copySkill(Skill source) { - Skill target = new Skill(); - target.setId(source.getId()); - target.setPackageRoot(source.getPackageRoot()); - target.setName(source.getName()); - target.setDescription(source.getDescription()); - target.setMetadata(copyMetadata(source.getMetadata())); - SkillDocument copiedDocument = copyDocument(source.getDocument()); - if (copiedDocument == null) { - target.setSkillContent(source.getSkillContent()); - } else { - target.setDocument(copiedDocument); - } - target.setResources(copyResources(source.getResources())); - target.setReferences(copyReferences(source.getReferences())); - target.setScripts(copyScripts(source.getScripts())); - target.setAssets(copyAssets(source.getAssets())); - return target; - } - - private static SkillDocument copyDocument(SkillDocument source) { - if (source == null) { - return null; - } - SkillDocument target = new SkillDocument(source.getRawContent(), - source.getFrontmatter().getValues(), source.getMarkdownBody(), - source.getFrontmatterLocations(), source.getDiagnostics()); - target.setModified(source.isModified()); - return target; - } - - private static List copyResources(List sources) { - List targets = new ArrayList<>(); - if (sources == null) { - return targets; - } - for (SkillResource source : sources) { - SkillResource target = new SkillResource(); - target.setPath(source.getPath()); - target.setKind(source.getKind()); - target.setMediaType(source.getMediaType()); - target.setTextContent(source.getTextContent()); - target.setContentRef(source.getContentRef()); - target.setContentHash(source.getContentHash()); - target.setSize(source.getSize()); - target.setMetadata(copyMetadata(source.getMetadata())); - targets.add(target); - } - return targets; - } - - private static List copyReferences(List sources) { - List targets = new ArrayList<>(); - if (sources == null) { - return targets; - } - for (SkillReference source : sources) { - SkillReference target = new SkillReference(); - target.setPath(source.getPath()); - target.setName(source.getName()); - target.setContent(source.getContent()); - target.setContentHash(source.getContentHash()); - target.setSize(source.getSize()); - target.setMetadata(copyMetadata(source.getMetadata())); - targets.add(target); - } - return targets; - } - - private static List copyScripts(List sources) { - List targets = new ArrayList<>(); - if (sources == null) { - return targets; - } - for (SkillScript source : sources) { - SkillScript target = new SkillScript(); - target.setPath(source.getPath()); - target.setLanguage(source.getLanguage()); - target.setContent(source.getContent()); - target.setContentHash(source.getContentHash()); - target.setSize(source.getSize()); - target.setMetadata(copyMetadata(source.getMetadata())); - targets.add(target); - } - return targets; - } - - private static List copyAssets(List sources) { - List targets = new ArrayList<>(); - if (sources == null) { - return targets; - } - for (SkillAsset source : sources) { - SkillAsset target = new SkillAsset(); - target.setPath(source.getPath()); - target.setName(source.getName()); - target.setMediaType(source.getMediaType()); - target.setContentRef(source.getContentRef()); - target.setContentHash(source.getContentHash()); - target.setSize(source.getSize()); - target.setMetadata(copyMetadata(source.getMetadata())); - targets.add(target); - } - return targets; - } - - private static SkillDescriptor copyDescriptor(SkillDescriptor source) { - return new SkillDescriptor( - source.getId(), - source.getName(), - source.getDescription(), - copyMetadata(source.getMetadata()) - ); - } - - private static SkillMetadata copyMetadata(SkillMetadata source) { - return source == null ? new SkillMetadata() : new SkillMetadata(source.getValues()); - } - - private static boolean isBlank(String value) { - return value == null || value.isBlank(); - } -} diff --git a/easy-agents-skill/src/main/java/com/easyagents/skill/store/SkillContentStage.java b/easy-agents-skill/src/main/java/com/easyagents/skill/store/SkillContentStage.java index c94e138..6db9b80 100644 --- a/easy-agents-skill/src/main/java/com/easyagents/skill/store/SkillContentStage.java +++ b/easy-agents-skill/src/main/java/com/easyagents/skill/store/SkillContentStage.java @@ -1,6 +1,5 @@ package com.easyagents.skill.store; -import java.nio.file.Path; import java.util.Objects; /** @@ -12,8 +11,6 @@ public final class SkillContentStage { private final String contentRef; private final String contentHash; private final long size; - private final boolean alreadyCommitted; - private final Path compatibilityPath; /** * 创建内容阶段结果。 @@ -22,21 +19,12 @@ public final class SkillContentStage { * @param contentRef 最终内容引用 * @param contentHash SHA-256 * @param size 字节大小 - * @param alreadyCommitted 是否由兼容实现提前写入正式存储 */ - public SkillContentStage(String stageId, String contentRef, String contentHash, - long size, boolean alreadyCommitted) { - this(stageId, contentRef, contentHash, size, alreadyCommitted, null); - } - - private SkillContentStage(String stageId, String contentRef, String contentHash, - long size, boolean alreadyCommitted, Path compatibilityPath) { + public SkillContentStage(String stageId, String contentRef, String contentHash, long size) { this.stageId = Objects.requireNonNull(stageId, "stageId"); this.contentRef = Objects.requireNonNull(contentRef, "contentRef"); this.contentHash = Objects.requireNonNull(contentHash, "contentHash"); this.size = size; - this.alreadyCommitted = alreadyCommitted; - this.compatibilityPath = compatibilityPath; } /** @return 暂存标识 */ @@ -59,18 +47,4 @@ public final class SkillContentStage { return size; } - /** @return 已提前提交时为 true */ - public boolean isAlreadyCommitted() { - return alreadyCommitted; - } - - static SkillContentStage compatibility(Path path, String contentHash, long size) { - String contentRef = "sha256:" + contentHash; - return new SkillContentStage(contentRef, contentRef, contentHash, - size, false, path); - } - - Path compatibilityPath() { - return compatibilityPath; - } } diff --git a/easy-agents-skill/src/main/java/com/easyagents/skill/store/SkillContentStore.java b/easy-agents-skill/src/main/java/com/easyagents/skill/store/SkillContentStore.java index 99f39af..404f094 100644 --- a/easy-agents-skill/src/main/java/com/easyagents/skill/store/SkillContentStore.java +++ b/easy-agents-skill/src/main/java/com/easyagents/skill/store/SkillContentStore.java @@ -1,16 +1,6 @@ package com.easyagents.skill.store; -import com.easyagents.skill.exception.SkillException; -import com.easyagents.skill.util.SkillHashes; - -import java.io.ByteArrayOutputStream; -import java.io.IOException; import java.io.InputStream; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.StandardOpenOption; -import java.security.DigestOutputStream; -import java.security.MessageDigest; /** * Skill 二进制内容流式存储与引用生命周期接口。 @@ -25,62 +15,14 @@ public interface SkillContentStore { */ String put(byte[] bytes); - /** - * 流式保存内容并返回内容引用。 - * - *

兼容默认实现仅缓存单个文件;正式持久化实现应覆盖该方法以直接流式写入。

- * - * @param inputStream 内容流,不由本方法关闭 - * @param maxBytes 最大允许字节数 - * @return 内容引用 - */ - default String put(InputStream inputStream, long maxBytes) { - return put(readBounded(inputStream, maxBytes)); - } - /** * 暂存内容,供完成全包校验后统一提交。 * - *

为兼容旧实现,默认实现会立即写入;正式实现应覆盖并提供真实暂存区。

- * * @param inputStream 内容流,不由本方法关闭 * @param maxBytes 最大允许字节数 * @return 暂存结果 */ - default SkillContentStage stage(InputStream inputStream, long maxBytes) { - if (inputStream == null || maxBytes < 0) { - throw new SkillException("Valid Skill content stream and limit are required."); - } - Path temporaryFile = null; - try { - temporaryFile = Files.createTempFile("easy-agents-skill-content-", ".stage"); - MessageDigest digest = SkillHashes.newSha256Digest(); - long total = 0; - try (DigestOutputStream output = new DigestOutputStream( - Files.newOutputStream(temporaryFile, StandardOpenOption.TRUNCATE_EXISTING), digest)) { - byte[] buffer = new byte[16 * 1024]; - int read; - while ((read = inputStream.read(buffer)) >= 0) { - if (read == 0) { - continue; - } - if (total > maxBytes - read) { - throw new SkillException("Skill content exceeds " + maxBytes + " bytes."); - } - output.write(buffer, 0, read); - total += read; - } - } - return SkillContentStage.compatibility( - temporaryFile, SkillHashes.toHex(digest.digest()), total); - } catch (IOException | RuntimeException e) { - deleteTemporaryFile(temporaryFile); - if (e instanceof SkillException skillException) { - throw skillException; - } - throw new SkillException("Failed to stage Skill content stream.", e); - } - } + SkillContentStage stage(InputStream inputStream, long maxBytes); /** * 提交暂存内容。 @@ -88,59 +30,28 @@ public interface SkillContentStore { * @param stage 暂存结果 * @return 最终内容引用 */ - default String commit(SkillContentStage stage) { - if (stage == null) { - throw new SkillException("Skill content stage is required."); - } - Path compatibilityPath = stage.compatibilityPath(); - if (compatibilityPath != null) { - try (InputStream input = Files.newInputStream(compatibilityPath)) { - String contentRef = put(input, stage.getSize()); - if (!stage.getContentRef().equals(contentRef)) { - release(contentRef); - throw new SkillException("Skill content store returned a non-hash content reference."); - } - return contentRef; - } catch (IOException e) { - throw new SkillException("Failed to commit staged Skill content.", e); - } finally { - deleteTemporaryFile(compatibilityPath); - } - } - return stage.getContentRef(); - } + String commit(SkillContentStage stage); /** * 回滚尚未提交的内容。 * * @param stage 暂存结果 */ - default void rollback(SkillContentStage stage) { - if (stage != null) { - deleteTemporaryFile(stage.compatibilityPath()); - if (stage.isAlreadyCommitted()) { - release(stage.getContentRef()); - } - } - } + void rollback(SkillContentStage stage); /** * 增加正式内容引用计数。 * * @param contentRef 内容引用 */ - default void retain(String contentRef) { - // 旧实现没有引用计数,保留兼容空操作。 - } + void retain(String contentRef); /** * 释放正式内容引用;引用归零后实现可以删除物理内容。 * * @param contentRef 内容引用 */ - default void release(String contentRef) { - // 旧实现没有引用计数,保留兼容空操作。 - } + void release(String contentRef); /** * 打开内容流。 @@ -150,14 +61,6 @@ public interface SkillContentStore { */ InputStream open(String contentRef); - /** - * 读取全部内容。 - * - * @param contentRef 内容引用 - * @return 内容字节 - */ - byte[] readAllBytes(String contentRef); - /** * 判断内容是否存在。 * @@ -165,43 +68,4 @@ public interface SkillContentStore { * @return 存在时为 true */ boolean exists(String contentRef); - - private static byte[] readBounded(InputStream inputStream, long maxBytes) { - if (inputStream == null) { - throw new SkillException("Skill content input stream is required."); - } - if (maxBytes < 0) { - throw new SkillException("Skill content max bytes cannot be negative."); - } - try { - ByteArrayOutputStream output = new ByteArrayOutputStream((int) Math.min(maxBytes, 8_192)); - byte[] buffer = new byte[8_192]; - long total = 0; - int read; - while ((read = inputStream.read(buffer)) >= 0) { - if (read == 0) { - continue; - } - total += read; - if (total > maxBytes) { - throw new SkillException("Skill content exceeds " + maxBytes + " bytes."); - } - output.write(buffer, 0, read); - } - return output.toByteArray(); - } catch (IOException e) { - throw new SkillException("Failed to read Skill content stream.", e); - } - } - - private static void deleteTemporaryFile(Path path) { - if (path == null) { - return; - } - try { - Files.deleteIfExists(path); - } catch (IOException ignored) { - // 临时文件清理由操作系统兜底,调用方异常语义保持不变。 - } - } } diff --git a/easy-agents-skill/src/main/java/com/easyagents/skill/store/file/TemporaryFileSkillContentStore.java b/easy-agents-skill/src/main/java/com/easyagents/skill/store/file/TemporaryFileSkillContentStore.java index 7a39bdd..7302814 100644 --- a/easy-agents-skill/src/main/java/com/easyagents/skill/store/file/TemporaryFileSkillContentStore.java +++ b/easy-agents-skill/src/main/java/com/easyagents/skill/store/file/TemporaryFileSkillContentStore.java @@ -86,19 +86,7 @@ public final class TemporaryFileSkillContentStore implements SkillContentStore, @Override public String put(byte[] bytes) { byte[] safeBytes = bytes == null ? new byte[0] : bytes; - return put(new ByteArrayInputStream(safeBytes), safeBytes.length); - } - - /** - * 流式保存内容并持有一个正式引用。 - * - * @param inputStream 内容流,不由本方法关闭 - * @param maxBytes 最大允许字节数 - * @return SHA-256 内容引用 - */ - @Override - public String put(InputStream inputStream, long maxBytes) { - SkillContentStage stage = stage(inputStream, maxBytes); + SkillContentStage stage = stage(new ByteArrayInputStream(safeBytes), safeBytes.length); try { return commit(stage); } catch (RuntimeException exception) { @@ -153,7 +141,7 @@ public final class TemporaryFileSkillContentStore implements SkillContentStore, ensureOpenLocked(); stagedContents.put(stageId, stagedContent); } - return new SkillContentStage(stageId, contentRef, contentHash, size, false); + return new SkillContentStage(stageId, contentRef, contentHash, size); } catch (IOException | RuntimeException exception) { deleteFileQuietly(stagedPath); if (exception instanceof SkillException skillException) { @@ -309,24 +297,6 @@ public final class TemporaryFileSkillContentStore implements SkillContentStore, } } - /** - * 读取正式内容的全部字节。 - * - *

该兼容方法会按接口约定返回一个字节数组;流式调用方应优先使用 {@link #open(String)}。

- * - * @param contentRef 内容引用 - * @return 内容字节 - */ - @Override - public byte[] readAllBytes(String contentRef) { - try (InputStream input = open(contentRef)) { - return input.readAllBytes(); - } catch (IOException exception) { - throw new SkillException("Failed to read temporary Skill content: " + contentRef, - exception); - } - } - /** * 判断正式内容是否仍有有效引用。 * diff --git a/easy-agents-skill/src/main/java/com/easyagents/skill/store/memory/InMemorySkillContentStore.java b/easy-agents-skill/src/main/java/com/easyagents/skill/store/memory/InMemorySkillContentStore.java deleted file mode 100644 index e33f711..0000000 --- a/easy-agents-skill/src/main/java/com/easyagents/skill/store/memory/InMemorySkillContentStore.java +++ /dev/null @@ -1,210 +0,0 @@ -package com.easyagents.skill.store.memory; - -import com.easyagents.skill.exception.SkillException; -import com.easyagents.skill.store.SkillContentStage; -import com.easyagents.skill.store.SkillContentStore; -import com.easyagents.skill.util.SkillHashes; - -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.util.Arrays; -import java.util.UUID; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ConcurrentMap; -import java.util.concurrent.atomic.AtomicInteger; - -/** - * 基于内存的 Skill 内容存储,支持真实暂存与引用计数,适用于测试和轻量场景。 - */ -public class InMemorySkillContentStore implements SkillContentStore { - - private final ConcurrentMap contents = new ConcurrentHashMap<>(); - private final ConcurrentMap stagedContents = new ConcurrentHashMap<>(); - - /** - * 保存内容并持有一个引用。 - * - * @param bytes 内容字节 - * @return 内容引用 - */ - @Override - public String put(byte[] bytes) { - byte[] safeBytes = bytes == null ? new byte[0] : Arrays.copyOf(bytes, bytes.length); - String contentRef = SkillHashes.sha256Ref(safeBytes); - contents.compute(contentRef, (key, current) -> { - if (current == null) { - return new StoredContent(safeBytes); - } - if (!Arrays.equals(current.bytes, safeBytes)) { - throw new SkillException("SHA-256 collision detected for Skill content: " + contentRef); - } - current.references.incrementAndGet(); - return current; - }); - return contentRef; - } - - /** - * 流式保存内容并持有一个引用。 - * - * @param inputStream 内容流 - * @param maxBytes 最大允许字节数 - * @return 内容引用 - */ - @Override - public String put(InputStream inputStream, long maxBytes) { - return put(readBounded(inputStream, maxBytes)); - } - - /** - * 将内容写入独立暂存区。 - * - * @param inputStream 内容流 - * @param maxBytes 最大允许字节数 - * @return 暂存结果 - */ - @Override - public SkillContentStage stage(InputStream inputStream, long maxBytes) { - byte[] bytes = readBounded(inputStream, maxBytes); - String hash = SkillHashes.sha256Hex(bytes); - String stageId = UUID.randomUUID().toString(); - stagedContents.put(stageId, bytes); - return new SkillContentStage(stageId, "sha256:" + hash, hash, bytes.length, false); - } - - /** - * 原子地将暂存内容转为正式引用。 - * - * @param stage 暂存结果 - * @return 正式内容引用 - */ - @Override - public String commit(SkillContentStage stage) { - if (stage == null) { - throw new SkillException("Skill content stage is required."); - } - byte[] bytes = stagedContents.remove(stage.getStageId()); - if (bytes == null) { - throw new SkillException("Skill content stage does not exist: " + stage.getStageId()); - } - String contentRef = put(bytes); - if (!contentRef.equals(stage.getContentRef())) { - release(contentRef); - throw new SkillException("Skill staged content hash changed before commit."); - } - return contentRef; - } - - /** - * 删除暂存内容。 - * - * @param stage 暂存结果 - */ - @Override - public void rollback(SkillContentStage stage) { - if (stage != null) { - stagedContents.remove(stage.getStageId()); - } - } - - /** - * 增加正式内容引用计数。 - * - * @param contentRef 内容引用 - */ - @Override - public void retain(String contentRef) { - contents.compute(contentRef, (key, content) -> { - if (content == null) { - throw new SkillException("Skill content does not exist: " + contentRef); - } - content.references.incrementAndGet(); - return content; - }); - } - - /** - * 释放正式内容引用并在归零时删除内容。 - * - * @param contentRef 内容引用 - */ - @Override - public void release(String contentRef) { - contents.computeIfPresent(contentRef, (key, content) -> - content.references.decrementAndGet() <= 0 ? null : content); - } - - /** - * 打开内容流。 - * - * @param contentRef 内容引用 - * @return 内容流 - */ - @Override - public InputStream open(String contentRef) { - return new ByteArrayInputStream(readAllBytes(contentRef)); - } - - /** - * 读取全部内容。 - * - * @param contentRef 内容引用 - * @return 内容副本 - */ - @Override - public byte[] readAllBytes(String contentRef) { - StoredContent content = contents.get(contentRef); - if (content == null) { - throw new SkillException("Skill content does not exist: " + contentRef); - } - return Arrays.copyOf(content.bytes, content.bytes.length); - } - - /** - * 判断内容是否存在。 - * - * @param contentRef 内容引用 - * @return 存在时为 true - */ - @Override - public boolean exists(String contentRef) { - return contents.containsKey(contentRef); - } - - private static byte[] readBounded(InputStream inputStream, long maxBytes) { - if (inputStream == null || maxBytes < 0) { - throw new SkillException("Valid Skill content stream and limit are required."); - } - try { - ByteArrayOutputStream output = new ByteArrayOutputStream((int) Math.min(maxBytes, 8_192)); - byte[] buffer = new byte[8_192]; - long total = 0; - int read; - while ((read = inputStream.read(buffer)) >= 0) { - if (read == 0) { - continue; - } - total += read; - if (total > maxBytes) { - throw new SkillException("Skill content exceeds " + maxBytes + " bytes."); - } - output.write(buffer, 0, read); - } - return output.toByteArray(); - } catch (IOException e) { - throw new SkillException("Failed to read Skill content stream.", e); - } - } - - private static final class StoredContent { - - private final byte[] bytes; - private final AtomicInteger references = new AtomicInteger(1); - - private StoredContent(byte[] bytes) { - this.bytes = Arrays.copyOf(bytes, bytes.length); - } - } -} diff --git a/easy-agents-skill/src/main/java/com/easyagents/skill/util/SkillResources.java b/easy-agents-skill/src/main/java/com/easyagents/skill/util/SkillResources.java index 67f9ed9..48bb64d 100644 --- a/easy-agents-skill/src/main/java/com/easyagents/skill/util/SkillResources.java +++ b/easy-agents-skill/src/main/java/com/easyagents/skill/util/SkillResources.java @@ -1,29 +1,25 @@ package com.easyagents.skill.util; -import com.easyagents.skill.model.Skill; -import com.easyagents.skill.model.SkillAsset; -import com.easyagents.skill.model.SkillMetadata; -import com.easyagents.skill.model.SkillReference; -import com.easyagents.skill.model.SkillResource; import com.easyagents.skill.model.SkillResourceKind; -import com.easyagents.skill.model.SkillScript; -import com.easyagents.skill.model.SkillScriptLanguage; -import java.util.ArrayList; -import java.util.List; import java.util.Locale; import java.util.Set; /** - * 通用 Skill 资源与旧资源视图之间的兼容适配工具。 + * 通用 Skill 资源识别工具。 */ public final class SkillResources { private static final Set TEXT_EXTENSIONS = Set.of( ".md", ".markdown", ".txt", ".json", ".yaml", ".yml", ".xml", ".csv", ".tsv", - ".htm", ".html", ".css", ".properties", ".toml", ".ini", ".sql", ".java", ".kt", ".kts", + ".htm", ".html", ".svg", ".css", ".scss", ".less", ".properties", ".toml", ".ini", + ".conf", ".config", ".env", ".sql", ".graphql", ".gql", ".proto", ".java", ".kt", ".kts", ".py", ".js", ".mjs", ".cjs", ".ts", ".tsx", ".jsx", ".sh", ".bash", ".zsh", - ".rb", ".go", ".rs", ".c", ".h", ".cpp", ".hpp", ".gradle", ".groovy" + ".rb", ".go", ".rs", ".c", ".h", ".cpp", ".hpp", ".gradle", ".groovy", ".vue", ".svelte" + ); + + private static final Set TEXT_FILE_NAMES = Set.of( + "dockerfile", "makefile", "gradlew", ".gitignore", ".gitattributes", ".editorconfig" ); private SkillResources() { @@ -50,138 +46,22 @@ public final class SkillResources { * 判断资源是否应按严格 UTF-8 文本处理。 * * @param path 资源路径 - * @param kind 资源语义类型 * @param mediaType 媒体类型 * @return 文本资源时为 true */ - public static boolean isText(String path, SkillResourceKind kind, String mediaType) { - if (kind == SkillResourceKind.SCRIPT) { - return true; - } - if (kind == SkillResourceKind.ASSET) { - return false; - } - if (mediaType != null && (mediaType.startsWith("text/") - || mediaType.contains("json") || mediaType.contains("yaml") - || mediaType.contains("xml") || mediaType.contains("javascript"))) { + public static boolean isText(String path, String mediaType) { + String normalizedMediaType = mediaType == null ? "" : mediaType.toLowerCase(Locale.ROOT); + if (normalizedMediaType.startsWith("text/") + || normalizedMediaType.contains("json") || normalizedMediaType.contains("yaml") + || normalizedMediaType.contains("xml") || normalizedMediaType.contains("javascript") + || normalizedMediaType.contains("typescript") || normalizedMediaType.contains("toml") + || normalizedMediaType.contains("sql")) { return true; } String lowerPath = path.toLowerCase(Locale.ROOT); - return TEXT_EXTENSIONS.stream().anyMatch(lowerPath::endsWith); + String fileName = SkillPaths.fileName(lowerPath); + return TEXT_FILE_NAMES.contains(fileName) + || TEXT_EXTENSIONS.stream().anyMatch(lowerPath::endsWith); } - /** - * 获取 Skill 的正式通用资源;旧模型会被按需转换。 - * - * @param skill Skill 聚合 - * @return 通用资源副本 - */ - public static List canonicalResources(Skill skill) { - if (skill == null) { - return new ArrayList<>(); - } - return new ArrayList<>(skill.getResources()); - } - - /** - * 将尚未迁移的旧 references、scripts、assets 视图转换为正式通用资源。 - * - *

该方法只读取旧视图,不读取或修改正式资源列表,由 {@link Skill#getResources()} - * 在第一次正式访问时完成一次性迁移。

- * - * @param skill Skill 聚合 - * @return 从旧视图转换得到的通用资源副本 - */ - public static List fromLegacyViews(Skill skill) { - List resources = new ArrayList<>(); - if (skill == null) { - return resources; - } - if (skill.getReferences() != null) { - for (SkillReference reference : skill.getReferences()) { - SkillResource resource = base(reference.getPath(), SkillResourceKind.REFERENCE, - "text/markdown", reference.getContentHash(), reference.getSize(), reference.getMetadata()); - resource.setTextContent(reference.getContent()); - resources.add(resource); - } - } - if (skill.getScripts() != null) { - for (SkillScript script : skill.getScripts()) { - SkillResource resource = base(script.getPath(), SkillResourceKind.SCRIPT, - "text/plain", script.getContentHash(), script.getSize(), script.getMetadata()); - resource.setTextContent(script.getContent()); - resources.add(resource); - } - } - if (skill.getAssets() != null) { - for (SkillAsset asset : skill.getAssets()) { - SkillResource resource = base(asset.getPath(), SkillResourceKind.ASSET, - asset.getMediaType(), asset.getContentHash(), asset.getSize(), asset.getMetadata()); - resource.setContentRef(asset.getContentRef()); - resources.add(resource); - } - } - return resources; - } - - /** - * 依据通用资源刷新旧 references、scripts、assets 兼容视图。 - * - * @param skill Skill 聚合 - */ - public static void refreshLegacyViews(Skill skill) { - List references = new ArrayList<>(); - List scripts = new ArrayList<>(); - List assets = new ArrayList<>(); - for (SkillResource resource : skill.getResources()) { - if (resource.getKind() == SkillResourceKind.REFERENCE && resource.isText()) { - SkillReference reference = new SkillReference(); - reference.setPath(resource.getPath()); - reference.setName(SkillPaths.fileName(resource.getPath())); - reference.setContent(resource.getTextContent()); - reference.setContentHash(resource.getContentHash()); - reference.setSize(resource.getSize()); - reference.setMetadata(copy(resource.getMetadata())); - references.add(reference); - } else if (resource.getKind() == SkillResourceKind.SCRIPT && resource.isText()) { - SkillScript script = new SkillScript(); - script.setPath(resource.getPath()); - script.setLanguage(SkillScriptLanguage.fromPath(resource.getPath())); - script.setContent(resource.getTextContent()); - script.setContentHash(resource.getContentHash()); - script.setSize(resource.getSize()); - script.setMetadata(copy(resource.getMetadata())); - scripts.add(script); - } else if (resource.getKind() == SkillResourceKind.ASSET) { - SkillAsset asset = new SkillAsset(); - asset.setPath(resource.getPath()); - asset.setName(SkillPaths.fileName(resource.getPath())); - asset.setMediaType(resource.getMediaType()); - asset.setContentRef(resource.getContentRef()); - asset.setContentHash(resource.getContentHash()); - asset.setSize(resource.getSize()); - asset.setMetadata(copy(resource.getMetadata())); - assets.add(asset); - } - } - skill.setReferences(references); - skill.setScripts(scripts); - skill.setAssets(assets); - } - - private static SkillResource base(String path, SkillResourceKind kind, String mediaType, - String hash, long size, SkillMetadata metadata) { - SkillResource resource = new SkillResource(); - resource.setPath(path); - resource.setKind(kind); - resource.setMediaType(mediaType); - resource.setContentHash(hash); - resource.setSize(size); - resource.setMetadata(copy(metadata)); - return resource; - } - - private static SkillMetadata copy(SkillMetadata metadata) { - return metadata == null ? new SkillMetadata() : new SkillMetadata(metadata.getValues()); - } } diff --git a/easy-agents-skill/src/main/java/com/easyagents/skill/validation/SkillValidator.java b/easy-agents-skill/src/main/java/com/easyagents/skill/validation/SkillValidator.java deleted file mode 100644 index 8654b40..0000000 --- a/easy-agents-skill/src/main/java/com/easyagents/skill/validation/SkillValidator.java +++ /dev/null @@ -1,65 +0,0 @@ -package com.easyagents.skill.validation; - -import com.easyagents.skill.exception.SkillValidationException; -import com.easyagents.skill.model.Skill; -import com.easyagents.skill.model.SkillPackageLimits; - -/** - * Skill 校验接口。 - */ -public interface SkillValidator { - - /** - * 校验 Skill 聚合。 - * - * @param skill Skill 聚合 - */ - void validate(Skill skill); - - /** - * 聚合校验 Skill 并返回结构化报告。 - * - *

兼容默认实现会把旧命令式异常转换为单个错误;新实现应覆盖以返回全部问题。

- * - * @param skill Skill 聚合 - * @return 结构化校验报告 - */ - default SkillValidationReport validateReport(Skill skill) { - SkillValidationReport report = new SkillValidationReport(); - try { - validate(skill); - } catch (SkillValidationException e) { - report.add(new SkillValidationIssue(e.getCode(), SkillValidationSeverity.ERROR, e.getPath(), - e.getLine(), e.getColumn(), e.getMessage(), null)); - } - return report; - } - - /** - * 使用当前 Codec 操作的安全限额聚合校验 Skill。 - * - *

兼容实现默认委托给原有校验入口;需要检查包限额的实现应覆盖本方法。

- * - * @param skill Skill 聚合 - * @param limits 当前读写操作的安全限额 - * @return 结构化校验报告 - */ - default SkillValidationReport validateReport(Skill skill, SkillPackageLimits limits) { - return validateReport(skill); - } - - /** - * 使用指定标准模式和当前 Codec 安全限额聚合校验 Skill。 - * - *

兼容实现默认忽略模式;需要区分草稿导入与正式标准约束的实现应覆盖本方法。

- * - * @param skill Skill 聚合 - * @param limits 当前读写操作的安全限额 - * @param mode 标准校验模式 - * @return 结构化校验报告 - */ - default SkillValidationReport validateReport(Skill skill, SkillPackageLimits limits, - SkillValidationMode mode) { - return validateReport(skill, limits); - } -} diff --git a/easy-agents-skill/src/main/java/com/easyagents/skill/validation/defaults/DefaultSkillValidator.java b/easy-agents-skill/src/main/java/com/easyagents/skill/validation/defaults/DefaultSkillValidator.java index 7cf7ba7..42e7366 100644 --- a/easy-agents-skill/src/main/java/com/easyagents/skill/validation/defaults/DefaultSkillValidator.java +++ b/easy-agents-skill/src/main/java/com/easyagents/skill/validation/defaults/DefaultSkillValidator.java @@ -6,7 +6,6 @@ import com.easyagents.skill.model.SkillDocument; import com.easyagents.skill.model.SkillPackageLimits; import com.easyagents.skill.model.SkillResource; import com.easyagents.skill.model.SkillResourceKind; -import com.easyagents.skill.model.SkillScriptLanguage; import com.easyagents.skill.model.SkillSourceLocation; import com.easyagents.skill.util.SkillFrontmatter; import com.easyagents.skill.util.SkillHashes; @@ -17,7 +16,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 java.nio.charset.CharacterCodingException; import java.util.HashMap; @@ -31,7 +29,7 @@ import java.util.regex.Pattern; /** * 默认 Skill 聚合结构化校验器。 */ -public class DefaultSkillValidator implements SkillValidator { +public class DefaultSkillValidator { private static final Pattern CANONICAL_NAME = Pattern.compile("[a-z0-9]+(?:-[a-z0-9]+)*"); private static final Pattern LEGACY_UNDERSCORE_NAME = Pattern.compile("[a-z0-9]+(?:_[a-z0-9]+)+"); @@ -61,7 +59,6 @@ public class DefaultSkillValidator implements SkillValidator { * @param skill Skill 聚合 * @throws SkillValidationException 校验失败 */ - @Override public void validate(Skill skill) { validateReport(skill, limits, SkillValidationMode.STANDARD).throwIfInvalid(); } @@ -72,7 +69,6 @@ public class DefaultSkillValidator implements SkillValidator { * @param skill Skill 聚合 * @return 结构化校验报告 */ - @Override public SkillValidationReport validateReport(Skill skill) { return validateReport(skill, limits); } @@ -84,7 +80,6 @@ public class DefaultSkillValidator implements SkillValidator { * @param operationLimits 当前读写操作的安全限额 * @return 结构化校验报告 */ - @Override public SkillValidationReport validateReport(Skill skill, SkillPackageLimits operationLimits) { return validateReport(skill, operationLimits, SkillValidationMode.DRAFT_IMPORT); } @@ -97,7 +92,6 @@ public class DefaultSkillValidator implements SkillValidator { * @param mode 标准校验模式 * @return 结构化校验报告 */ - @Override public SkillValidationReport validateReport(Skill skill, SkillPackageLimits operationLimits, SkillValidationMode mode) { SkillPackageLimits effectiveLimits = operationLimits == null ? limits : operationLimits; @@ -110,11 +104,11 @@ public class DefaultSkillValidator implements SkillValidator { SkillDocument document = parseDocument(skill, report, effectiveLimits); validateName(skill, document, report, effectiveMode); - validateDescription(skill, document, report); + validateDescription(document, report); if (document != null) { - validateDocument(skill, document, report); + validateDocument(document, report); } - List resources = SkillResources.canonicalResources(skill); + List resources = skill.getResources(); validateAggregateLimits(skill, resources, report, effectiveLimits); validateResources(skill.getName(), resources, report, effectiveLimits); return report; @@ -123,7 +117,7 @@ public class DefaultSkillValidator implements SkillValidator { private static void validateName(Skill skill, SkillDocument document, SkillValidationReport report, SkillValidationMode mode) { SkillSourceLocation location = sourceLocation(document, "name"); - String name = skill.getName(); + String name = frontmatterString(document, "name"); if (isBlank(name)) { report.add(errorAt("NAME_REQUIRED", SkillPaths.SKILL_FILE, location, "Skill name is required.", "Set frontmatter name to a portable Skill name.")); @@ -153,13 +147,13 @@ public class DefaultSkillValidator implements SkillValidator { } } - private static void validateDescription(Skill skill, SkillDocument document, - SkillValidationReport report) { + private static void validateDescription(SkillDocument document, SkillValidationReport report) { SkillSourceLocation location = sourceLocation(document, "description"); - if (isBlank(skill.getDescription())) { + String description = frontmatterString(document, "description"); + if (isBlank(description)) { report.add(errorAt("DESCRIPTION_REQUIRED", SkillPaths.SKILL_FILE, location, "Skill description is required.", "Describe both the capability and when to use it.")); - } else if (skill.getDescription().length() > 1_024) { + } else if (description.length() > 1_024) { report.add(errorAt("DESCRIPTION_TOO_LONG", SkillPaths.SKILL_FILE, location, "Skill description cannot exceed 1024 characters.", "Shorten the description.")); } @@ -181,10 +175,8 @@ public class DefaultSkillValidator implements SkillValidator { } } - private static void validateDocument(Skill skill, SkillDocument document, SkillValidationReport report) { + private static void validateDocument(SkillDocument document, SkillValidationReport report) { Map values = document.getFrontmatter().getValues(); - validateCoreString(document, values, "name", skill.getName(), report); - validateCoreString(document, values, "description", skill.getDescription(), report); validateOptionalString(document, values, "license", null, report); Object compatibility = values.get("compatibility"); if (compatibility != null && (!(compatibility instanceof String text) @@ -195,12 +187,6 @@ public class DefaultSkillValidator implements SkillValidator { } validateOptionalString(document, values, "allowed-tools", "INVALID_ALLOWED_TOOLS", report); validateMetadataField(document, values.get("metadata"), report); - if (skill.getMetadata() == null || !skill.getMetadata().getValues().equals(values)) { - report.add(errorAt("METADATA_MISMATCH", SkillPaths.SKILL_FILE, - sourceLocation(document, "name"), - "Skill metadata must match SKILL.md frontmatter.", - "Reparse SKILL.md before saving the aggregate.")); - } if (document.getMarkdownBody().length() > 30_000) { report.add(new SkillValidationIssue("LONG_SKILL_BODY", SkillValidationSeverity.WARNING, SkillPaths.SKILL_FILE, null, null, @@ -209,21 +195,6 @@ public class DefaultSkillValidator implements SkillValidator { } } - private static void validateCoreString(SkillDocument document, Map values, - String key, String expected, - SkillValidationReport report) { - Object value = values.get(key); - if (!(value instanceof String text) || text.isBlank()) { - report.add(errorAt(key.toUpperCase(Locale.ROOT) + "_REQUIRED", SkillPaths.SKILL_FILE, - sourceLocation(document, key), - "SKILL.md frontmatter " + key + " must be a non-blank string.", null)); - } else if (!text.equals(expected)) { - report.add(errorAt(key.toUpperCase(Locale.ROOT) + "_MISMATCH", SkillPaths.SKILL_FILE, - sourceLocation(document, key), - "Skill " + key + " must match SKILL.md frontmatter.", null)); - } - } - private static void validateOptionalString(SkillDocument document, Map values, String key, String code, SkillValidationReport report) { @@ -335,11 +306,9 @@ public class DefaultSkillValidator implements SkillValidator { private static void validateResourceContent(SkillResource resource, String path, SkillValidationReport report, SkillPackageLimits limits) { - boolean expectedText = SkillResources.isText(path, resource.getKind(), resource.getMediaType()); + boolean expectedText = SkillResources.isText(path, resource.getMediaType()); if (expectedText != resource.isText()) { - String code = resource.getKind() == SkillResourceKind.SCRIPT - ? "SCRIPT_TEXT_REQUIRED" : "RESOURCE_CONTENT_MODE_MISMATCH"; - report.add(error(code, path, + report.add(error("RESOURCE_CONTENT_MODE_MISMATCH", path, expectedText ? "Resource path and media type require strict UTF-8 text content." : "Resource path and media type require a binary content reference.", @@ -350,13 +319,6 @@ public class DefaultSkillValidator implements SkillValidator { path, null, null, "Script resource is empty.", "Add script source or remove the unused file.")); } - if (resource.getKind() == SkillResourceKind.SCRIPT - && SkillScriptLanguage.fromPath(path) == SkillScriptLanguage.UNKNOWN) { - report.add(new SkillValidationIssue("SCRIPT_LANGUAGE_UNRECOGNIZED", - SkillValidationSeverity.WARNING, path, null, null, - "Script language is not recognized from its extension.", - "Use .py, .js, or .sh for first-class editing and syntax highlighting.")); - } if (isBlank(resource.getMediaType())) { report.add(error("MEDIA_TYPE_REQUIRED", path, "Skill resource media type is required.", null)); } @@ -525,4 +487,12 @@ public class DefaultSkillValidator implements SkillValidator { private static boolean isBlank(String value) { return value == null || value.isBlank(); } + + private static String frontmatterString(SkillDocument document, String key) { + if (document == null) { + return null; + } + Object value = document.getFrontmatter().get(key); + return value instanceof String text ? text : null; + } } diff --git a/easy-agents-skill/src/test/java/com/easyagents/skill/codec/ZipSkillPackageCodecTest.java b/easy-agents-skill/src/test/java/com/easyagents/skill/codec/ZipSkillPackageCodecTest.java index 68b9dee..ee4beaa 100644 --- a/easy-agents-skill/src/test/java/com/easyagents/skill/codec/ZipSkillPackageCodecTest.java +++ b/easy-agents-skill/src/test/java/com/easyagents/skill/codec/ZipSkillPackageCodecTest.java @@ -1,7 +1,6 @@ package com.easyagents.skill.codec; import com.easyagents.skill.exception.SkillPackageException; -import com.easyagents.skill.exception.SkillValidationException; import com.easyagents.skill.factory.SkillFactory; import com.easyagents.skill.model.Skill; import com.easyagents.skill.model.SkillPackage; @@ -14,7 +13,6 @@ import com.easyagents.skill.store.SkillContentStore; import com.easyagents.skill.store.memory.InMemorySkillContentStore; import com.easyagents.skill.util.SkillHashes; import com.easyagents.skill.util.SkillPaths; -import com.easyagents.skill.validation.SkillValidator; import org.apache.commons.compress.archivers.zip.UnixStat; import org.apache.commons.compress.archivers.zip.ZipArchiveEntry; import org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream; @@ -69,7 +67,7 @@ public class ZipSkillPackageCodecTest { } /** - * 单目录包导入通用资源并保持旧资源视图。 + * 单目录包导入通用资源。 */ @Test public void decodeWrappedSingleSkillWithGenericResources() { @@ -86,17 +84,16 @@ public class ZipSkillPackageCodecTest { Assert.assertEquals(SkillPackageLayout.SINGLE_DIRECTORY, result.getSkillPackage().getLayout()); Skill skill = result.getSkillPackage().getSkills().get(0); - Assert.assertNull(skill.getId()); Assert.assertEquals("skill-a", skill.getPackageRoot()); Assert.assertEquals(5, skill.getResources().size()); Assert.assertTrue(skill.getResources().stream().anyMatch(resource -> resource.getKind() == SkillResourceKind.EXAMPLE)); Assert.assertTrue(skill.getResources().stream().anyMatch(resource -> resource.getKind() == SkillResourceKind.OTHER)); - Assert.assertEquals(1, skill.getReferences().size()); - Assert.assertEquals(1, skill.getScripts().size()); - Assert.assertEquals(1, skill.getAssets().size()); - Assert.assertTrue(store.exists(skill.getAssets().get(0).getContentRef())); + SkillResource binary = skill.getResources().stream() + .filter(resource -> resource.getKind() == SkillResourceKind.ASSET) + .findFirst().orElseThrow(); + Assert.assertTrue(store.exists(binary.getContentRef())); } /** @@ -148,7 +145,7 @@ public class ZipSkillPackageCodecTest { "nested-skill/SKILL.md", utf8(markdown) ))).getSkillPackage().getSkills().get(0); - Assert.assertTrue(skill.getMetadata().get("metadata") instanceof Map); + Assert.assertTrue(skill.getDocument().getFrontmatter().get("metadata") instanceof Map); Assert.assertEquals(markdown, skill.getSkillContent()); } @@ -469,6 +466,56 @@ public class ZipSkillPackageCodecTest { Assert.assertEquals(0, store.committedCount); } + /** + * 资源的文本或二进制表示由文件类型决定,不受顶层语义目录约束。 + */ + @Test + public void decodeContentModeIndependentlyFromSemanticDirectory() throws Exception { + InMemorySkillContentStore store = new InMemorySkillContentStore(); + byte[] opaqueScript = new byte[]{0, (byte) 0xFF, 1}; + SkillPackageReadResult result = decode(new ZipSkillPackageCodec(store), zip(files( + "skill-a/SKILL.md", utf8(skillMd("skill-a")), + "skill-a/assets/template.md", utf8("# Template"), + "skill-a/scripts/helper.bin", opaqueScript + ))); + + List resources = result.getSkillPackage().getSkills().get(0).getResources(); + SkillResource template = resources.stream() + .filter(resource -> "assets/template.md".equals(resource.getPath())) + .findFirst().orElseThrow(); + SkillResource helper = resources.stream() + .filter(resource -> "scripts/helper.bin".equals(resource.getPath())) + .findFirst().orElseThrow(); + + Assert.assertTrue(template.isText()); + Assert.assertEquals("# Template", template.getTextContent()); + Assert.assertFalse(helper.isText()); + try (InputStream input = store.open(helper.getContentRef())) { + Assert.assertArrayEquals(opaqueScript, input.readAllBytes()); + } + } + + /** + * 预检回滚首次失败时,异常清理路径会重试尚未释放的 stage。 + */ + @Test + public void retryTransientReportOnlyRollbackFailure() { + FlakyRollbackContentStore store = new FlakyRollbackContentStore(); + + try { + new ZipSkillPackageCodec(store).decode( + new ByteArrayInputStream(zip(files( + "skill-a/SKILL.md", utf8(skillMd("skill-a")), + "skill-a/assets/data.bin", new byte[]{1, 2, 3} + ))), SkillPackageReadOptions.reportOnly()); + Assert.fail("Transient rollback failure should remain visible to the caller."); + } catch (SkillPackageException expected) { + Assert.assertEquals("SKILL_CONTENT_ROLLBACK_ERROR", expected.getCode()); + Assert.assertEquals(2, store.rollbackAttempts); + Assert.assertTrue(store.rollbackCompleted); + } + } + /** * 多 Skill 预检报告为每个相对路径补齐各自根目录。 */ @@ -493,12 +540,14 @@ public class ZipSkillPackageCodecTest { */ @Test public void exportPrefixesValidationPathsForMultipleSkills() { - Skill first = SkillFactory.create("first", skillMd("skill-a")); + Skill first = SkillFactory.create(skillMd("skill-a")); first.setPackageRoot("skill-a"); - first.setDescription(null); - Skill second = SkillFactory.create("second", skillMd("skill-b")); + first.getDocument().removeFrontmatter("description"); + first.setDocument(first.getDocument()); + Skill second = SkillFactory.create(skillMd("skill-b")); second.setPackageRoot("skill-b"); - second.setDescription(null); + second.getDocument().removeFrontmatter("description"); + second.setDocument(second.getDocument()); ByteArrayOutputStream output = new ByteArrayOutputStream(); try { @@ -596,8 +645,6 @@ public class ZipSkillPackageCodecTest { "skill-a/references/a.md", utf8("# A"), "skill-a/assets/data.bin", new byte[]{1, 2, 3} ))); - first.getSkillPackage().getSkills().get(0).setId("internal-repository-id"); - ByteArrayOutputStream firstZip = new ByteArrayOutputStream(); SkillPackageWriteResult firstWrite = codec.encode(first.getSkillPackage(), firstZip, SkillPackageWriteOptions.defaults()); @@ -609,13 +656,11 @@ public class ZipSkillPackageCodecTest { Assert.assertEquals(firstWrite.getPackageHash(), secondWrite.getPackageHash()); Assert.assertEquals(SkillPackageLayout.SINGLE_DIRECTORY, firstWrite.getLayout()); Assert.assertEquals(SkillHashes.sha256Hex(firstZip.toByteArray()), firstWrite.getPackageHash()); - Assert.assertFalse(zipContains(firstZip.toByteArray(), "internal-repository-id")); - SkillPackageReadResult roundTrip = decode(codec, firstZip.toByteArray()); Skill original = first.getSkillPackage().getSkills().get(0); Skill decoded = roundTrip.getSkillPackage().getSkills().get(0); - Assert.assertNull(decoded.getId()); - Assert.assertEquals(original.getMetadata().getValues(), decoded.getMetadata().getValues()); + Assert.assertEquals(original.getDocument().getFrontmatter().getValues(), + decoded.getDocument().getFrontmatter().getValues()); Assert.assertEquals(original.getSkillContent(), decoded.getSkillContent()); Assert.assertEquals(original.getResources().stream().map(resource -> resource.getPath()).toList(), decoded.getResources().stream().map(resource -> resource.getPath()).toList()); @@ -630,7 +675,7 @@ public class ZipSkillPackageCodecTest { @Test public void highCompressionTextRoundTripsWithSameLimits() { String content = skillMd("skill-a") + "a".repeat(20_000); - Skill skill = SkillFactory.create("id", content); + Skill skill = SkillFactory.create(content); SkillPackage skillPackage = new SkillPackage( SkillPackageLayout.SINGLE_DIRECTORY, List.of(skill)); SkillPackageLimits limits = SkillPackageLimits.builder() @@ -678,7 +723,7 @@ public class ZipSkillPackageCodecTest { @Test public void enforceOutputLimitsOnGeneratedSkillEntryPath() { ZipSkillPackageCodec codec = new ZipSkillPackageCodec(); - Skill skill = SkillFactory.create("id", skillMd("skill-a")); + Skill skill = SkillFactory.create(skillMd("skill-a")); String skillPath = "skill-a/SKILL.md"; assertExportIssue(codec, skill, new SkillPackageWriteOptions( @@ -690,95 +735,17 @@ public class ZipSkillPackageCodecTest { } /** - * 删除最后一个正式资源后不得从滞留的旧兼容视图恢复并再次导出。 + * Codec 内建的 YAML 与包大小安全校验不可绕过。 */ @Test - public void emptyCanonicalResourcesDoNotFallBackToLegacyViews() { - String content = "# Rules"; - SkillResource reference = new SkillResource(); - reference.setPath("references/rules.md"); - reference.setKind(SkillResourceKind.REFERENCE); - reference.setMediaType("text/markdown"); - reference.setTextContent(content); - reference.setContentHash(SkillHashes.sha256Hex(utf8(content))); - reference.setSize(utf8(content).length); - Skill skill = SkillFactory.createWithResources( - "id", skillMd("skill-a"), List.of(reference)); - Assert.assertEquals(1, skill.getReferences().size()); - skill.getResources().clear(); - + public void enforceMandatoryYamlAndPackageLimits() { ZipSkillPackageCodec codec = new ZipSkillPackageCodec(); - ByteArrayOutputStream output = new ByteArrayOutputStream(); - codec.encode(new SkillPackage(SkillPackageLayout.SINGLE_DIRECTORY, List.of(skill)), - output, SkillPackageWriteOptions.defaults()); - Skill decoded = codec.decode(new ByteArrayInputStream(output.toByteArray()), - SkillPackageReadOptions.defaults()).getSkillPackage().getSkills().get(0); - - Assert.assertTrue(decoded.getResources().isEmpty()); - Assert.assertTrue(decoded.getReferences().isEmpty()); - } - - /** - * 注入的默认校验器仍属于附加校验,其更严格限额不得被类型判断或操作选项绕过。 - */ - @Test - public void injectedDefaultValidatorAppliesItsStricterLimits() { - ZipSkillPackageCodec codec = new ZipSkillPackageCodec( - new InMemorySkillContentStore(), - new com.easyagents.skill.validation.defaults.DefaultSkillValidator( - SkillPackageLimits.builder().maxTextFileBytes(128).build())); - Skill skill = SkillFactory.create("id", skillMd("skill-a") + "x".repeat(300)); - - assertExportIssue(codec, skill, SkillPackageWriteOptions.defaults(), - "TEXT_FILE_SIZE_LIMIT", SkillPaths.SKILL_FILE); - } - - /** - * 自定义限额感知校验器必须收到当前 Codec 操作选项。 - */ - @Test - public void customValidatorReceivesOperationLimits() { - SkillPackageLimits limits = SkillPackageLimits.builder().maxPathLength(64).build(); - SkillPackageLimits[] observed = new SkillPackageLimits[1]; - SkillValidator validator = new SkillValidator() { - @Override - public void validate(Skill skill) { - // 限额感知实现通过下方结构化入口完成业务校验。 - } - - @Override - public com.easyagents.skill.validation.SkillValidationReport validateReport( - Skill skill, SkillPackageLimits operationLimits) { - observed[0] = operationLimits; - return new com.easyagents.skill.validation.SkillValidationReport(); - } - }; - ZipSkillPackageCodec codec = new ZipSkillPackageCodec( - new InMemorySkillContentStore(), validator); - - codec.encode(new SkillPackage(SkillPackageLayout.SINGLE_DIRECTORY, - List.of(SkillFactory.create("id", skillMd("skill-a")))), - new ByteArrayOutputStream(), new SkillPackageWriteOptions(limits)); - - Assert.assertSame(limits, observed[0]); - } - - /** - * noop 业务校验器不能绕过 Codec 内建的 YAML 与包大小安全校验。 - */ - @Test - public void noopValidatorCannotBypassMandatoryYamlAndPackageLimits() { - SkillValidator noopValidator = skill -> { - // 业务层不增加规则,Codec 仍必须独立完成标准安全校验。 - }; - ZipSkillPackageCodec codec = new ZipSkillPackageCodec( - new InMemorySkillContentStore(), noopValidator); - Skill malformedYaml = SkillFactory.create("id", skillMd("skill-a")); + Skill malformedYaml = SkillFactory.create(skillMd("skill-a")); malformedYaml.setSkillContent("---\nname: skill-a\ndescription: [\n---\n# Invalid\n"); assertExportIssue(codec, malformedYaml, SkillPackageWriteOptions.defaults(), "INVALID_FRONTMATTER_YAML", SkillPaths.SKILL_FILE); - Skill oversized = SkillFactory.create("id", skillMd("skill-a") + "x".repeat(300)); + Skill oversized = SkillFactory.create(skillMd("skill-a") + "x".repeat(300)); SkillPackageLimits limits = SkillPackageLimits.builder() .maxTextFileBytes(128) .build(); @@ -786,22 +753,6 @@ public class ZipSkillPackageCodecTest { "TEXT_FILE_SIZE_LIMIT", SkillPaths.SKILL_FILE); } - /** - * Codec 强制标准校验后仍执行调用方注入的业务校验器。 - */ - @Test - public void preserveAdditionalBusinessValidator() { - SkillValidator businessValidator = skill -> { - throw new SkillValidationException("CUSTOM_POLICY", SkillPaths.SKILL_FILE, - null, null, "Custom Skill policy rejected the package.", null); - }; - ZipSkillPackageCodec codec = new ZipSkillPackageCodec( - new InMemorySkillContentStore(), businessValidator); - - assertExportIssue(codec, SkillFactory.create("id", skillMd("skill-a")), - SkillPackageWriteOptions.defaults(), "CUSTOM_POLICY", SkillPaths.SKILL_FILE); - } - /** * SKILL.md 与普通文本资源中的孤立 UTF-16 surrogate 均以结构化错误拒绝。 */ @@ -809,12 +760,12 @@ public class ZipSkillPackageCodecTest { public void rejectMalformedUtf16SurrogateDuringExport() { String isolatedSurrogate = String.valueOf((char) 0xD800); ZipSkillPackageCodec codec = new ZipSkillPackageCodec(); - Skill invalidDocument = SkillFactory.create("id", skillMd("skill-a")); + Skill invalidDocument = SkillFactory.create(skillMd("skill-a")); invalidDocument.setSkillContent(skillMd("skill-a") + isolatedSurrogate); assertExportIssue(codec, invalidDocument, SkillPackageWriteOptions.defaults(), "INVALID_UTF8", SkillPaths.SKILL_FILE); - Skill invalidResource = SkillFactory.create("id", skillMd("skill-a")); + Skill invalidResource = SkillFactory.create(skillMd("skill-a")); SkillResource resource = new SkillResource(); resource.setPath("references/invalid.md"); resource.setKind(SkillResourceKind.REFERENCE); @@ -826,7 +777,7 @@ public class ZipSkillPackageCodecTest { assertExportIssue(codec, invalidResource, SkillPackageWriteOptions.defaults(), "INVALID_UTF8", "references/invalid.md"); - Skill invalidPath = SkillFactory.create("id", skillMd("skill-a")); + Skill invalidPath = SkillFactory.create(skillMd("skill-a")); SkillResource pathResource = new SkillResource(); String malformedPath = "references/" + isolatedSurrogate + ".md"; pathResource.setPath(malformedPath); @@ -845,7 +796,7 @@ public class ZipSkillPackageCodecTest { */ @Test public void rejectMissingResourcePathBeforeExportWritesBytes() { - Skill skill = SkillFactory.create("id", skillMd("skill-a")); + Skill skill = SkillFactory.create(skillMd("skill-a")); SkillResource resource = new SkillResource(); resource.setKind(SkillResourceKind.REFERENCE); resource.setMediaType("text/markdown"); @@ -902,7 +853,7 @@ public class ZipSkillPackageCodecTest { */ @Test public void aggregateInvalidSkillBeforePreparingOutput() { - Skill skill = com.easyagents.skill.factory.SkillFactory.create("id", skillMd("skill-a")); + Skill skill = SkillFactory.create(skillMd("skill-a")); skill.setSkillContent(null); ByteArrayOutputStream output = new ByteArrayOutputStream(); @@ -975,7 +926,7 @@ public class ZipSkillPackageCodecTest { resource.setContentRef("sha256:" + hash); resource.setContentHash(hash); resource.setSize(expected.length); - Skill skill = com.easyagents.skill.factory.SkillFactory.create("id", skillMd("skill-a")); + Skill skill = SkillFactory.create(skillMd("skill-a")); skill.setResources(List.of(resource)); ByteArrayOutputStream output = new ByteArrayOutputStream(); @@ -1258,21 +1209,6 @@ public class ZipSkillPackageCodecTest { } } - private static boolean zipContains(byte[] zip, String value) { - try (ZipInputStream input = new ZipInputStream(new ByteArrayInputStream(zip), StandardCharsets.UTF_8)) { - ZipEntry entry; - while ((entry = input.getNextEntry()) != null) { - if (entry.getName().contains(value) - || new String(input.readAllBytes(), StandardCharsets.UTF_8).contains(value)) { - return true; - } - } - return false; - } catch (Exception e) { - throw new IllegalStateException(e); - } - } - private static byte[] utf8(String value) { return value.getBytes(StandardCharsets.UTF_8); } @@ -1285,7 +1221,7 @@ public class ZipSkillPackageCodecTest { return "---\nname: " + name + "\ndescription: ''\n---\n# " + name + "\n"; } - private static final class CountingContentStore implements SkillContentStore { + private static final class CountingContentStore extends InMemorySkillContentStore { private int putCount; @@ -1300,11 +1236,6 @@ public class ZipSkillPackageCodecTest { throw new UnsupportedOperationException(); } - @Override - public byte[] readAllBytes(String contentRef) { - throw new UnsupportedOperationException(); - } - @Override public boolean exists(String contentRef) { return false; @@ -1356,6 +1287,12 @@ public class ZipSkillPackageCodecTest { stagedCount--; } + @Override + public void retain(String contentRef) { + delegate.retain(contentRef); + committedCount++; + } + @Override public void release(String contentRef) { delegate.release(contentRef); @@ -1367,18 +1304,13 @@ public class ZipSkillPackageCodecTest { return delegate.open(contentRef); } - @Override - public byte[] readAllBytes(String contentRef) { - return delegate.readAllBytes(contentRef); - } - @Override public boolean exists(String contentRef) { return delegate.exists(contentRef); } } - private static final class CorruptReadContentStore implements SkillContentStore { + private static final class CorruptReadContentStore extends InMemorySkillContentStore { @Override public String put(byte[] bytes) { @@ -1390,14 +1322,25 @@ public class ZipSkillPackageCodecTest { return new ByteArrayInputStream(utf8("xyz")); } - @Override - public byte[] readAllBytes(String contentRef) { - return utf8("xyz"); - } - @Override public boolean exists(String contentRef) { return true; } } + + private static final class FlakyRollbackContentStore extends InMemorySkillContentStore { + + private int rollbackAttempts; + private boolean rollbackCompleted; + + @Override + public void rollback(SkillContentStage stage) { + rollbackAttempts++; + if (rollbackAttempts == 1) { + throw new IllegalStateException("simulated transient rollback failure"); + } + super.rollback(stage); + rollbackCompleted = true; + } + } } diff --git a/easy-agents-skill/src/test/java/com/easyagents/skill/repository/memory/InMemorySkillRepositoryTest.java b/easy-agents-skill/src/test/java/com/easyagents/skill/repository/memory/InMemorySkillRepositoryTest.java deleted file mode 100644 index b2371a9..0000000 --- a/easy-agents-skill/src/test/java/com/easyagents/skill/repository/memory/InMemorySkillRepositoryTest.java +++ /dev/null @@ -1,118 +0,0 @@ -package com.easyagents.skill.repository.memory; - -import com.easyagents.skill.model.Skill; -import com.easyagents.skill.model.SkillDescriptor; -import com.easyagents.skill.model.SkillReference; -import com.easyagents.skill.util.SkillResources; -import org.junit.Assert; -import org.junit.Test; - -import java.util.Optional; - -/** - * InMemorySkillRepository 单元测试。 - */ -public class InMemorySkillRepositoryTest { - - /** - * 覆盖保存、读取、描述、列表、删除和存在性判断。 - */ - @Test - public void crudSkill() { - InMemorySkillRepository repository = new InMemorySkillRepository(); - Skill skill = skill(); - - repository.save(skill); - - Assert.assertTrue(repository.exists("skill-a")); - Assert.assertTrue(repository.get("skill-a").isPresent()); - Assert.assertTrue(repository.getDescriptor("skill-a").isPresent()); - Assert.assertEquals(1, repository.listDescriptors().size()); - - repository.delete("skill-a"); - - Assert.assertFalse(repository.exists("skill-a")); - Assert.assertFalse(repository.get("skill-a").isPresent()); - } - - /** - * descriptor 不携带 references/scripts/assets 内容。 - */ - @Test - public void descriptorDoesNotExposeResourceContent() { - InMemorySkillRepository repository = new InMemorySkillRepository(); - repository.save(skill()); - - Optional descriptor = repository.getDescriptor("skill-a"); - - Assert.assertTrue(descriptor.isPresent()); - Assert.assertEquals("skill-a", descriptor.get().getId()); - Assert.assertEquals("Skill A", descriptor.get().getName()); - } - - /** - * 读取返回副本,避免外部修改仓储内部状态。 - */ - @Test - public void getReturnsCopy() { - InMemorySkillRepository repository = new InMemorySkillRepository(); - repository.save(skill()); - - Skill loaded = repository.get("skill-a").get(); - loaded.setName("Changed"); - loaded.getReferences().get(0).setContent("changed"); - - Skill reloaded = repository.get("skill-a").get(); - Assert.assertEquals("Skill A", reloaded.getName()); - Assert.assertEquals("# A", reloaded.getReferences().get(0).getContent()); - } - - /** - * 仓储复制旧资源对象时应先迁移正式资源,不能因空 canonical 列表丢失 reference。 - */ - @Test - public void repositoryCopyMigratesLegacyOnlyResources() { - InMemorySkillRepository repository = new InMemorySkillRepository(); - Skill legacy = skill(); - Assert.assertFalse(legacy.isResourcesInitialized()); - - repository.save(legacy); - Skill loaded = repository.get("skill-a").orElseThrow(); - - Assert.assertTrue(loaded.isResourcesInitialized()); - Assert.assertEquals(1, SkillResources.canonicalResources(loaded).size()); - Assert.assertEquals("references/a.md", loaded.getResources().get(0).getPath()); - } - - /** - * 显式清空正式资源列表后,仓储往返不得从旧兼容视图恢复已删除资源。 - */ - @Test - public void repositoryCopyPreservesExplicitlyEmptyCanonicalResources() { - InMemorySkillRepository repository = new InMemorySkillRepository(); - Skill skill = skill(); - Assert.assertEquals(1, skill.getResources().size()); - skill.getResources().clear(); - - repository.save(skill); - Skill loaded = repository.get("skill-a").orElseThrow(); - - Assert.assertTrue(loaded.isResourcesInitialized()); - Assert.assertTrue(SkillResources.canonicalResources(loaded).isEmpty()); - } - - private static Skill skill() { - Skill skill = new Skill(); - skill.setId("skill-a"); - skill.setName("Skill A"); - skill.setDescription("Desc A"); - skill.setSkillContent("---\nname: Skill A\ndescription: Desc A\n---\n# Skill A\n"); - - SkillReference reference = new SkillReference(); - reference.setPath("references/a.md"); - reference.setName("a.md"); - reference.setContent("# A"); - skill.getReferences().add(reference); - return skill; - } -} diff --git a/easy-agents-skill/src/test/java/com/easyagents/skill/store/file/TemporaryFileSkillContentStoreTest.java b/easy-agents-skill/src/test/java/com/easyagents/skill/store/file/TemporaryFileSkillContentStoreTest.java index 43b4f38..a592f16 100644 --- a/easy-agents-skill/src/test/java/com/easyagents/skill/store/file/TemporaryFileSkillContentStoreTest.java +++ b/easy-agents-skill/src/test/java/com/easyagents/skill/store/file/TemporaryFileSkillContentStoreTest.java @@ -4,7 +4,6 @@ import com.easyagents.skill.codec.ZipSkillPackageCodec; import com.easyagents.skill.exception.SkillException; import com.easyagents.skill.store.SkillContentStage; import com.easyagents.skill.store.SkillContentStore; -import com.easyagents.skill.store.memory.InMemorySkillContentStore; import com.easyagents.skill.util.SkillHashes; import org.junit.Assert; import org.junit.Test; @@ -167,7 +166,6 @@ public class TemporaryFileSkillContentStoreTest { ZipSkillPackageCodec defaultCodec = new ZipSkillPackageCodec(); SkillContentStore defaultStore = (SkillContentStore) contentStoreField.get(defaultCodec); Assert.assertTrue(defaultStore instanceof TemporaryFileSkillContentStore); - Assert.assertFalse(defaultStore instanceof InMemorySkillContentStore); Path defaultDirectory = ((TemporaryFileSkillContentStore) defaultStore).storageDirectory(); defaultCodec.close(); Assert.assertFalse(Files.exists(defaultDirectory)); diff --git a/easy-agents-skill/src/test/java/com/easyagents/skill/store/memory/InMemorySkillContentStore.java b/easy-agents-skill/src/test/java/com/easyagents/skill/store/memory/InMemorySkillContentStore.java new file mode 100644 index 0000000..5b9e457 --- /dev/null +++ b/easy-agents-skill/src/test/java/com/easyagents/skill/store/memory/InMemorySkillContentStore.java @@ -0,0 +1,124 @@ +package com.easyagents.skill.store.memory; + +import com.easyagents.skill.exception.SkillException; +import com.easyagents.skill.store.SkillContentStage; +import com.easyagents.skill.store.SkillContentStore; +import com.easyagents.skill.util.SkillHashes; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +/** + * Codec 测试使用的内存内容存储。 + */ +public class InMemorySkillContentStore implements SkillContentStore { + + private final Map contents = new HashMap<>(); + private final Map stages = new HashMap<>(); + + @Override + public String put(byte[] bytes) { + byte[] copy = bytes == null ? new byte[0] : Arrays.copyOf(bytes, bytes.length); + String ref = SkillHashes.sha256Ref(copy); + StoredContent stored = contents.get(ref); + if (stored == null) { + contents.put(ref, new StoredContent(copy)); + } else { + stored.references++; + } + return ref; + } + + @Override + public SkillContentStage stage(InputStream inputStream, long maxBytes) { + byte[] bytes = readBounded(inputStream, maxBytes); + String stageId = UUID.randomUUID().toString(); + String hash = SkillHashes.sha256Hex(bytes); + stages.put(stageId, bytes); + return new SkillContentStage(stageId, "sha256:" + hash, hash, bytes.length); + } + + @Override + public String commit(SkillContentStage stage) { + byte[] bytes = stage == null ? null : stages.remove(stage.getStageId()); + if (bytes == null) { + throw new SkillException("Skill content stage does not exist."); + } + return put(bytes); + } + + @Override + public void rollback(SkillContentStage stage) { + if (stage != null) { + stages.remove(stage.getStageId()); + } + } + + @Override + public void retain(String contentRef) { + StoredContent stored = require(contentRef); + stored.references++; + } + + @Override + public void release(String contentRef) { + StoredContent stored = contents.get(contentRef); + if (stored != null && --stored.references <= 0) { + contents.remove(contentRef); + } + } + + @Override + public InputStream open(String contentRef) { + StoredContent stored = require(contentRef); + return new ByteArrayInputStream(Arrays.copyOf(stored.bytes, stored.bytes.length)); + } + + @Override + public boolean exists(String contentRef) { + return contents.containsKey(contentRef); + } + + private StoredContent require(String contentRef) { + StoredContent stored = contents.get(contentRef); + if (stored == null) { + throw new SkillException("Skill content does not exist: " + contentRef); + } + return stored; + } + + private static byte[] readBounded(InputStream inputStream, long maxBytes) { + try { + ByteArrayOutputStream output = new ByteArrayOutputStream(); + byte[] buffer = new byte[8_192]; + long total = 0; + int read; + while ((read = inputStream.read(buffer)) >= 0) { + total += read; + if (total > maxBytes) { + throw new SkillException("Skill content exceeds limit."); + } + output.write(buffer, 0, read); + } + return output.toByteArray(); + } catch (IOException exception) { + throw new SkillException("Failed to read Skill content.", exception); + } + } + + private static final class StoredContent { + + private final byte[] bytes; + private int references = 1; + + private StoredContent(byte[] bytes) { + this.bytes = bytes; + } + } +} diff --git a/easy-agents-skill/src/test/java/com/easyagents/skill/store/memory/InMemorySkillContentStoreTest.java b/easy-agents-skill/src/test/java/com/easyagents/skill/store/memory/InMemorySkillContentStoreTest.java deleted file mode 100644 index e5f14d3..0000000 --- a/easy-agents-skill/src/test/java/com/easyagents/skill/store/memory/InMemorySkillContentStoreTest.java +++ /dev/null @@ -1,83 +0,0 @@ -package com.easyagents.skill.store.memory; - -import com.easyagents.skill.store.SkillContentStage; -import org.junit.Assert; -import org.junit.Test; - -import java.io.InputStream; -import java.nio.charset.StandardCharsets; - -/** - * InMemorySkillContentStore 单元测试。 - */ -public class InMemorySkillContentStoreTest { - - /** - * put 返回 sha256 内容引用,且相同内容引用相同。 - */ - @Test - public void putReturnsStableSha256Ref() { - InMemorySkillContentStore store = new InMemorySkillContentStore(); - - String firstRef = store.put("abc".getBytes(StandardCharsets.UTF_8)); - String secondRef = store.put("abc".getBytes(StandardCharsets.UTF_8)); - - Assert.assertTrue(firstRef.startsWith("sha256:")); - Assert.assertEquals(firstRef, secondRef); - } - - /** - * open、readAllBytes 和 exists 正常工作。 - * - * @throws Exception 读取流失败时抛出 - */ - @Test - public void openReadAndExists() throws Exception { - InMemorySkillContentStore store = new InMemorySkillContentStore(); - byte[] bytes = "abc".getBytes(StandardCharsets.UTF_8); - - String contentRef = store.put(bytes); - - Assert.assertTrue(store.exists(contentRef)); - Assert.assertArrayEquals(bytes, store.readAllBytes(contentRef)); - try (InputStream inputStream = store.open(contentRef)) { - Assert.assertArrayEquals(bytes, inputStream.readAllBytes()); - } - } - - /** - * 暂存内容在 commit 前不可见,rollback 后不残留。 - */ - @Test - public void stageCommitAndRollback() { - InMemorySkillContentStore store = new InMemorySkillContentStore(); - byte[] bytes = "abc".getBytes(StandardCharsets.UTF_8); - - SkillContentStage rolledBack = store.stage(new java.io.ByteArrayInputStream(bytes), bytes.length); - Assert.assertFalse(store.exists(rolledBack.getContentRef())); - store.rollback(rolledBack); - Assert.assertFalse(store.exists(rolledBack.getContentRef())); - - SkillContentStage committed = store.stage(new java.io.ByteArrayInputStream(bytes), bytes.length); - String contentRef = store.commit(committed); - Assert.assertTrue(store.exists(contentRef)); - Assert.assertArrayEquals(bytes, store.readAllBytes(contentRef)); - } - - /** - * 相同内容按引用计数释放,归零后删除。 - */ - @Test - public void releaseDeletesOnlyAfterReferenceCountReachesZero() { - InMemorySkillContentStore store = new InMemorySkillContentStore(); - byte[] bytes = "abc".getBytes(StandardCharsets.UTF_8); - - String first = store.put(bytes); - String second = store.put(bytes); - store.release(first); - - Assert.assertTrue(store.exists(second)); - store.release(second); - Assert.assertFalse(store.exists(second)); - } -} diff --git a/easy-agents-skill/src/test/java/com/easyagents/skill/util/SkillResourcesTest.java b/easy-agents-skill/src/test/java/com/easyagents/skill/util/SkillResourcesTest.java index 555e83d..95016f8 100644 --- a/easy-agents-skill/src/test/java/com/easyagents/skill/util/SkillResourcesTest.java +++ b/easy-agents-skill/src/test/java/com/easyagents/skill/util/SkillResourcesTest.java @@ -1,6 +1,5 @@ package com.easyagents.skill.util; -import com.easyagents.skill.model.SkillResourceKind; import org.junit.Assert; import org.junit.Test; @@ -15,8 +14,18 @@ public class SkillResourcesTest { @Test public void recognizeBothHtmlExtensionsAsText() { Assert.assertTrue(SkillResources.isText( - "references/page.htm", SkillResourceKind.REFERENCE, "application/octet-stream")); + "references/page.htm", "application/octet-stream")); Assert.assertTrue(SkillResources.isText( - "references/page.html", SkillResourceKind.REFERENCE, "application/octet-stream")); + "references/page.html", "application/octet-stream")); + } + + /** + * 资源目录只表达语义分类,不决定文本或二进制存储。 + */ + @Test + public void determineContentModeIndependentlyFromTopLevelDirectory() { + Assert.assertTrue(SkillResources.isText("assets/template.md", "application/octet-stream")); + Assert.assertTrue(SkillResources.isText("custom/data.csv", "application/octet-stream")); + Assert.assertFalse(SkillResources.isText("scripts/helper.bin", "application/octet-stream")); } } diff --git a/easy-agents-skill/src/test/java/com/easyagents/skill/validation/defaults/DefaultSkillValidatorTest.java b/easy-agents-skill/src/test/java/com/easyagents/skill/validation/defaults/DefaultSkillValidatorTest.java index 7af188f..ad91028 100644 --- a/easy-agents-skill/src/test/java/com/easyagents/skill/validation/defaults/DefaultSkillValidatorTest.java +++ b/easy-agents-skill/src/test/java/com/easyagents/skill/validation/defaults/DefaultSkillValidatorTest.java @@ -45,15 +45,15 @@ public class DefaultSkillValidatorTest { String content = "---\nname: nested-skill\ndescription: Handles nested metadata\n" + "metadata:\n enabled: true\n retries: 3\n tags:\n - alpha\n - beta\n" + "---\n# Nested\n"; - Skill skill = SkillFactory.create("repository-id", content); + Skill skill = SkillFactory.create(content); validator.validate(skill); - Assert.assertTrue(skill.getMetadata().get("metadata") instanceof java.util.Map); + Assert.assertTrue(skill.getDocument().getFrontmatter().get("metadata") instanceof java.util.Map); } /** - * 结构化文档重新应用到聚合时同步 name、description 和 metadata。 + * 结构化文档更新后名称与描述从 frontmatter 派生。 */ @Test public void applyEditedDocumentToAggregate() { @@ -64,7 +64,8 @@ public class DefaultSkillValidatorTest { skill.setDocument(document); Assert.assertEquals("Updated description for the Skill", skill.getDescription()); - Assert.assertEquals("Updated description for the Skill", skill.getMetadata().get("description")); + Assert.assertEquals("Updated description for the Skill", + skill.getDocument().getFrontmatter().get("description")); validator.validate(skill); } @@ -98,20 +99,12 @@ public class DefaultSkillValidatorTest { && issue.getSeverity() == SkillValidationSeverity.ERROR)); } - /** - * 严格工厂入口执行正式标准名称校验。 - */ - @Test(expected = SkillValidationException.class) - public void strictFactoryRejectsLegacyUnderscoreName() { - SkillFactory.createStrict("repository-id", skillMd("legacy_skill")); - } - /** * AgentScope 嵌套 metadata 保真并标记标准兼容 warning。 */ @Test public void nestedMetadataProducesCompatibilityWarning() { - Skill skill = SkillFactory.create("id", "---\nname: metadata-skill\n" + Skill skill = SkillFactory.create("---\nname: metadata-skill\n" + "description: Nested metadata compatibility\nmetadata:\n provider:\n enabled: true\n" + "---\n# Metadata\n"); @@ -127,7 +120,7 @@ public class DefaultSkillValidatorTest { */ @Test public void rejectInvalidOptionalStandardFields() { - Skill skill = SkillFactory.create("id", "---\nname: optional-skill\n" + Skill skill = SkillFactory.create("---\nname: optional-skill\n" + "description: Invalid optional fields\nlicense: []\ncompatibility: ''\nallowed-tools:\n - Read\n" + "---\n# Optional\n"); @@ -261,10 +254,10 @@ public class DefaultSkillValidatorTest { } /** - * scripts 目录只接受严格 UTF-8 文本表示,二进制引用必须被拒绝。 + * 文本扩展名必须使用严格 UTF-8 文本表示,与所在目录无关。 */ @Test - public void rejectBinaryScriptResource() { + public void rejectBinaryRepresentationForTextExtension() { Skill skill = validSkill("skill-a"); SkillResource script = binaryResource("scripts/run.sh", "echo unsafe"); script.setKind(SkillResourceKind.SCRIPT); @@ -273,23 +266,37 @@ public class DefaultSkillValidatorTest { SkillValidationReport report = validator.validateReport(skill); Assert.assertTrue(report.getIssues().stream().anyMatch(issue -> - "SCRIPT_TEXT_REQUIRED".equals(issue.getCode()) + "RESOURCE_CONTENT_MODE_MISMATCH".equals(issue.getCode()) && issue.getSeverity() == SkillValidationSeverity.ERROR)); } /** - * 资源的文本或二进制表示必须与统一路径和媒体类型判定一致。 + * assets 目录中的文本资源使用规范文本表示。 */ @Test - public void rejectNonCanonicalResourceContentMode() { + public void acceptTextResourceInAssetsDirectory() { Skill skill = validSkill("skill-a"); SkillResource textAsset = textResource("assets/readme.txt", SkillResourceKind.ASSET, "text asset"); skill.setResources(java.util.List.of(textAsset)); SkillValidationReport report = validator.validateReport(skill); - Assert.assertTrue(report.getIssues().stream().anyMatch(issue -> - "RESOURCE_CONTENT_MODE_MISMATCH".equals(issue.getCode()))); + Assert.assertFalse(report.hasErrors()); + } + + /** + * scripts 目录允许保存不透明二进制辅助文件。 + */ + @Test + public void acceptBinaryResourceInScriptsDirectory() { + Skill skill = validSkill("skill-a"); + SkillResource binaryScript = binaryResource("scripts/helper.bin", "opaque"); + binaryScript.setKind(SkillResourceKind.SCRIPT); + skill.setResources(java.util.List.of(binaryScript)); + + SkillValidationReport report = validator.validateReport(skill); + + Assert.assertFalse(report.hasErrors()); } /** @@ -309,23 +316,6 @@ public class DefaultSkillValidatorTest { && issue.getSeverity() == SkillValidationSeverity.WARNING)); } - /** - * 未识别脚本语言需要提示,但不能破坏外部标准 Skill 包的资源保真。 - */ - @Test - public void warnForUnrecognizedScriptLanguageExtension() { - Skill skill = validSkill("skill-a"); - skill.setResources(java.util.List.of( - textResource("scripts/run.txt", SkillResourceKind.SCRIPT, "echo ok"))); - - SkillValidationReport report = validator.validateReport(skill); - - Assert.assertFalse(report.hasErrors()); - Assert.assertTrue(report.getIssues().stream().anyMatch(issue -> - "SCRIPT_LANGUAGE_UNRECOGNIZED".equals(issue.getCode()) - && issue.getSeverity() == SkillValidationSeverity.WARNING)); - } - /** * 结构化校验器执行资源单文件安全限额。 */ @@ -361,19 +351,8 @@ public class DefaultSkillValidatorTest { Assert.assertFalse(report.hasErrors()); } - /** - * 元数据与 SKILL.md 不一致时失败。 - */ - @Test(expected = SkillValidationException.class) - public void rejectMetadataMismatch() { - Skill skill = validSkill("skill-a"); - skill.getMetadata().put("extra", "value"); - - validator.validate(skill); - } - private static Skill validSkill(String name) { - return SkillFactory.create("repository-id", skillMd(name)); + return SkillFactory.create(skillMd(name)); } private static String skillMd(String name) {