From 30545d6a8a93e8f9f9c2ea5f1db2ad88568a1e92 Mon Sep 17 00:00:00 2001 From: yun-zhi-ztl <15071461069@163.com> Date: Sun, 15 Mar 2026 18:26:14 +0800 Subject: [PATCH] fix(publish): clarify precheck failures and avoid duplicate toasts --- .../validation/BasicPrePublishValidator.java | 47 +++++++++++++++---- .../BasicPrePublishValidatorTest.java | 44 +++++++++++++---- web/src/i18n/locales/en.json | 2 + web/src/i18n/locales/zh.json | 2 + web/src/pages/dashboard/publish.tsx | 19 ++++++++ web/src/shared/hooks/use-skill-queries.ts | 3 ++ 6 files changed, 99 insertions(+), 18 deletions(-) diff --git a/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/skill/validation/BasicPrePublishValidator.java b/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/skill/validation/BasicPrePublishValidator.java index 6f17fb95..c464b4d4 100644 --- a/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/skill/validation/BasicPrePublishValidator.java +++ b/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/skill/validation/BasicPrePublishValidator.java @@ -6,16 +6,23 @@ import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.List; import java.util.Locale; +import java.util.regex.Matcher; import java.util.regex.Pattern; @Component public class BasicPrePublishValidator implements PrePublishValidator { - private static final List SECRET_PATTERNS = List.of( - Pattern.compile("AKIA[0-9A-Z]{16}"), - Pattern.compile("ghp_[A-Za-z0-9]{20,}"), - Pattern.compile("sk-[A-Za-z0-9]{20,}"), - Pattern.compile("(?i)(api[_-]?key|access[_-]?key|secret|password|token)\\s*[:=]\\s*['\\\"]?[A-Za-z0-9_\\-]{12,}") + private static final Pattern PLACEHOLDER_VALUE = Pattern.compile( + "(?i).*(your|example|sample|placeholder|changeme|replace|dummy|mock|test|fake|todo|xxx|redacted).*" + ); + private static final List SECRET_RULES = List.of( + new SecretRule(Pattern.compile("(AKIA[0-9A-Z]{16})"), 1, "cloud access key"), + new SecretRule(Pattern.compile("(ghp_[A-Za-z0-9]{20,})"), 1, "GitHub token"), + new SecretRule(Pattern.compile("(sk-[A-Za-z0-9]{20,})"), 1, "API key"), + new SecretRule( + Pattern.compile("(?i)(api[_-]?key|access[_-]?key|secret|password|token)\\s*[:=]\\s*['\\\"]?([A-Za-z0-9_\\-]{12,})"), + 2, + "secret or token") ); @Override @@ -27,9 +34,23 @@ public class BasicPrePublishValidator implements PrePublishValidator { continue; } String content = new String(entry.content(), StandardCharsets.UTF_8); - for (Pattern secretPattern : SECRET_PATTERNS) { - if (secretPattern.matcher(content).find()) { - errors.add("Potential secret detected in " + entry.path()); + String[] lines = content.split("\\R", -1); + for (int i = 0; i < lines.length; i++) { + String line = lines[i]; + for (SecretRule rule : SECRET_RULES) { + Matcher matcher = rule.pattern().matcher(line); + if (!matcher.find()) { + continue; + } + String matchedValue = matcher.group(rule.valueGroup()); + if (isPlaceholderValue(matchedValue)) { + continue; + } + errors.add(entry.path() + + " line " + (i + 1) + + " contains a value that looks like a " + + rule.label() + + ". Replace real credentials with placeholders before publishing."); break; } } @@ -51,4 +72,14 @@ public class BasicPrePublishValidator implements PrePublishValidator { || lowerPath.endsWith(".sh") || lowerPath.endsWith(".svg"); } + + private boolean isPlaceholderValue(String value) { + if (value == null || value.isBlank()) { + return false; + } + return PLACEHOLDER_VALUE.matcher(value).matches() + || value.chars().allMatch(ch -> ch == 'x' || ch == 'X' || ch == '*' || ch == '-'); + } + + private record SecretRule(Pattern pattern, int valueGroup, String label) {} } diff --git a/server/skillhub-domain/src/test/java/com/iflytek/skillhub/domain/skill/validation/BasicPrePublishValidatorTest.java b/server/skillhub-domain/src/test/java/com/iflytek/skillhub/domain/skill/validation/BasicPrePublishValidatorTest.java index 46e9e127..a40eb5c3 100644 --- a/server/skillhub-domain/src/test/java/com/iflytek/skillhub/domain/skill/validation/BasicPrePublishValidatorTest.java +++ b/server/skillhub-domain/src/test/java/com/iflytek/skillhub/domain/skill/validation/BasicPrePublishValidatorTest.java @@ -15,7 +15,7 @@ class BasicPrePublishValidatorTest { private final BasicPrePublishValidator validator = new BasicPrePublishValidator(); @Test - void shouldRejectObviousCredentialLeak() { + void shouldRejectObviousCredentialLeakWithHelpfulLocation() { PackageEntry skillMd = new PackageEntry( "SKILL.md", """ @@ -23,26 +23,24 @@ class BasicPrePublishValidatorTest { name: Secret Skill version: 1.0.0 --- + token=sk-abcdefghijklmnopqrstuvwxyz123456 """.getBytes(StandardCharsets.UTF_8), - 47, + 91, "text/markdown" ); - PackageEntry config = new PackageEntry( - "config.txt", - "OPENAI_API_KEY=sk-abcdefghijklmnopqrstuvwxyz123456".getBytes(StandardCharsets.UTF_8), - 50, - "text/plain" - ); ValidationResult result = validator.validate(new PrePublishValidator.SkillPackageContext( - List.of(skillMd, config), + List.of(skillMd), new SkillMetadata("Secret Skill", "desc", "1.0.0", "body", Map.of()), "user-1", 1L )); assertFalse(result.passed()); - assertTrue(result.errors().stream().anyMatch(error -> error.contains("Potential secret detected"))); + assertTrue(result.errors().stream().anyMatch(error -> + error.contains("SKILL.md") + && error.contains("line 5") + && error.contains("looks like a"))); } @Test @@ -74,4 +72,30 @@ class BasicPrePublishValidatorTest { assertTrue(result.passed()); } + + @Test + void shouldIgnoreObviousPlaceholderSecrets() { + PackageEntry skillMd = new PackageEntry( + "SKILL.md", + """ + --- + name: Example Skill + version: 1.0.0 + --- + token=YOUR_TOKEN_HERE + api_key=example-key-value + """.getBytes(StandardCharsets.UTF_8), + 102, + "text/markdown" + ); + + ValidationResult result = validator.validate(new PrePublishValidator.SkillPackageContext( + List.of(skillMd), + new SkillMetadata("Example Skill", "desc", "1.0.0", "body", Map.of()), + "user-1", + 1L + )); + + assertTrue(result.passed()); + } } diff --git a/web/src/i18n/locales/en.json b/web/src/i18n/locales/en.json index 838d927b..a0416e64 100644 --- a/web/src/i18n/locales/en.json +++ b/web/src/i18n/locales/en.json @@ -589,6 +589,8 @@ "timeoutDescription": "The publish request took too long. Please check the skill list later or try again.", "versionExistsTitle": "Version already exists", "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.", "selectRequired": "Please select namespace and file" }, "toast": { diff --git a/web/src/i18n/locales/zh.json b/web/src/i18n/locales/zh.json index 606d60dd..f9d2c329 100644 --- a/web/src/i18n/locales/zh.json +++ b/web/src/i18n/locales/zh.json @@ -589,6 +589,8 @@ "timeoutDescription": "本次发布等待时间过长,请稍后到技能列表确认结果,或重新尝试发布。", "versionExistsTitle": "版本号已存在", "versionExistsDescription": "当前技能版本已经发布过,请修改 SKILL.md 中的 version 后重新打包上传。", + "precheckFailedTitle": "发布前校验未通过", + "precheckFailedDescription": "技能包中包含疑似密钥、令牌或密码内容。请将真实凭证替换为占位符后再重试。", "selectRequired": "请选择命名空间和文件" }, "toast": { diff --git a/web/src/pages/dashboard/publish.tsx b/web/src/pages/dashboard/publish.tsx index febfb8e0..b6d81f76 100644 --- a/web/src/pages/dashboard/publish.tsx +++ b/web/src/pages/dashboard/publish.tsx @@ -21,6 +21,17 @@ function isVersionExistsMessage(message?: string): boolean { || message.includes('版本已存在') } +function isPrecheckFailureMessage(message?: string): boolean { + if (!message) { + return false + } + + return message.includes('error.skill.publish.precheck.failed') + || message.includes('Pre-publish validation failed') + || message.includes('预发布校验失败') + || message.includes('looks like a secret or token') +} + export function PublishPage() { const { t } = useTranslation() const navigate = useNavigate() @@ -68,6 +79,14 @@ export function PublishPage() { return } + if (error instanceof ApiError && isPrecheckFailureMessage(error.serverMessage || error.message)) { + toast.error( + t('publish.precheckFailedTitle'), + error.serverMessage || t('publish.precheckFailedDescription'), + ) + return + } + toast.error(t('publish.error'), error instanceof Error ? error.message : '') } } diff --git a/web/src/shared/hooks/use-skill-queries.ts b/web/src/shared/hooks/use-skill-queries.ts index eb2504c3..a6b5b555 100644 --- a/web/src/shared/hooks/use-skill-queries.ts +++ b/web/src/shared/hooks/use-skill-queries.ts @@ -165,6 +165,9 @@ export function usePublishSkill() { return useMutation({ mutationFn: publishSkill, + meta: { + skipGlobalErrorHandler: true, + }, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['skills', 'my'] }) },