diff --git a/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/skill/validation/SkillPackageValidator.java b/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/skill/validation/SkillPackageValidator.java index e47158f4..a483612c 100644 --- a/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/skill/validation/SkillPackageValidator.java +++ b/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/skill/validation/SkillPackageValidator.java @@ -7,8 +7,11 @@ import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; public class SkillPackageValidator { + private static final Pattern YAML_LINE_COLUMN = Pattern.compile("line\\s+(\\d+),\\s+column\\s+(\\d+)"); private final SkillMetadataParser metadataParser; private final int maxFileCount; @@ -83,10 +86,7 @@ public class SkillPackageValidator { String content = new String(skillMd.content()); metadataParser.parse(content); } catch (LocalizedDomainException e) { - String detail = e.messageArgs().length == 0 - ? e.messageCode() - : e.messageCode() + " " + java.util.Arrays.toString(e.messageArgs()); - errors.add("Invalid SKILL.md frontmatter: " + detail); + errors.add("Invalid SKILL.md frontmatter: " + formatMetadataError(e)); } // 3. Check file count @@ -113,4 +113,38 @@ public class SkillPackageValidator { private boolean hasAllowedExtension(String normalizedPath) { return allowedExtensions.stream().anyMatch(normalizedPath::endsWith); } + + private String formatMetadataError(LocalizedDomainException exception) { + return switch (exception.messageCode()) { + case "error.skill.metadata.requiredField.missing" -> + "missing required field \"" + exception.messageArgs()[0] + "\""; + case "error.skill.metadata.frontmatter.missingStart" -> + "missing opening --- marker"; + case "error.skill.metadata.frontmatter.missingEnd" -> + "missing closing --- marker"; + case "error.skill.metadata.frontmatter.missingContent" -> + "frontmatter is empty"; + case "error.skill.metadata.yaml.notMap" -> + "frontmatter must be a YAML object"; + case "error.skill.metadata.yaml.invalid" -> + formatYamlSyntaxError(exception.messageArgs()); + default -> { + if (exception.messageArgs().length == 0) { + yield exception.messageCode(); + } + yield exception.messageCode() + " " + java.util.Arrays.toString(exception.messageArgs()); + } + }; + } + + private String formatYamlSyntaxError(Object[] args) { + String raw = args.length > 0 && args[0] != null ? args[0].toString() : ""; + Matcher matcher = YAML_LINE_COLUMN.matcher(raw); + if (matcher.find()) { + return "invalid YAML near line " + matcher.group(1) + + ", column " + matcher.group(2) + + ". If a value contains a colon, wrap it in quotes."; + } + return "invalid YAML syntax"; + } } diff --git a/server/skillhub-domain/src/test/java/com/iflytek/skillhub/domain/skill/validation/SkillPackageValidatorTest.java b/server/skillhub-domain/src/test/java/com/iflytek/skillhub/domain/skill/validation/SkillPackageValidatorTest.java index 9a140beb..a880f7d5 100644 --- a/server/skillhub-domain/src/test/java/com/iflytek/skillhub/domain/skill/validation/SkillPackageValidatorTest.java +++ b/server/skillhub-domain/src/test/java/com/iflytek/skillhub/domain/skill/validation/SkillPackageValidatorTest.java @@ -142,6 +142,31 @@ class SkillPackageValidatorTest { assertTrue(result.errors().stream().anyMatch(e -> e.contains("Invalid SKILL.md frontmatter") && e.contains("name"))); } + @Test + void testInvalidYamlFrontmatterUsesFriendlyMessage() { + String skillMdContent = """ + --- + name: clawdbot + description: Send messages from Clawdbot via the discord tool: send messages, react, post or edit + version: 1.0.0 + --- + Body + """; + + List entries = List.of( + new PackageEntry("SKILL.md", skillMdContent.getBytes(), skillMdContent.length(), "text/markdown") + ); + + ValidationResult result = validator.validate(entries); + + assertFalse(result.passed()); + assertTrue(result.errors().stream().anyMatch(e -> + e.contains("Invalid SKILL.md frontmatter") + && e.contains("line") + && e.contains("column"))); + assertFalse(result.errors().stream().anyMatch(e -> e.contains("mapping values are not allowed here"))); + } + @Test void testPackageTooLarge() { String skillMdContent = """ diff --git a/web/src/i18n/locales/en.json b/web/src/i18n/locales/en.json index a0416e64..e7a685ad 100644 --- a/web/src/i18n/locales/en.json +++ b/web/src/i18n/locales/en.json @@ -591,6 +591,8 @@ "versionExistsDescription": "This skill version has already been published. Update the version in SKILL.md, rebuild the package, and upload it again.", "precheckFailedTitle": "Pre-publish check failed", "precheckFailedDescription": "The package appears to contain a secret, token, or password. Replace real credentials with placeholders and try again.", + "frontmatterFailedTitle": "SKILL.md format is invalid", + "frontmatterFailedDescription": "Please check the YAML frontmatter at the top of SKILL.md. If a field value contains a colon, wrap it in quotes.", "selectRequired": "Please select namespace and file" }, "toast": { diff --git a/web/src/i18n/locales/zh.json b/web/src/i18n/locales/zh.json index f9d2c329..04830c27 100644 --- a/web/src/i18n/locales/zh.json +++ b/web/src/i18n/locales/zh.json @@ -591,6 +591,8 @@ "versionExistsDescription": "当前技能版本已经发布过,请修改 SKILL.md 中的 version 后重新打包上传。", "precheckFailedTitle": "发布前校验未通过", "precheckFailedDescription": "技能包中包含疑似密钥、令牌或密码内容。请将真实凭证替换为占位符后再重试。", + "frontmatterFailedTitle": "SKILL.md 格式有误", + "frontmatterFailedDescription": "请检查 SKILL.md 顶部 frontmatter 的 YAML 格式。若字段值中包含冒号,请用引号包裹。", "selectRequired": "请选择命名空间和文件" }, "toast": { diff --git a/web/src/pages/dashboard/publish.tsx b/web/src/pages/dashboard/publish.tsx index b6d81f76..a79036df 100644 --- a/web/src/pages/dashboard/publish.tsx +++ b/web/src/pages/dashboard/publish.tsx @@ -32,6 +32,15 @@ function isPrecheckFailureMessage(message?: string): boolean { || message.includes('looks like a secret or token') } +function isFrontmatterFailureMessage(message?: string): boolean { + if (!message) { + return false + } + + return message.includes('Invalid SKILL.md frontmatter') + || message.includes('技能包校验失败:Invalid SKILL.md frontmatter') +} + export function PublishPage() { const { t } = useTranslation() const navigate = useNavigate() @@ -87,6 +96,14 @@ export function PublishPage() { return } + if (error instanceof ApiError && isFrontmatterFailureMessage(error.serverMessage || error.message)) { + toast.error( + t('publish.frontmatterFailedTitle'), + error.serverMessage || t('publish.frontmatterFailedDescription'), + ) + return + } + toast.error(t('publish.error'), error instanceof Error ? error.message : '') } }