Files
kefu/wechat_rpa/updater_main.py
T
2026-09-21 10:34:06 +08:00

134 lines
4.5 KiB
Python

"""甄 AI 客服独立更新器;仅依赖 Python 标准库,打包为单文件 EXE。"""
from __future__ import annotations
import argparse
import ctypes
import hashlib
import json
import os
import subprocess
import time
from pathlib import Path
def _sha256(path: Path) -> str:
digest = hashlib.sha256()
with path.open("rb") as source:
for chunk in iter(lambda: source.read(1024 * 1024), b""):
digest.update(chunk)
return digest.hexdigest()
def _process_running(pid: int) -> bool:
if pid <= 0:
return False
if os.name == "nt":
process_query_limited_information = 0x1000
handle = ctypes.windll.kernel32.OpenProcess(
process_query_limited_information, False, pid
)
if not handle:
return False
ctypes.windll.kernel32.CloseHandle(handle)
return True
try:
os.kill(pid, 0)
return True
except OSError:
return False
def _write_result(path: Path, payload: dict) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
temporary = path.with_suffix(path.suffix + ".tmp")
temporary.write_text(json.dumps(payload, ensure_ascii=False, indent=2), encoding="utf-8")
os.replace(temporary, path)
def _wait_for_parent(pid: int, timeout: int = 120) -> None:
deadline = time.monotonic() + timeout
while _process_running(pid):
if time.monotonic() >= deadline:
raise RuntimeError("主程序在 120 秒内没有退出")
time.sleep(0.25)
def _start_available_version(install_dir: Path, preferred: Path | None = None) -> bool:
"""启动指定新版本;失败时回退到安装目录中最近的可执行旧版本。"""
candidates = []
if preferred is not None:
candidates.append(preferred)
try:
discovered = sorted(
install_dir.glob("ZhenAI-WeCom-Assistant-v*.exe"),
key=lambda item: item.stat().st_mtime,
reverse=True,
)
except OSError:
discovered = []
candidates.extend(item for item in discovered if item not in candidates)
for candidate in candidates:
if not candidate.is_file():
continue
try:
subprocess.Popen([str(candidate)], cwd=install_dir, close_fds=True)
return True
except OSError:
continue
return False
def main() -> int:
parser = argparse.ArgumentParser(description="甄 AI 客服独立更新器")
parser.add_argument("--installer", required=True)
parser.add_argument("--parent-pid", type=int, required=True)
parser.add_argument("--install-dir", required=True)
parser.add_argument("--target-version", required=True)
parser.add_argument("--result-file", required=True)
parser.add_argument("--expected-sha256", default="")
parser.add_argument("--from-version", default="")
args = parser.parse_args()
installer = Path(args.installer).resolve()
install_dir = Path(args.install_dir).resolve()
result_file = Path(args.result_file).resolve()
result = {
"target_version": args.target_version,
"from_version": args.from_version,
"status": "failed",
"error": "",
"updated_at": time.strftime("%Y-%m-%d %H:%M:%S"),
}
try:
_wait_for_parent(args.parent_pid)
if not installer.is_file():
raise RuntimeError("升级安装包不存在")
if args.expected_sha256 and _sha256(installer) != args.expected_sha256.lower():
raise RuntimeError("安装前 SHA256 复核失败")
completed = subprocess.run(
[str(installer), "/S"],
timeout=600,
creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0),
)
if completed.returncode != 0:
raise RuntimeError(f"静默安装失败,退出码 {completed.returncode}")
target = install_dir / f"ZhenAI-WeCom-Assistant-v{args.target_version}.exe"
if not target.is_file():
raise RuntimeError("安装完成但没有找到新版本主程序")
result["status"] = "installed"
_write_result(result_file, result)
if not _start_available_version(install_dir, target):
raise RuntimeError("安装成功,但无法启动新版本主程序")
return 0
except Exception as exc:
result["error"] = str(exc)[:1000]
_write_result(result_file, result)
_start_available_version(install_dir)
return 1
if __name__ == "__main__":
raise SystemExit(main())