feat(migration): 建立 Skill 迁移工作流与追踪工具
This commit is contained in:
@@ -0,0 +1,79 @@
|
||||
#!/usr/bin/env python3
|
||||
"""检查 CraftKit Skill 的基础结构、引用和敏感内容。"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
NAME_RE = re.compile(r"^[a-z0-9]+(?:-[a-z0-9]+)*$")
|
||||
LINK_RE = re.compile(r"\[[^]]+]\((?!https?://|#)([^)]+)\)")
|
||||
SECRET_RE = re.compile(
|
||||
r"(?i)(api[_-]?key|password|private[_-]?key|access[_-]?token)\s*[:=]\s*[^\s]+"
|
||||
)
|
||||
|
||||
|
||||
def frontmatter(text: str) -> dict[str, str]:
|
||||
"""解析当前校验所需的简单顶层 frontmatter 字段。"""
|
||||
if not text.startswith("---\n"):
|
||||
return {}
|
||||
end = text.find("\n---", 4)
|
||||
if end < 0:
|
||||
return {}
|
||||
values: dict[str, str] = {}
|
||||
for line in text[4:end].splitlines():
|
||||
if ":" in line and not line.startswith((" ", "\t")):
|
||||
key, value = line.split(":", 1)
|
||||
values[key.strip()] = value.strip().strip("'\"")
|
||||
return values
|
||||
|
||||
|
||||
def check_skill(root: Path, forbidden: list[str]) -> dict:
|
||||
"""返回结构化问题列表,不修改被检查目录。"""
|
||||
issues: list[dict[str, str]] = []
|
||||
skill_md = root / "SKILL.md"
|
||||
if not skill_md.is_file():
|
||||
return {"skill": root.name, "issues": [{"code": "missing-skill-md", "path": "SKILL.md"}]}
|
||||
text = skill_md.read_text(encoding="utf-8-sig")
|
||||
metadata = frontmatter(text)
|
||||
name = metadata.get("name", "")
|
||||
if not NAME_RE.fullmatch(name):
|
||||
issues.append({"code": "invalid-name", "path": "SKILL.md"})
|
||||
if name != root.name:
|
||||
issues.append({"code": "name-path-mismatch", "path": "SKILL.md"})
|
||||
if not metadata.get("description"):
|
||||
issues.append({"code": "missing-description", "path": "SKILL.md"})
|
||||
|
||||
for path in sorted(item for item in root.rglob("*") if item.is_file()):
|
||||
relative = path.relative_to(root).as_posix()
|
||||
content = path.read_text(encoding="utf-8-sig", errors="replace")
|
||||
if "[TODO:" in content:
|
||||
issues.append({"code": "todo-placeholder", "path": relative})
|
||||
if SECRET_RE.search(content):
|
||||
issues.append({"code": "possible-secret", "path": relative})
|
||||
lowered = content.casefold()
|
||||
for term in forbidden:
|
||||
if term.casefold() in lowered:
|
||||
issues.append({"code": f"forbidden:{term}", "path": relative})
|
||||
if path.suffix.lower() == ".md":
|
||||
for link in LINK_RE.findall(content):
|
||||
target = link.split("#", 1)[0]
|
||||
if target and not (path.parent / target).resolve().is_file():
|
||||
issues.append({"code": "broken-link", "path": f"{relative}:{link}"})
|
||||
return {"skill": root.name, "issues": issues}
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(description="检查 CraftKit Skill")
|
||||
parser.add_argument("skill", nargs="+", type=Path)
|
||||
parser.add_argument("--forbid", action="append", default=[])
|
||||
args = parser.parse_args()
|
||||
results = [check_skill(path.resolve(), args.forbid) for path in args.skill]
|
||||
print(json.dumps({"results": results}, ensure_ascii=False, indent=2))
|
||||
raise SystemExit(1 if any(item["issues"] for item in results) else 0)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user