siyuan-note/siyuan · error

unsupported skill source

Error message

unsupported skill source (content-type: %s); expected a zip archive or a SKILL.md text file

What it means

After downloading the skill source, InstallSkill inspects the HTTP Content-Type (and a leading '---' fallback for frontmatter). If the payload is neither a zip archive nor SKILL.md-style text, it refuses to install with this error naming the observed content-type.

Solutions

  1. Use a raw.githubusercontent.com URL for SKILL.md or a direct .zip link instead of an HTML page URL
  2. Ensure release assets are zip archives (not tar.gz or bare binaries)
  3. Check the URL in a browser/curl to see what content-type it actually returns
  4. If the server sends a wrong MIME type but the content is valid SKILL.md text, ensure the body starts with '---'

Example fix

// before
InstallSkill("https://github.com/owner/repo/blob/main/SKILL.md") // HTML page
// after
InstallSkill("https://raw.githubusercontent.com/owner/repo/main/SKILL.md")
Defensive patterns

Strategy: validation

Validate before calling

const res = await fetch(url, { method: "HEAD" });
const ct = res.headers.get("content-type") || "";
const isZip = ct.includes("zip") || url.endsWith(".zip");
const isText = ct.startsWith("text/") || url.endsWith("SKILL.md");
if (!isZip && !isText) throw new Error(`unsupported content-type: ${ct}`);

Try / catch

try {
  await installSkill(url);
} catch (e) {
  if (String(e.message).startsWith("unsupported skill source")) {
    // retry with a raw.githubusercontent.com URL or a zip asset
  }
}

Prevention

When it happens

Trigger: Downloading a URL that returns a non-zip, non-text content type — e.g. an HTML error page (text/html is caught as text only if it starts with '---'; HTML pages typically won't), a binary release asset that isn't a zip, or a server returning application/octet-stream for a non-zip file.

Common situations: Pointing the installer at a GitHub web page URL instead of the raw file; a release asset that is a tar.gz rather than zip; rate-limit or login pages served with unexpected content types; misconfigured CDN returning wrong MIME type.

Related errors


AI-assisted analysis of siyuan-note/siyuan@9f775e8a12 (2026-09-19). Data as JSON: /api/errors/42aef9e191a5be28. Report an issue: GitHub.

Appendix: source

Thrown at kernel/util/skill.go:581

	data, contentType, err := downloadSkillSource(src)
	if err != nil {
		return nil, err
	}

	// 按内容类型或来源判定处理方式
	isZip := src.isZip || strings.HasPrefix(contentType, "application/zip") ||
		strings.HasPrefix(contentType, "application/x-zip-compressed")

	if isZip {
		return installFromZip(data)
	}

	// 文本:当作单个 SKILL.md
	if strings.HasPrefix(contentType, "text/") || strings.HasPrefix(strings.TrimSpace(string(data)), "---") {
		return installFromSingleSkillMD(data)
	}

	return nil, fmt.Errorf("unsupported skill source (content-type: %s); expected a zip archive or a SKILL.md text file", contentType)
}

// normalizeSkillURL 把各种输入归一化为下载源
func normalizeSkillURL(raw string) (normalizedSkillSource, error) {
	raw = strings.TrimSpace(raw)

	// 1. 整条 "npx skills add owner/repo ..." 命令:提取 owner/repo
	if strings.Contains(raw, "skills add") || strings.Contains(raw, "skills@") {
		if m := skillsAddPattern.FindStringSubmatch(raw); len(m) == 2 {
			return codeloadSource(m[1], "main"), nil
		}
	}

	// 2. owner/repo 简写(无 scheme、无点、单个 /)
	if !strings.Contains(raw, "://") && !strings.Contains(raw, "//") && ownerRepoPattern.MatchString(raw) {
		return codeloadSource(raw, "main"), nil
	}

View on GitHub (pinned to 9f775e8a12)