zip_and_verify.py
Rama main · réplica de lectura del repositorio original.
"""打 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()