"""打 zip + 解压实跑验证(按 workbuddy-pyinstaller-packaging skill 步骤 6-9). 流程: 1. 用 Python zipfile 打 zip(内容平铺,与历史包结构一致) 2. 解压到干净临时目录 3. 启动解压版的 exe,探 /chat 和 /api/version/check 4. 报告结果 """ import hashlib import os import shutil import socket import subprocess import sys import tempfile import time import urllib.request import zipfile ROOT = r"D:\gongniu_project\scout-agent" DIST_DESKTOP = os.path.join(ROOT, "dist", "ScoutDesktop") ZIP_PATH = os.path.join(ROOT, "dist", "scout-agent-1.0.0.6-win-x64.zip") VERSION = "1.0.0.6" def free_port(): s = socket.socket() s.bind(("127.0.0.1", 0)) p = s.getsockname()[1] s.close() return p def make_zip(): """打 zip — 内容平铺(顶层直接 ScoutAgent.exe + _internal/,不含 ScoutDesktop/ 这层).""" print(f"=== 1. making zip: {ZIP_PATH} ===") if os.path.exists(ZIP_PATH): # 备份旧 zip bak = ZIP_PATH + ".prev" if os.path.exists(bak): os.remove(bak) os.rename(ZIP_PATH, bak) print(f"[ok] backed up old zip -> {os.path.basename(bak)}") count = 0 total_size = 0 with zipfile.ZipFile(ZIP_PATH, "w", zipfile.ZIP_DEFLATED, compresslevel=6) as zf: for dirpath, _, files in os.walk(DIST_DESKTOP): for fn in files: full = os.path.join(dirpath, fn) # relpath 相对 DIST_DESKTOP,转正斜杠 — 内容平铺 arc = os.path.relpath(full, DIST_DESKTOP).replace("\\", "/") zf.write(full, arc) count += 1 total_size += os.path.getsize(full) zsize = os.path.getsize(ZIP_PATH) print(f"[ok] zip created: {count} files, raw={total_size/1e6:.1f}MB, zip={zsize/1e6:.1f}MB") return zsize def extract_and_verify(): """解压到干净临时目录并实跑验证.""" print(f"\n=== 2. extracting to clean temp dir & verify ===") tmp = tempfile.mkdtemp(prefix="scout_upgrade_") print(f"[info] extract dir: {tmp}") try: with zipfile.ZipFile(ZIP_PATH) as zf: zf.extractall(tmp) # 校验 1: 顶层直接是 ScoutAgent.exe + _internal(不多套一层目录) top = os.listdir(tmp) has_exe = "ScoutAgent.exe" in top has_internal = "_internal" in top has_extra_dir = any( os.path.isdir(os.path.join(tmp, d)) and d not in ("_internal",) for d in top ) print(f"[{'ok' if has_exe else 'FAIL'}] top-level ScoutAgent.exe: {has_exe}") print(f"[{'ok' if has_internal else 'FAIL'}] top-level _internal/: {has_internal}") if has_extra_dir: print(f"[warn] unexpected top-level dirs: {[d for d in top if d not in ('ScoutAgent.exe','_internal')]}") if not has_exe: return False # 校验 2: 启动解压版 exe exe = os.path.join(tmp, "ScoutAgent.exe") port = free_port() print(f"\n[info] launching extracted exe on port {port}...") proc = subprocess.Popen( [exe, "--no-gui", "--host", "127.0.0.1", "--port", str(port)], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, cwd=tmp, ) ok = False try: for _ in range(20): time.sleep(1) try: with urllib.request.urlopen( f"http://127.0.0.1:{port}/chat", timeout=2 ) as r: if r.status == 200: print(f"[ok] /chat returned 200") ok = True break except Exception: continue if not ok: print("[FAIL] extracted exe did not respond on /chat within 20s") # 抓 stdout 看错误 proc.terminate() try: out, _ = proc.communicate(timeout=3) if out: print("[diag] exe stdout/stderr:") print(out.decode("utf-8", errors="replace")[:1500]) except Exception: pass return False # 校验 3: 版本号 import json with urllib.request.urlopen( f"http://127.0.0.1:{port}/api/version/check", timeout=3 ) as r: data = json.loads(r.read().decode()) v = data.get("current_version") or data.get("version") match = v == VERSION print(f"[{'ok' if match else 'FAIL'}] /api/version/check current_version = {v} (expect {VERSION})") if not match: return False finally: proc.terminate() try: proc.wait(timeout=5) except subprocess.TimeoutExpired: proc.kill() print("\n=== zip extract verification PASSED ===") return True finally: # 清理临时目录(按 skill 坑 7:rmtree 可能被 SIGTERM 打断,再跑一次收尾) shutil.rmtree(tmp, ignore_errors=True) if os.path.exists(tmp): shutil.rmtree(tmp, ignore_errors=True) def compare_with_prev(): """对比新 zip 与上一版体积(按 skill 坑 5).""" print(f"\n=== 3. size comparison ===") prev = ZIP_PATH + ".prev" if not os.path.exists(prev): print("[skip] no previous zip to compare") return prev_sz = os.path.getsize(prev) new_sz = os.path.getsize(ZIP_PATH) diff = new_sz - prev_sz print(f"[info] previous: {prev_sz/1e6:.1f}MB") print(f"[info] new: {new_sz/1e6:.1f}MB") print(f"[info] diff: {diff/1e6:+.1f}MB ({diff/prev_sz*100:+.1f}%)") if abs(diff) > 10 * 1e6: # 对比 namelist with zipfile.ZipFile(prev) as zp, zipfile.ZipFile(ZIP_PATH) as zn: pp = set(zp.namelist()) nn = set(zn.namelist()) only_prev = pp - nn only_new = nn - pp if only_prev: print(f"[warn] files only in previous ({len(only_prev)}):") for f in sorted(only_prev)[:5]: print(f" - {f}") if only_new: print(f"[info] files only in new ({len(only_new)}):") for f in sorted(only_new)[:5]: print(f" + {f}") if not only_prev and not only_new: print("[ok] same file list, size diff from compression/content changes") else: print("[ok] size within ±10MB of previous") def main(): zsize = make_zip() ok = extract_and_verify() compare_with_prev() print(f"\n=== final: {'PASS' if ok else 'FAIL'} | zip={zsize/1e6:.1f}MB ===") sys.exit(0 if ok else 1) if __name__ == "__main__": main()