Files
holy-python/skills/holy-crab/scripts/check_update.py
2026-08-04 14:02:45 +08:00

193 lines
6.0 KiB
Python
Executable File
Raw Permalink 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
"""
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()