feat: 1.0.0
This commit is contained in:
@@ -0,0 +1,114 @@
|
||||
"""检查仓库里的每个技能(仅用标准库)。
|
||||
|
||||
用法:
|
||||
python scripts/check_skills.py # 检查所有顶层技能目录(含 SKILL.md 的目录)
|
||||
python scripts/check_skills.py bugfix # 只检查指定技能
|
||||
|
||||
检查项: frontmatter 存在; name 与目录名一致且符合命名规则; description 非空且 <= 1024 字符;
|
||||
所有 .md 中的相对链接可解析; SKILL.md 正文 < 500 行、约 < 5000 tokens(超出只警告)。
|
||||
完整的规范校验请再运行官方校验器: agentskills validate <技能目录>(pip install skills-ref)。
|
||||
"""
|
||||
import pathlib
|
||||
import re
|
||||
import sys
|
||||
|
||||
ROOT = pathlib.Path(__file__).resolve().parent.parent
|
||||
NAME_RE = re.compile(r"^[a-z0-9]+(-[a-z0-9]+)*$")
|
||||
LINK_RE = re.compile(r"\]\(([^)#\s]+)(?:#[^)]*)?\)")
|
||||
|
||||
|
||||
def frontmatter(text):
|
||||
if not text.startswith("---"):
|
||||
return None, text
|
||||
parts = text.split("---", 2)
|
||||
if len(parts) < 3:
|
||||
return None, text
|
||||
return parts[1], parts[2]
|
||||
|
||||
|
||||
def scalar(block, key):
|
||||
"""读取顶层 key 的值,支持单行、引号、以及 >- / | 多行写法。"""
|
||||
lines = block.splitlines()
|
||||
for i, line in enumerate(lines):
|
||||
m = re.match(rf"^{key}:\s*(.*)$", line)
|
||||
if not m:
|
||||
continue
|
||||
value = m.group(1).strip()
|
||||
if value in (">", ">-", "|", "|-", ""):
|
||||
body = []
|
||||
for nxt in lines[i + 1:]:
|
||||
if nxt and not nxt.startswith((" ", "\t")):
|
||||
break
|
||||
body.append(nxt.strip())
|
||||
joiner = " " if value.startswith(">") or value == "" else "\n"
|
||||
return joiner.join(b for b in body if b).strip()
|
||||
return value.strip("'\"")
|
||||
return None
|
||||
|
||||
|
||||
def check(skill_dir):
|
||||
errors, warnings = [], []
|
||||
skill_md = skill_dir / "SKILL.md"
|
||||
text = skill_md.read_text(encoding="utf-8")
|
||||
fm, body = frontmatter(text)
|
||||
if fm is None:
|
||||
return ["SKILL.md 缺少 YAML frontmatter(--- ... ---)"], warnings
|
||||
|
||||
name = scalar(fm, "name")
|
||||
if name != skill_dir.name:
|
||||
errors.append(f"name '{name}' 必须与目录名 '{skill_dir.name}' 一致")
|
||||
if not name or not NAME_RE.match(name) or len(name) > 64:
|
||||
errors.append(f"name '{name}' 不合规:只能用小写字母/数字/单个连字符,且 <= 64 字符")
|
||||
desc = scalar(fm, "description")
|
||||
if not desc:
|
||||
errors.append("description 为空")
|
||||
elif len(desc) > 1024:
|
||||
errors.append(f"description 长度 {len(desc)} > 1024")
|
||||
|
||||
for md in sorted(skill_dir.rglob("*.md")):
|
||||
for target in LINK_RE.findall(md.read_text(encoding="utf-8")):
|
||||
if target.startswith(("http://", "https://", "mailto:")):
|
||||
continue
|
||||
if not (md.parent / target).resolve().exists():
|
||||
errors.append(f"断链: {md.relative_to(ROOT)} -> {target}")
|
||||
|
||||
body_lines = body.count("\n")
|
||||
body_tokens = len(body) // 4
|
||||
if body_lines >= 500:
|
||||
warnings.append(f"SKILL.md 正文 {body_lines} 行(建议 < 500,细节移到 references/)")
|
||||
if body_tokens >= 5000:
|
||||
warnings.append(f"SKILL.md 正文约 {body_tokens} tokens(建议 < 5000)")
|
||||
return errors, warnings + [f"正文 {body_lines} 行,约 {body_tokens} tokens"]
|
||||
|
||||
|
||||
def main(argv):
|
||||
# Windows 下被管道/重定向时 stdout 可能是 cp1252/GBK,中文会抛 UnicodeEncodeError
|
||||
for stream in (sys.stdout, sys.stderr):
|
||||
if hasattr(stream, "reconfigure"):
|
||||
stream.reconfigure(encoding="utf-8", errors="replace")
|
||||
if argv:
|
||||
dirs = [ROOT / a for a in argv]
|
||||
else:
|
||||
dirs = sorted(p for p in ROOT.iterdir() if p.is_dir() and (p / "SKILL.md").is_file())
|
||||
if not dirs:
|
||||
print("没有找到技能(顶层目录中没有 SKILL.md)")
|
||||
return 1
|
||||
failed = 0
|
||||
for d in dirs:
|
||||
if not (d / "SKILL.md").is_file():
|
||||
print(f"[失败] {d.name}: 没有 SKILL.md")
|
||||
failed += 1
|
||||
continue
|
||||
errors, notes = check(d)
|
||||
print(f"[{'失败' if errors else '通过'}] {d.name}")
|
||||
for e in errors:
|
||||
print(f" 错误: {e}")
|
||||
for n in notes:
|
||||
print(f" 提示: {n}")
|
||||
failed += bool(errors)
|
||||
print(f"\n共 {len(dirs)} 个技能,{failed} 个失败")
|
||||
return 1 if failed else 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main(sys.argv[1:]))
|
||||
Reference in New Issue
Block a user