{"record":{"id":"257c6a1f1d9c060a","repo":"siyuan-note/siyuan","slug":"skill-source-is-required","errorCode":null,"errorMessage":"skill source is required","messagePattern":"skill source is required","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"warning","filePath":"kernel/util/skill.go","lineNumber":253,"sourceCode":"var ownerRepoPattern = regexp.MustCompile(`^[A-Za-z0-9][A-Za-z0-9._-]*/[A-Za-z0-9][A-Za-z0-9._-]*$`)\n\n// skillsAddPattern 从 \"npx skills add owner/repo ...\" 这类命令里提取 owner/repo\nvar skillsAddPattern = regexp.MustCompile(`(?:^|\\s)([A-Za-z0-9][A-Za-z0-9._-]*/[A-Za-z0-9][A-Za-z0-9._-]*)(?:\\s|$)`)\n\n// normalizedSkillSource 描述归一化后的下载源\ntype normalizedSkillSource struct {\n\tdownloadURL string // 实际 GET 的地址\n\tisZip       bool   // 是否按 zip 解压处理（codeload / release zip / Content-Type 判定为 zip）\n\tbranch      string // codeload 分支，空表示无需回退；main 失败回退 master\n}\n\n// InstallSkill 从 GitHub 仓库或直链下载并安装 skill 到 SkillsDir()。\n// 支持的输入：owner/repo 简写、整条 \"npx skills add owner/repo -g\" 命令、\n// 完整 GitHub 仓库/子目录/commit URL、raw SKILL.md 直链、release zip 直链。\nfunc InstallSkill(rawURL string) (*InstallSkillResult, error) {\n\trawURL = strings.TrimSpace(rawURL)\n\tif rawURL == \"\" {\n\t\treturn nil, errors.New(\"skill source is required\")\n\t}\n\n\tsrc, err := normalizeSkillURL(rawURL)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tdata, contentType, err := downloadSkillSource(src)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// 按内容类型或来源判定处理方式\n\tisZip := src.isZip || strings.HasPrefix(contentType, \"application/zip\") ||\n\t\tstrings.HasPrefix(contentType, \"application/x-zip-compressed\")\n\n\tif isZip {\n\t\treturn installFromZip(data)","sourceCodeStart":235,"sourceCodeEnd":271,"githubUrl":"https://github.com/siyuan-note/siyuan/blob/251596fc0de2f9528c00c224252fd073a99973f4/kernel/util/skill.go#L235-L271","documentation":"Thrown by InstallSkill at the very top when rawURL is empty after trimming whitespace. It is the precondition guard ensuring a non-empty source string before any URL normalization or network call. Pure input validation, no I/O involved.","triggerScenarios":"Calling InstallSkill with an empty string, only whitespace, or a value that became empty after TrimSpace. Typically an API handler or UI that forwarded an unvalidated empty field.","commonSituations":"The install dialog was submitted without a URL; a programmatic caller passed a zero-value string; an upstream trim already cleared the field but the caller did not short-circuit.","solutions":["Validate non-empty input in the API/UI handler before calling InstallSkill and return a clear field error.","Trim and check the string client-side; disable the install button when empty.","Default to a documented example URL in test scaffolding rather than an empty string."],"exampleFix":"// before\nres, err := util.InstallSkill(strings.TrimSpace(input))\n\n// after\nsrc := strings.TrimSpace(input)\nif src == \"\" {\n    return errors.New(\"please provide a skill source URL or owner/repo\")\n}\nres, err := util.InstallSkill(src)","handlingStrategy":"validation","validationCode":"src := strings.TrimSpace(rawURL)\nif src == \"\" {\n    return errors.New(\"a skill source URL or owner/repo is required\")\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Disable the install action in the UI when the input is blank.","Trim and validate input in the API handler before forwarding to InstallSkill.","Provide a placeholder/example so users know the expected format."],"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"}