{"record":{"id":"4b4cbadba6a07e32","repo":"siyuan-note/siyuan","slug":"invalid-github-url-s","errorCode":null,"errorMessage":"invalid github URL: %s","messagePattern":"invalid github URL: (.+?)","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"kernel/util/skill.go","lineNumber":333,"sourceCode":"\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,\n\t\tisZip:       true,\n\t\tbranch:      branch,\n\t}\n}\n\n// normalizeGitHubURL 处理 github.com 的各种路径形态\nfunc normalizeGitHubURL(u *url.URL) (normalizedSkillSource, error) {\n\t// /owner/repo/tree/<branch> 或 /owner/repo/tree/<branch>/<path>\n\t// /owner/repo/commit/<sha>\n\t// /owner/repo/releases/download/<tag>/<asset>\n\t// /owner/repo（默认分支）\n\tparts := strings.Split(strings.Trim(u.Path, \"/\"), \"/\")\n\tif len(parts) < 2 {\n\t\treturn normalizedSkillSource{}, fmt.Errorf(\"invalid github URL: %s\", u.String())\n\t}\n\townerRepo := parts[0] + \"/\" + parts[1]\n\n\t// releases/download/<tag>/<asset>\n\tif len(parts) >= 6 && parts[2] == \"releases\" && parts[3] == \"download\" {\n\t\tasset := parts[5]\n\t\t// 是否 zip 交由 Content-Type 最终判定，这里仅按 asset 后缀预判\n\t\treturn normalizedSkillSource{downloadURL: u.String(), isZip: strings.HasSuffix(asset, \".zip\")}, nil\n\t}\n\n\t// tree/<branch>[/path] 或 blob/<branch>/...\n\tif len(parts) >= 4 && (parts[2] == \"tree\" || parts[2] == \"blob\") {\n\t\tbranch := parts[3]\n\t\tif parts[2] == \"blob\" {\n\t\t\t// blob 指向单个文件，走 raw\n\t\t\trawPath := strings.Join(parts[4:], \"/\")\n\t\t\treturn normalizedSkillSource{\n\t\t\t\tdownloadURL: \"https://raw.githubusercontent.com/\" + ownerRepo + \"/\" + branch + \"/\" + rawPath,","sourceCodeStart":315,"sourceCodeEnd":351,"githubUrl":"https://github.com/siyuan-note/siyuan/blob/251596fc0de2f9528c00c224252fd073a99973f4/kernel/util/skill.go#L315-L351","documentation":"Thrown by normalizeGitHubURL when a github.com URL path has fewer than two segments (i.e. lacks both owner and repo). The installer requires at least /owner/repo to build a codeload download URL; anything shorter is ambiguous.","triggerScenarios":"Passing 'https://github.com/' or 'https://github.com/owner' (only one path segment); a URL whose path was stripped to empty by trimming.","commonSituations":"User copied a profile URL instead of a repository URL; trailing slash trimming collapsed the path; the URL is the GitHub root.","solutions":["Provide a full repository URL such as https://github.com/owner/repo.","If targeting a subdirectory, use the tree/<branch>/<path> form with at least owner/repo present.","Validate the path has at least two segments client-side."],"exampleFix":"// before\nutil.InstallSkill(\"https://github.com/owner\")\n\n// after\nutil.InstallSkill(\"https://github.com/owner/repo\")","handlingStrategy":"validation","validationCode":"u, err := url.Parse(src)\nif err == nil && u.Host == \"github.com\" {\n    segs := strings.Split(strings.Trim(u.Path, \"/\"), \"/\")\n    if len(segs) < 2 || segs[0] == \"\" || segs[1] == \"\" {\n        return errors.New(\"github URL must include owner and repo\")\n    }\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Always copy the repository URL from the browser address bar to ensure owner/repo are present.","Validate github.com paths have at least two segments before submission.","Reject profile-only URLs (https://github.com/<owner>) in the UI."],"tags":["skill-management","validation","github"],"backgroundTag":null,"analyzedSha":"251596fc0de2f9528c00c224252fd073a99973f4","analyzedAt":"2026-08-12T21:18:37.123Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}