🔍 核心机关与修改指南
📦 白名单式纯净打包 只装核心文件
只精准写入 cfg/pkg.yaml、readme.md、src/*.py 和 media/*,目录里放再多临时文件或 .html 说明页都不会污染安装包!
💻 完整源码与配置内容(共 49 行 · 支持行号定位与一键复制)
| 1 | #!/usr/bin/env python3 |
| 2 | """pack.py —— 把技能目录打成 .rpk(普通 zip,固定结构) |
| 3 | |
| 4 | 用法: |
| 5 | python3 pack.py skill-voice-commander [输出目录] |
| 6 | |
| 7 | 产出 skill-xxx.rpk,内部结构: |
| 8 | cfg/pkg.yaml |
| 9 | src/*.py |
| 10 | readme.md |
| 11 | 另有 media/ 目录(可选)原样打包,用于放 mp4/mp3 资源。 |
| 12 | """ |
| 13 | import sys |
| 14 | import zipfile |
| 15 | from pathlib import Path |
| 16 | |
| 17 | |
| 18 | def pack(skill_dir: Path, out_dir: Path) -> Path: |
| 19 | assert (skill_dir / "cfg" / "pkg.yaml").exists(), f"{skill_dir} 缺 cfg/pkg.yaml" |
| 20 | py_files = list((skill_dir / "src").glob("*.py")) |
| 21 | assert py_files, f"{skill_dir} 缺 src/*.py" |
| 22 | |
| 23 | out = out_dir / f"{skill_dir.name}.rpk" |
| 24 | with zipfile.ZipFile(out, "w", zipfile.ZIP_DEFLATED) as z: |
| 25 | z.write(skill_dir / "cfg" / "pkg.yaml", "cfg/pkg.yaml") |
| 26 | readme = skill_dir / "readme.md" |
| 27 | if readme.exists(): |
| 28 | z.write(readme, "readme.md") |
| 29 | for f in py_files: |
| 30 | z.write(f, f"src/{f.name}") |
| 31 | # 可选媒体资源目录 |
| 32 | media = skill_dir / "media" |
| 33 | if media.exists(): |
| 34 | for f in media.rglob("*"): |
| 35 | if f.is_file(): |
| 36 | z.write(f, f"media/{f.relative_to(media)}") |
| 37 | # 校验 |
| 38 | with zipfile.ZipFile(out) as z: |
| 39 | names = z.namelist() |
| 40 | assert "cfg/pkg.yaml" in names |
| 41 | assert any(n.startswith("src/") and n.endswith(".py") for n in names) |
| 42 | print(f"OK {out} ({out.stat().st_size} B, {len(names)} files)") |
| 43 | return out |
| 44 | |
| 45 | |
| 46 | if __name__ == "__main__": |
| 47 | skill = Path(sys.argv[1] if len(sys.argv) > 1 else "skill-voice-commander") |
| 48 | out_dir = Path(sys.argv[2]) if len(sys.argv) > 2 else Path(".") |
| 49 | pack(skill, out_dir) |