refactor: unify skill source model

This commit is contained in:
2026-07-30 12:11:25 +08:00
parent 076d87e303
commit 7d1994cf93
18 changed files with 669 additions and 527 deletions
+79 -50
View File
@@ -6,15 +6,19 @@ import re
from pathlib import Path
from skiff.paths import SKILLS_DIR, TEMPLATE_DIR, ensure_skills_home
from skiff.registry import (
external_collection_skill_path,
external_skill_path,
load_registry,
from skiff.catalog import (
catalog_skill_path,
discover_catalog_skills,
load_catalog,
)
from skiff.sources import (
discover_source_skills,
list_source_skills,
load_sources,
)
from skiff.sources import list_source_skills, load_sources, source_skills_root
def list_owned_skills() -> list[str]:
def list_builtin_skills() -> list[str]:
ensure_skills_home()
if not SKILLS_DIR.is_dir():
return []
@@ -29,19 +33,31 @@ def list_owned_skills() -> list[str]:
return names
def owned_skill_path(name: str) -> Path:
def builtin_skill_path(name: str) -> Path:
path = SKILLS_DIR / name
if not (path / "SKILL.md").is_file():
raise SystemExit(f"自研 skill 不存在: {name}")
raise SystemExit(f"builtin skill 不存在: {name}")
return path
def normalize_source(source: str | None) -> str | None:
if source == "owned":
return "builtin"
if source == "registry":
return "catalog"
if source and source.startswith("registry:"):
return f"catalog:{source.split(':', 1)[1]}"
return source
def split_skill_spec(spec: str, source: str | None = None) -> tuple[str, str | None]:
if "/" not in spec:
return spec, source
return spec, normalize_source(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")
qualified_source = normalize_source(qualified_source)
source = normalize_source(source)
if source and source != qualified_source:
raise SystemExit(
f"skill 来源冲突: {spec!r} 与 --source {source!r} 不一致"
@@ -63,42 +79,36 @@ def resolve_skill_source(name: str, *, source: str | None = None) -> tuple[Path,
ensure_skills_home()
name, source = split_skill_spec(name, source)
owned = SKILLS_DIR / name
if source == "owned":
if not (owned / "SKILL.md").is_file():
raise SystemExit(f"owned source 中找不到 skill: {name}")
return owned, "owned"
builtin = SKILLS_DIR / name
if source == "builtin":
if not (builtin / "SKILL.md").is_file():
raise SystemExit(f"builtin source 中找不到 skill: {name}")
return builtin, "builtin"
registry = load_registry()
catalog = load_catalog()
sources = load_sources()
registry_collection = None
if source and source.startswith("registry:"):
registry_collection = source.split(":", 1)[1]
elif (
source
and source not in ("owned", "registry")
and source in registry
and source not in sources
):
registry_collection = source
if registry_collection:
path = external_collection_skill_path(
registry_collection,
name,
registry[registry_collection],
)
return path, f"registry:{registry_collection}"
if source and source.startswith("catalog:"):
provider = source.split(":", 1)[1]
if provider not in catalog:
raise SystemExit(f"catalog 中找不到 source: {provider}")
skills = discover_catalog_skills(provider, catalog[provider])
if name not in skills:
available = ", ".join(skills) or "(无)"
raise SystemExit(
f"catalog source {provider!r} 中找不到 skill {name!r}。可用: {available}"
)
return skills[name], f"catalog:{provider}"
if source == "registry":
if name not in registry:
raise SystemExit(f"registry 中找不到 skill: {name}")
path = external_skill_path(name, registry[name])
if source == "catalog":
if name not in catalog:
raise SystemExit(f"catalog 中找不到 skill source: {name}")
path = catalog_skill_path(name, catalog[name])
if not (path / "SKILL.md").is_file():
raise SystemExit(
f"外部 skill {name!r} 尚未 fetch 或 path 中缺少 SKILL.md。"
f"catalog skill {name!r} 尚未 fetch 或 path 中缺少 SKILL.md。"
f"请运行: skiff fetch {name}"
)
return path, "registry"
return path, f"catalog:{name}"
if source:
if source not in sources:
@@ -106,28 +116,47 @@ def resolve_skill_source(name: str, *, source: str | None = None) -> tuple[Path,
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():
skills = discover_source_skills(source, sources[source])
if name not in skills:
raise SystemExit(f"source {source!r} 中找不到 skill: {name}")
return path, source
return skills[name], source
candidates: list[tuple[Path, str]] = []
if (owned / "SKILL.md").is_file():
candidates.append((owned, "owned"))
if name in registry:
candidates.append((external_skill_path(name, registry[name]), "registry"))
if (builtin / "SKILL.md").is_file():
candidates.append((builtin, "builtin"))
if name in catalog:
path = catalog_skill_path(name, catalog[name])
if (path / "SKILL.md").is_file() or not path.exists():
candidates.append((path, f"catalog:{name}"))
for provider, entry in catalog.items():
if provider == name:
continue
skills = discover_catalog_skills(provider, entry)
if name in skills:
candidates.append((skills[name], f"catalog:{provider}"))
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))
skills = discover_source_skills(source_name, entry)
if name in skills:
candidates.append((skills[name], source_name))
unique_candidates: list[tuple[Path, str]] = []
seen_paths: set[Path] = set()
for path, candidate_source in candidates:
resolved = path.resolve()
if resolved in seen_paths:
continue
seen_paths.add(resolved)
unique_candidates.append((path, candidate_source))
candidates = unique_candidates
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}")
if resolved_source.startswith("catalog:") and not path.exists():
provider = resolved_source.split(":", 1)[1]
raise SystemExit(f"catalog source {provider!r} 尚未 fetch。请先运行: skiff fetch {provider}")
return path, resolved_source
raise SystemExit(f"找不到 skill: {name}")