feat: support custom skill sources

This commit is contained in:
2026-07-27 18:09:13 +08:00
parent 86cd1fa36d
commit 3411e54b13
9 changed files with 695 additions and 49 deletions
+61 -5
View File
@@ -7,6 +7,7 @@ from pathlib import Path
from skiff.paths import SKILLS_DIR, TEMPLATE_DIR, ensure_skills_home
from skiff.registry import external_skill_path, load_registry
from skiff.sources import list_source_skills, load_sources, source_skills_root
def list_owned_skills() -> list[str]:
@@ -31,25 +32,80 @@ def owned_skill_path(name: str) -> Path:
return path
def split_skill_spec(spec: str, source: str | None = None) -> tuple[str, str | None]:
if "/" not in spec:
return spec, source
qualified_source, name = spec.split("/", 1)
if not qualified_source or not name or "/" in name:
raise SystemExit(f"skill 限定名称无效: {spec!r}(应为 source/name")
if source and source != qualified_source:
raise SystemExit(
f"skill 来源冲突: {spec!r} 与 --source {source!r} 不一致"
)
return name, qualified_source
def list_custom_skills(source: str | None = None) -> dict[str, list[str]]:
sources = load_sources()
if source:
if source not in sources:
raise SystemExit(f"未配置 source: {source}")
return {source: list_source_skills(source, sources[source])}
return {name: list_source_skills(name, entry) for name, entry in sources.items()}
def resolve_skill_source(name: str, *, source: str | None = None) -> tuple[Path, str]:
"""返回 (skill_path, kind)kind 为 owned 或 external"""
"""返回 (skill_path, source),未指定来源时拒绝同名歧义"""
ensure_skills_home()
name, source = split_skill_spec(name, source)
owned = SKILLS_DIR / name
if source in (None, "owned") and (owned / "SKILL.md").is_file():
if source == "owned":
if not (owned / "SKILL.md").is_file():
raise SystemExit(f"owned source 中找不到 skill: {name}")
return owned, "owned"
registry = load_registry()
if source in (None, "registry") and name in registry:
if source == "registry":
if name not in registry:
raise SystemExit(f"registry 中找不到 skill: {name}")
path = external_skill_path(name, registry[name])
if not path.exists():
raise SystemExit(
f"外部 skill {name!r} 尚未 fetch。请先运行: skiff fetch {name}"
)
return path, "external"
return path, "registry"
sources = load_sources()
if source:
if source not in sources:
raise SystemExit(
f"项目依赖 source {source!r},但本机尚未配置。"
f"请运行: skiff source add {source} <repo>"
)
path = source_skills_root(source, sources[source]) / name
if not (path / "SKILL.md").is_file():
raise SystemExit(f"source {source!r} 中找不到 skill: {name}")
return path, source
candidates: list[tuple[Path, str]] = []
if (owned / "SKILL.md").is_file():
return owned, "owned"
candidates.append((owned, "owned"))
if name in registry:
candidates.append((external_skill_path(name, registry[name]), "registry"))
for source_name, entry in sources.items():
path = source_skills_root(source_name, entry) / name
if (path / "SKILL.md").is_file():
candidates.append((path, source_name))
if len(candidates) > 1:
choices = ", ".join(f"{candidate_source}/{name}" for _, candidate_source in candidates)
raise SystemExit(f"skill 名称存在多个来源,请明确指定: {choices}")
if candidates:
path, resolved_source = candidates[0]
if resolved_source == "registry" and not path.exists():
raise SystemExit(f"外部 skill {name!r} 尚未 fetch。请先运行: skiff fetch {name}")
return path, resolved_source
raise SystemExit(f"找不到 skill: {name}")