fix: relax skill frontmatter parsing

This commit is contained in:
yun-zhi-ztl 2026-03-17 11:22:08 +08:00
parent 4e71c93e86
commit 43493fa63d
3 changed files with 85 additions and 21 deletions

View file

@ -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<String, Object> 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<String, Object> map = (Map<String, Object>) 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<String, Object> 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<String, Object> map = (Map<String, Object>) parsed;
return map;
} catch (DomainBadRequestException exception) {
throw exception;
} catch (Exception exception) {
Map<String, Object> fallback = parseLooseFrontmatter(yamlContent);
if (!fallback.isEmpty()) {
return fallback;
}
throw exception;
}
}
private Map<String, Object> parseLooseFrontmatter(String yamlContent) {
Map<String, Object> 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<String, Object> frontmatter, String fieldName) {
Object value = frontmatter.get(fieldName);
if (value == null) {

View file

@ -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

View file

@ -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