Files
playbook/cangjie-skill/scripts/apply_skill_patch.py
T
2026-08-31 05:00:46 +08:00

86 lines
3.2 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""apply_skill_patch.py — 最小补丁事务:快照 → 覆盖补丁文件 → 校验 → 可回滚(Phase 2B/3)。
repair/update 的落盘环节。补丁是"文件覆盖层"(patch 目录里的文件按相对路径覆盖目标),
不做自由重写;应用前自动快照,校验失败自动回滚。
用法:
python3 scripts/apply_skill_patch.py apply --target <skill-dir> --patch-dir <dir> --snapshots <dir>
python3 scripts/apply_skill_patch.py restore --target <skill-dir> --snapshot <snapshot-dir>
"""
from __future__ import annotations
import argparse
import shutil
import subprocess
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).parent))
from cangjie_common import WriterLock, snapshot_dir # noqa: E402
SCRIPTS = Path(__file__).parent
def apply_patch(target: Path, patch_dir: Path, snapshots: Path) -> int:
patch_files = [p for p in patch_dir.rglob("*") if p.is_file()]
if not patch_files:
raise SystemExit(f"补丁目录为空: {patch_dir}")
with WriterLock(target):
snap = snapshot_dir(target, snapshots, "pre-patch")
print(f"已快照: {snap}")
diff_lines = []
for pf in patch_files:
rel = pf.relative_to(patch_dir)
dest = target / rel
old = dest.read_text(encoding="utf-8") if dest.exists() else ""
new = pf.read_text(encoding="utf-8")
diff_lines.append(f"### {rel}: {'修改' if dest.exists() else '新增'}{len(old)}{len(new)} 字符)")
dest.parent.mkdir(parents=True, exist_ok=True)
shutil.copyfile(pf, dest)
check = subprocess.run([sys.executable, str(SCRIPTS / "validate_skill_pack.py"), str(target)],
capture_output=True, text=True)
if check.returncode != 0:
shutil.rmtree(target)
shutil.copytree(snap, target)
print(check.stdout + check.stderr)
raise SystemExit("[rollback] 补丁后校验失败,已自动恢复快照")
print("\n".join(diff_lines))
print(f"补丁已应用并通过校验({len(patch_files)} 个文件)。回滚: apply_skill_patch.py restore --snapshot {snap}")
return 0
def restore(target: Path, snapshot: Path) -> int:
if not snapshot.is_dir():
raise SystemExit(f"快照不存在: {snapshot}")
with WriterLock(target):
if target.exists():
shutil.rmtree(target)
shutil.copytree(snapshot, target)
print(f"已恢复 {target}{snapshot}")
return 0
def main() -> int:
ap = argparse.ArgumentParser(description=__doc__)
sub = ap.add_subparsers(dest="mode", required=True)
a = sub.add_parser("apply")
a.add_argument("--target", required=True)
a.add_argument("--patch-dir", required=True)
a.add_argument("--snapshots", required=True)
r = sub.add_parser("restore")
r.add_argument("--target", required=True)
r.add_argument("--snapshot", required=True)
args = ap.parse_args()
if args.mode == "apply":
return apply_patch(Path(args.target), Path(args.patch_dir), Path(args.snapshots))
return restore(Path(args.target), Path(args.snapshot))
if __name__ == "__main__":
raise SystemExit(main())