193 lines
6.0 KiB
Python
193 lines
6.0 KiB
Python
|
|
#!/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()
|