fix(publish): simplify frontmatter validation errors

This commit is contained in:
yun-zhi-ztl 2026-03-15 18:28:22 +08:00
parent 30545d6a8a
commit 74ac44ed65
5 changed files with 84 additions and 4 deletions

View file

@ -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";
}
}

View file

@ -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<PackageEntry> 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 = """

View file

@ -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": {

View file

@ -591,6 +591,8 @@
"versionExistsDescription": "当前技能版本已经发布过,请修改 SKILL.md 中的 version 后重新打包上传。",
"precheckFailedTitle": "发布前校验未通过",
"precheckFailedDescription": "技能包中包含疑似密钥、令牌或密码内容。请将真实凭证替换为占位符后再重试。",
"frontmatterFailedTitle": "SKILL.md 格式有误",
"frontmatterFailedDescription": "请检查 SKILL.md 顶部 frontmatter 的 YAML 格式。若字段值中包含冒号,请用引号包裹。",
"selectRequired": "请选择命名空间和文件"
},
"toast": {

View file

@ -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 : '')
}
}