diff --git a/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/skill/metadata/SkillMetadataParser.java b/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/skill/metadata/SkillMetadataParser.java index ffaae86c..a461d2d2 100644 --- a/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/skill/metadata/SkillMetadataParser.java +++ b/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/skill/metadata/SkillMetadataParser.java @@ -3,6 +3,7 @@ package com.iflytek.skillhub.domain.skill.metadata; import com.iflytek.skillhub.domain.shared.exception.DomainBadRequestException; import org.yaml.snakeyaml.Yaml; +import java.util.LinkedHashMap; import java.util.Map; public class SkillMetadataParser { @@ -35,14 +36,7 @@ public class SkillMetadataParser { Map frontmatter; try { - Yaml yaml = new Yaml(); - Object parsed = yaml.load(yamlContent); - if (!(parsed instanceof Map)) { - throw new DomainBadRequestException("error.skill.metadata.yaml.notMap"); - } - @SuppressWarnings("unchecked") - Map map = (Map) parsed; - frontmatter = map; + frontmatter = parseFrontmatter(yamlContent); } catch (DomainBadRequestException e) { throw e; } catch (Exception e) { @@ -56,6 +50,62 @@ public class SkillMetadataParser { return new SkillMetadata(name, description, version, body, frontmatter); } + private Map parseFrontmatter(String yamlContent) { + try { + Yaml yaml = new Yaml(); + Object parsed = yaml.load(yamlContent); + if (!(parsed instanceof Map)) { + throw new DomainBadRequestException("error.skill.metadata.yaml.notMap"); + } + @SuppressWarnings("unchecked") + Map map = (Map) parsed; + return map; + } catch (DomainBadRequestException exception) { + throw exception; + } catch (Exception exception) { + Map fallback = parseLooseFrontmatter(yamlContent); + if (!fallback.isEmpty()) { + return fallback; + } + throw exception; + } + } + + private Map parseLooseFrontmatter(String yamlContent) { + Map values = new LinkedHashMap<>(); + for (String rawLine : yamlContent.split("\\R")) { + String line = rawLine.trim(); + if (line.isEmpty() || line.startsWith("#")) { + continue; + } + + int separatorIndex = line.indexOf(':'); + if (separatorIndex <= 0) { + continue; + } + + String key = line.substring(0, separatorIndex).trim(); + String value = line.substring(separatorIndex + 1).trim(); + if (key.isEmpty()) { + continue; + } + + values.put(key, stripWrappingQuotes(value)); + } + return values; + } + + private String stripWrappingQuotes(String value) { + if (value.length() >= 2) { + boolean wrappedInDoubleQuotes = value.startsWith("\"") && value.endsWith("\""); + boolean wrappedInSingleQuotes = value.startsWith("'") && value.endsWith("'"); + if (wrappedInDoubleQuotes || wrappedInSingleQuotes) { + return value.substring(1, value.length() - 1); + } + } + return value; + } + private String extractRequiredField(Map frontmatter, String fieldName) { Object value = frontmatter.get(fieldName); if (value == null) { diff --git a/server/skillhub-domain/src/test/java/com/iflytek/skillhub/domain/skill/metadata/SkillMetadataParserTest.java b/server/skillhub-domain/src/test/java/com/iflytek/skillhub/domain/skill/metadata/SkillMetadataParserTest.java index 2c864578..15969023 100644 --- a/server/skillhub-domain/src/test/java/com/iflytek/skillhub/domain/skill/metadata/SkillMetadataParserTest.java +++ b/server/skillhub-domain/src/test/java/com/iflytek/skillhub/domain/skill/metadata/SkillMetadataParserTest.java @@ -124,7 +124,7 @@ class SkillMetadataParserTest { } @Test - void testThrowsWhenInvalidYaml() { + void testFallsBackToLooseFrontmatterParsingWhenYamlSyntaxIsNotStrict() { String content = """ --- name: test-skill @@ -134,11 +134,29 @@ class SkillMetadataParserTest { Body """; - DomainBadRequestException exception = assertThrows( - DomainBadRequestException.class, - () -> parser.parse(content) - ); - assertEquals("error.skill.metadata.yaml.invalid", exception.messageCode()); + SkillMetadata metadata = parser.parse(content); + + assertEquals("test-skill", metadata.name()); + assertEquals("[unclosed bracket", metadata.description()); + assertEquals("1.0.0", metadata.version()); + } + + @Test + void testAllowsColonInDescriptionWithoutStrictYamlQuoting() { + String content = """ + --- + name: clawdbot + description: Send messages from Clawdbot via the discord tool: send messages, react, post or edit + version: 1.0.0 + --- + Body + """; + + SkillMetadata metadata = parser.parse(content); + + assertEquals("clawdbot", metadata.name()); + assertEquals("Send messages from Clawdbot via the discord tool: send messages, react, post or edit", metadata.description()); + assertEquals("1.0.0", metadata.version()); } @Test 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 a880f7d5..535ca17c 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 @@ -143,7 +143,7 @@ class SkillPackageValidatorTest { } @Test - void testInvalidYamlFrontmatterUsesFriendlyMessage() { + void testInvalidYamlFrontmatterWithColonInValueShouldStillPass() { String skillMdContent = """ --- name: clawdbot @@ -159,12 +159,8 @@ class SkillPackageValidatorTest { 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"))); + assertTrue(result.passed()); + assertTrue(result.errors().isEmpty()); } @Test