Ctrl K
ComunidadGuías…

zip_and_verify.py

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
"""打 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()