{"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":"error","filePath":"kernel/util/skill.go","lineNumber":555,"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":537,"sourceCodeEnd":573,"githubUrl":"https://github.com/siyuan-note/siyuan/blob/9f775e8a12daef8255556097396f9b2739078892/kernel/util/skill.go#L537-L573","documentation":"InstallSkill downloads and installs a skill from a GitHub repo, raw SKILL.md link, or release zip. It rejects an empty/whitespace-only source string with 'skill source is required' before any URL normalization is attempted.","triggerScenarios":"Calling InstallSkill(\"\") or InstallSkill(\"   \") — a blank source string after TrimSpace.","commonSituations":"Empty input field in the install dialog; variable not assigned before the call; config value for skill source missing or blank; form submitted without filling the source.","solutions":["Provide a non-empty source: an owner/repo shorthand, a GitHub URL, or a raw SKILL.md/zip URL","Validate the input is non-empty in the UI/form before invoking the install","Check that the config value supplying the source is populated"],"exampleFix":"// before\nInstallSkill(cfg.SkillSource) // may be \"\"\n// after\nif strings.TrimSpace(cfg.SkillSource) == \"\" {\n    return errors.New(\"skill source must be set\")\n}\nresult, err := InstallSkill(cfg.SkillSource)","handlingStrategy":"validation","validationCode":"const source = (input ?? \"\").trim();\nif (!source) throw new Error(\"skill source is required\");","typeGuard":null,"tryCatchPattern":"if (!input || !input.trim()) {\n  return { ok: false, reason: \"empty skill source\" };\n}\ntry {\n  await installSkill(input);\n} catch (e) {\n  if (String(e.message) === \"skill source is required\") {\n    // prompt the user for a source\n  }\n}","preventionTips":["Validate install-form inputs before submission","Check that config values feeding the source are non-empty","Trim whitespace from pasted sources"],"tags":["validation","empty-input","skills"],"backgroundTag":"empty-required-field","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"}