refactor: unify skill source model
This commit is contained in:
+79
-50
@@ -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}")
|
||||
|
||||
Reference in New Issue
Block a user