{"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":635,"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":617,"sourceCodeEnd":653,"githubUrl":"https://github.com/siyuan-note/siyuan/blob/9f775e8a12daef8255556097396f9b2739078892/kernel/util/skill.go#L617-L653","documentation":"normalizeGitHubURL rejects a github.com URL whose path does not contain at least an owner and a repository segment. The library parses the URL path into slash-separated parts and needs at least 2 parts to build the owner/repo pair it downloads from. Any github.com URL without owner/repo (e.g. bare github.com or github.com/owner) is invalid.","triggerScenarios":"Calling InstallSkill with a github.com URL whose path splits into fewer than 2 segments, e.g. https://github.com, https://github.com/, or https://github.com/owner (missing the repo name).","commonSituations":"Truncated copy-paste of a repo URL; stripping the repo segment when templating URLs; typo like https://github.com//repo producing empty parts; pasting a GitHub org page or search URL instead of a repo URL.","solutions":["Provide a full github.com URL containing both owner and repository, e.g. https://github.com/owner/repo","Check for stray slashes or truncation when pasting the URL; remove empty segments (a leading '//' produces empty parts)","If pointing at a single file, use the full blob URL (https://github.com/owner/repo/blob/<branch>/<path>) or a raw.githubusercontent.com URL","If using a non-GitHub source, pass its direct URL (raw.githubusercontent.com or any direct link) instead of a bare host"],"exampleFix":"// before\nInstallSkill(\"https://github.com/siyuan-note\")\n// after\nInstallSkill(\"https://github.com/siyuan-note/siyuan\")","handlingStrategy":"validation","validationCode":"const u = new URL(src)\nif (u.hostname !== \"github.com\") throw new Error(\"not a github.com URL\")\nconst parts = u.pathname.replace(/^\\/+/, \"\").split(\"/\").filter(Boolean)\nif (parts.length < 2) throw new Error(\"github URL needs /<owner>/<repo>\")","typeGuard":"function isGitHubRepoUrl(src) {\n  try {\n    const u = new URL(src)\n    return u.hostname === \"github.com\" &&\n      u.pathname.replace(/^\\/+/, \"\").split(\"/\").filter(Boolean).length >= 2\n  } catch { return false }\n}","tryCatchPattern":"try {\n  await installSkill(src)\n} catch (e) {\n  if (String(e).includes(\"invalid github URL\")) {\n    showHint(\"Use a full URL: https://github.com/<owner>/<repo>\")\n  }\n}","preventionTips":["Always paste complete repo URLs including owner and repo segments","Filter empty path segments before validating (protects against double slashes)","UI: pre-validate the input field before submitting the install request"],"tags":["url","github","validation","skill-install"],"backgroundTag":"invalid-url-format","analyzedSha":"9f775e8a12daef8255556097396f9b2739078892","analyzedAt":"2026-09-19T03:17:15.984Z","contentChangedAt":"2026-09-19T03:17:15.984Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}