# -*- coding: utf-8 -*- """一键打包脚本:PyInstaller 构建(不跑自检)→ 同步数据目录 → 复制 Chrome 登录态 → 生成 dist_vXX。 用法(在项目根目录执行): python scripts/package.py # 自动取下一个版本号 python scripts/package.py 71 # 指定版本号 """ import argparse import os import re import shutil import subprocess import sys ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) DIST = os.path.join(ROOT, "dist") # 优先使用项目 .venv 的 Python(含 PyInstaller),避免系统 python 无该模块 VENV_PY = os.path.join(ROOT, ".venv", "Scripts", "python.exe") PYTHON = VENV_PY if os.path.isfile(VENV_PY) else sys.executable # 从源码同步到 dist 的目录/文件 SYNC_ITEMS = [ "config.yaml", "configs", "prompts", "templates", "basemap", "material_library", "db", "output", "logs", ] # 登录态目录:打包时把当前 Chrome 登录态复制到 exe 同目录,供冻结后复用 CHROME_SESSION_SRC = os.path.join(ROOT, "pinterest_scraper", ".chrome_session") CHROME_SESSION_DST = os.path.join(DIST, "pinterest_scraper", ".chrome_session") def find_prev_dist() -> str | None: """找到上一个 dist_vXX(用于同步 .cache,避免新 exe 首次运行重新采集)。""" versions = [] for name in os.listdir(ROOT): m = re.fullmatch(r"dist_v(\d+)", name) if m and os.path.isdir(os.path.join(ROOT, name)): versions.append(int(m.group(1))) if not versions: return None return os.path.join(ROOT, f"dist_v{max(versions)}") def next_version() -> int: versions = [] for name in os.listdir(ROOT): m = re.fullmatch(r"dist_v(\d+)", name) if m and os.path.isdir(os.path.join(ROOT, name)): versions.append(int(m.group(1))) return (max(versions) + 1) if versions else 71 def sync_dir(src: str, dst: str) -> None: if not os.path.isdir(src): print(f"[sync] 跳过(源不存在): {src}") return os.makedirs(dst, exist_ok=True) for name in os.listdir(src): s = os.path.join(src, name) d = os.path.join(dst, name) if os.path.isdir(s): if os.path.isdir(d): shutil.rmtree(d) shutil.copytree(s, d) else: shutil.copy2(s, d) print(f"[sync] 目录同步完成: {src} -> {dst}") def sync_file(src: str, dst: str) -> None: if not os.path.isfile(src): print(f"[sync] 跳过(源不存在): {src}") return os.makedirs(os.path.dirname(dst), exist_ok=True) shutil.copy2(src, dst) print(f"[sync] 文件同步完成: {src} -> {dst}") def build_exe() -> None: print("===== 1/4 PyInstaller 构建(不跑自检) =====") if os.path.isdir(DIST): shutil.rmtree(DIST) os.makedirs(DIST, exist_ok=True) subprocess.run( [PYTHON, "-m", "PyInstaller", "--noconfirm", "--clean", "PODTrendAgent.spec"], cwd=ROOT, check=True, ) exe = os.path.join(DIST, "PODTrendAgent.exe") if not os.path.isfile(exe): raise SystemExit(f"[error] 构建失败:未找到 {exe}") print(f"[build] 构建完成: {exe} ({os.path.getsize(exe) / 1024 / 1024:.1f} MB)") def sync_data(prev_dist: str | None) -> None: print("===== 2/4 同步数据目录 =====") for item in SYNC_ITEMS: sync_file(os.path.join(ROOT, item), os.path.join(DIST, item)) \ if os.path.isfile(os.path.join(ROOT, item)) \ else sync_dir(os.path.join(ROOT, item), os.path.join(DIST, item)) # .cache 从上一个 dist_vXX 同步(避免新 exe 首次运行重新采集) if prev_dist: prev_cache = os.path.join(prev_dist, ".cache") if os.path.isdir(prev_cache): sync_dir(prev_cache, os.path.join(DIST, ".cache")) else: print(f"[sync] 上一个版本无 .cache,跳过: {prev_cache}") else: print("[sync] 未找到上一个 dist_vXX,跳过 .cache 同步") def copy_chrome_session() -> None: print("===== 3/4 复制当前 Chrome 登录态 =====") if os.path.isdir(CHROME_SESSION_SRC): sync_dir(CHROME_SESSION_SRC, CHROME_SESSION_DST) else: print(f"[warn] 未找到 Chrome 登录态目录,跳过: {CHROME_SESSION_SRC}") def rename_dist(version: int) -> str: print("===== 4/4 生成 dist_vXX =====") target = os.path.join(ROOT, f"dist_v{version}") if os.path.isdir(target): shutil.rmtree(target) shutil.move(DIST, target) print(f"[done] 发布目录: {target}") return target def main() -> None: parser = argparse.ArgumentParser(description="PODTrendAgent 一键打包") parser.add_argument("version", nargs="?", type=int, default=None, help="版本号(默认自动取下一个)") args = parser.parse_args() version = args.version or next_version() prev_dist = find_prev_dist() print(f"目标版本: dist_v{version},上一个版本: {os.path.basename(prev_dist) if prev_dist else '无'}") build_exe() sync_data(prev_dist) copy_chrome_session() target = rename_dist(version) print(f"\n打包完成:{target}") if __name__ == "__main__": main()