#!/usr/bin/env python3 """ check_update.py — 检查 / 更新 Holy Crab Skill 版本 用法: python3 check_update.py # 检查更新 python3 check_update.py --force # 强制更新到最新版本 python3 check_update.py --install # 从后端下载并安装 """ import hashlib import json import os import shutil import sys import zipfile from datetime import datetime from pathlib import Path SCRIPT_DIR = Path(__file__).resolve().parent SKILL_DIR = SCRIPT_DIR.parent VERSION_FILE = SKILL_DIR / "VERSION" BACKEND = os.environ.get("HC_BACKEND_URL", "http://localhost:8000").rstrip("/") VERSION_API = f"{BACKEND}/api/v1/skill/holy-crab/version" DOWNLOAD_API = f"{BACKEND}/api/v1/skill/holy-crab/download" def local_version() -> dict | None: if not VERSION_FILE.exists(): return None return json.loads(VERSION_FILE.read_text()) def compute_hash(path: Path) -> str: h = hashlib.sha256() with open(path, "rb") as f: for chunk in iter(lambda: f.read(65536), b""): h.update(chunk) return h.hexdigest()[:16] def update_version_file(remote: dict) -> None: # 更新本地 VERSION v = remote.copy() for fname, finfo in v.get("files", {}).items(): fpath = SKILL_DIR / fname if fpath.exists(): finfo["hash"] = compute_hash(fpath) VERSION_FILE.write_text(json.dumps(v, ensure_ascii=False, indent=2)) def check_update() -> tuple[bool, dict | None, dict | None]: """检查是否有更新。返回 (有更新, 本地版本, 远程版本)""" local = local_version() remote_v = None import urllib.request try: req = urllib.request.Request(VERSION_API, headers={"Accept": "application/json"}) with urllib.request.urlopen(req, timeout=10) as resp: raw = json.loads(resp.read().decode("utf-8")) # 后端返回格式:{"data": {"version": {...}}} remote_v = raw.get("data", {}).get("version") or raw except Exception: return False, local, None if not local: return True, None, remote_v def parse_ver(v: str) -> tuple: return tuple(int(x) for x in v.split(".")) local_ver = local.get("version", "0.0.0") remote_ver = remote_v.get("version", "0.0.0") has_update = parse_ver(remote_ver) > parse_ver(local_ver) return has_update, local, remote_v def download_and_install() -> None: import urllib.request print(" 📥 正在从后端下载...") try: req = urllib.request.Request(DOWNLOAD_API, headers={"Accept": "application/zip"}) with urllib.request.urlopen(req, timeout=60) as resp: zip_data = resp.read() except urllib.error.HTTPError as e: body = e.read().decode("utf-8", errors="replace") print(f" ❌ 下载失败 HTTP {e.code}: {body[:200]}") sys.exit(1) # 备份当前版本 backup_dir = SKILL_DIR.parent / f"holy-crab.backup.{datetime.now().strftime('%Y%m%d%H%M%S')}" shutil.copytree(SKILL_DIR, backup_dir) print(f" 📦 当前版本已备份至: {backup_dir}") # 解压到 skill 目录 import io with zipfile.ZipFile(io.BytesIO(zip_data), "r") as zf: members = [m for m in zf.namelist() if not m.endswith("/")] for member in members: # 去掉顶层目录名(skill 安装包通常带 holy-crab/v1.0.0/ 前缀) dst_name = member for prefix in [f"holy-crab-{VERSION_FILE.parent.name}/", "holy-crab/"]: if member.startswith(prefix): dst_name = member[len(prefix):] break if not dst_name or dst_name == member: # 直接从根目录解压的文件 dst_name = member.split("/", 1)[1] if "/" in member else member dst = SKILL_DIR / dst_name dst.parent.mkdir(parents=True, exist_ok=True) if dst_name: dst.write_bytes(zf.read(member)) print(" ✅ 安装完成!") def main() -> None: force = "--force" in sys.argv install = "--install" in sys.argv or "-u" in sys.argv print() print("═" * 55) print(" 🔍 Holy Crab Skill 版本检查") print("═" * 55) print() print(f" 后端地址: {BACKEND}") print(f" 本地路径: {SKILL_DIR}") print() local = local_version() if local: print(f" 本地版本: {local.get('version')} ({local.get('released', '未知日期')})") else: print(" 本地版本: 未安装") has_update, _, remote = check_update() if not remote: print() print(" ⚠️ 无法连接到后端获取远程版本信息") print(" 请确认:") print(" 1. 后端是否已启动") print(" 2. HC_BACKEND_URL 是否正确") print() sys.exit(1) print(f" 远程版本: {remote.get('version')} ({remote.get('released', '未知日期')})") if not has_update and not force: print() print(" ✅ 当前已是最新版本,无需更新") print() if local: print(" 文件状态:") for fname, finfo in local.get("files", {}).items(): fpath = SKILL_DIR / fname status = "✅" if fpath.exists() else "❌ 缺失" print(f" {status} {fname}") print() return print() print(f" 🆕 发现新版本: {remote.get('version')}") changelog = remote.get("changelog", "") if changelog: print(f" 更新内容: {changelog}") print() if install or force or "--yes" in sys.argv: confirm = True else: answer = input(" 是否立即更新?(y/n): ").strip().lower() confirm = answer in ("y", "yes", "是") if confirm: download_and_install() # 更新本地 VERSION 记录 if local_version(): update_version_file(remote) print() print(f" ✅ Holy Crab Skill 已更新到 {remote.get('version')}") else: print(" 已取消更新") print() print("═" * 55) if __name__ == "__main__": main()