{"record":{"id":"0389996b8091dbfa","repo":"siyuan-note/siyuan","slug":"unrecognized-skill-source-s","errorCode":null,"errorMessage":"unrecognized skill source: %s","messagePattern":"unrecognized skill source: (.+?)","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"kernel/util/skill.go","lineNumber":301,"sourceCode":"func normalizeSkillURL(raw string) (normalizedSkillSource, error) {\n\traw = strings.TrimSpace(raw)\n\n\t// 1. 整条 \"npx skills add owner/repo ...\" 命令：提取 owner/repo\n\tif strings.Contains(raw, \"skills add\") || strings.Contains(raw, \"skills@\") {\n\t\tif m := skillsAddPattern.FindStringSubmatch(raw); len(m) == 2 {\n\t\t\treturn codeloadSource(m[1], \"main\"), nil\n\t\t}\n\t}\n\n\t// 2. owner/repo 简写（无 scheme、无点、单个 /）\n\tif !strings.Contains(raw, \"://\") && !strings.Contains(raw, \"//\") && ownerRepoPattern.MatchString(raw) {\n\t\treturn codeloadSource(raw, \"main\"), nil\n\t}\n\n\t// 3. 带 scheme 的 URL\n\tu, err := url.Parse(raw)\n\tif err != nil || u.Scheme == \"\" || u.Host == \"\" {\n\t\treturn normalizedSkillSource{}, fmt.Errorf(\"unrecognized skill source: %s\", raw)\n\t}\n\n\tswitch u.Host {\n\tcase \"github.com\":\n\t\treturn normalizeGitHubURL(u)\n\tcase \"raw.githubusercontent.com\":\n\t\t// 直接 GET 单个 SKILL.md（或其它文本文件）\n\t\treturn normalizedSkillSource{downloadURL: u.String()}, nil\n\tdefault:\n\t\t// 其它直链（release zip、自建站点等）：直接 GET，是否 zip 交由 Content-Type 判定\n\t\treturn normalizedSkillSource{downloadURL: u.String()}, nil\n\t}\n}\n\n// codeloadSource 构造 codeload zip 下载源，branch 用于 main→master 回退\nfunc codeloadSource(ownerRepo, branch string) normalizedSkillSource {\n\treturn normalizedSkillSource{\n\t\tdownloadURL: \"https://codeload.github.com/\" + ownerRepo + \"/zip/refs/heads/\" + branch,","sourceCodeStart":283,"sourceCodeEnd":319,"githubUrl":"https://github.com/siyuan-note/siyuan/blob/251596fc0de2f9528c00c224252fd073a99973f4/kernel/util/skill.go#L283-L319","documentation":"Thrown by normalizeSkillURL when the input is neither an 'npx skills add' command, an owner/repo shorthand, nor a parseable URL with scheme and host. It is the catch-all rejection for inputs that do not match any supported source format.","triggerScenarios":"Passing a bare string with no scheme (e.g. 'myskill'), a malformed URL (e.g. 'htp://example'), a URL with empty host, or a value containing '://' that url.Parse rejects. The owner/repo regex also requires no dots and exactly one slash, so 'example.com/x' bypasses it.","commonSituations":"User typed a skill name instead of a source; missing https:// prefix; copy-paste introduced a stray character; the shorthand regex rejected input containing dots or multiple slashes.","solutions":["Provide a full URL starting with https://, an owner/repo shorthand, or the full 'npx skills add owner/repo' command.","Ensure owner/repo shorthand has no scheme, no '//' and exactly one slash with no dots in either segment.","Validate and pre-format the input in the UI before submission."],"exampleFix":"// before\nutil.InstallSkill(\"myskill\")\n\n// after — valid shorthand or full URL\nutil.InstallSkill(\"owner/repo\")\n// or\nutil.InstallSkill(\"https://github.com/owner/repo\")","handlingStrategy":"validation","validationCode":"import \"net/url\"\n\nfunc isValidSkillSource(raw string) bool {\n    raw = strings.TrimSpace(raw)\n    if raw == \"\" {\n        return false\n    }\n    if strings.Contains(raw, \"skills add\") || strings.Contains(raw, \"skills@\") {\n        return true\n    }\n    if !strings.Contains(raw, \"://\") && !strings.Contains(raw, \"//\") && ownerRepoPattern.MatchString(raw) {\n        return true\n    }\n    u, err := url.Parse(raw)\n    return err == nil && u.Scheme != \"\" && u.Host != \"\"\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Document the three accepted input shapes to users (shorthand, npx command, full URL).","Pre-validate with url.Parse and report the parse error verbatim.","Avoid pasting URLs with leading/trailing whitespace or smart quotes."],"tags":["skill-management","validation","input"],"backgroundTag":null,"analyzedSha":"251596fc0de2f9528c00c224252fd073a99973f4","analyzedAt":"2026-08-12T21:18:37.123Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}