build_exe.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320
  1. """
  2. 智能股票分析助手 — exe 打包脚本
  3. 用法:
  4. # 一键打包(从项目根目录执行)
  5. python scripts/build_exe.py
  6. # 仅检查环境不打包
  7. python scripts/build_exe.py --check
  8. # 强制重新 npm build(frontend/dist 已存在时默认跳过,可加速打包)
  9. python scripts/build_exe.py --rebuild-frontend
  10. # 或环境变量(PowerShell: $env:BUILD_EXE='1'; python scripts/build_exe.py)
  11. # 离线打包若不想拉取 tensorboard:BUILD_EXE_SKIP_TENSORBOARD=1(可能出现 torch 相关 WARNING,可忽略)
  12. 打包结果:
  13. dist_exe/stock_analyzer.exe # 主程序
  14. dist_exe/.env.example # 配置模板(需重命名为 .env 并填入 API Key)
  15. dist_exe/data/ # 数据目录(自动创建)
  16. """
  17. import os
  18. import sys
  19. import platform
  20. import shutil
  21. import subprocess
  22. from pathlib import Path
  23. # Windows 控制台默认 GBK,直接 print Unicode 勾选符号会触发 UnicodeEncodeError
  24. if hasattr(sys.stdout, "reconfigure"):
  25. try:
  26. sys.stdout.reconfigure(encoding="utf-8", errors="replace")
  27. sys.stderr.reconfigure(encoding="utf-8", errors="replace")
  28. except Exception:
  29. pass
  30. PROJECT_ROOT = Path(__file__).parent.parent
  31. FRONTEND_DIR = PROJECT_ROOT / "frontend"
  32. DIST_DIR = PROJECT_ROOT / "dist_exe"
  33. BACKEND_DIR = PROJECT_ROOT / "backend"
  34. # Windows 下 npm 实际是 npm.cmd
  35. _NPM_CMD = "npm.cmd" if platform.system() == "Windows" else "npm"
  36. def ensure_tensorboard_for_pyinstaller_scan() -> None:
  37. """PyInstaller 分析 PyTorch 时会执行 import torch.utils.tensorboard,依赖可选包 tensorboard。
  38. 未安装时仅打印 WARNING,不影响生成的 exe(本应用运行时不需要 TensorBoard)。
  39. 默认尝试 pip install tensorboard 以消除告警;离线打包可设环境变量 BUILD_EXE_SKIP_TENSORBOARD=1 跳过。
  40. """
  41. if os.getenv("BUILD_EXE_SKIP_TENSORBOARD", "").lower() in ("1", "true", "yes"):
  42. print(
  43. "[*] 已跳过 tensorboard 检查(BUILD_EXE_SKIP_TENSORBOARD);"
  44. "若出现 torch.utils.tensorboard 相关 WARNING 可忽略"
  45. )
  46. return
  47. try:
  48. import tensorboard # noqa: F401
  49. return
  50. except ImportError:
  51. pass
  52. print("[*] 正在安装 tensorboard(供 PyInstaller 分析 torch 时使用,消除可选模块告警)...")
  53. r = subprocess.run(
  54. [sys.executable, "-m", "pip", "install", "tensorboard"],
  55. cwd=str(PROJECT_ROOT),
  56. capture_output=True,
  57. text=True,
  58. )
  59. if r.returncode != 0:
  60. print(
  61. "[!] tensorboard 安装失败,打包仍会继续;"
  62. "可能出现 ModuleNotFoundError: tensorboard 类 WARNING,不影响本程序运行"
  63. )
  64. else:
  65. print("[OK] tensorboard 已就绪")
  66. def _force_rebuild_frontend() -> bool:
  67. """frontend/dist 已存在时,是否仍执行 npm run build"""
  68. if "--rebuild-frontend" in sys.argv:
  69. return True
  70. v = os.getenv("BUILD_EXE", "").lower()
  71. return v in ("1", "true", "yes", "rebuild")
  72. def check_env():
  73. """检查打包所需的工具是否可用"""
  74. issues = []
  75. # 检查 npm
  76. try:
  77. subprocess.run([_NPM_CMD, "--version"], capture_output=True, check=True)
  78. print("[OK] npm 可用")
  79. except (subprocess.CalledProcessError, FileNotFoundError):
  80. issues.append("npm 未安装或不在 PATH 中(需 Node.js)")
  81. # 检查 PyInstaller
  82. try:
  83. subprocess.run([sys.executable, "-m", "PyInstaller", "--version"],
  84. capture_output=True, check=True)
  85. print("[OK] PyInstaller 可用")
  86. except (subprocess.CalledProcessError, FileNotFoundError):
  87. issues.append("PyInstaller 未安装,请执行: pip install pyinstaller")
  88. # 检查前端是否已构建
  89. if not (FRONTEND_DIR / "dist" / "index.html").exists():
  90. issues.append("前端未构建,将自动构建")
  91. return issues
  92. def build_frontend():
  93. """构建 Vue3 前端为静态文件"""
  94. print("\n[1/3] 构建前端...")
  95. env = os.environ.copy()
  96. result = subprocess.run(
  97. [_NPM_CMD, "run", "build"],
  98. cwd=str(FRONTEND_DIR),
  99. env=env,
  100. capture_output=True,
  101. text=True,
  102. )
  103. if result.returncode != 0:
  104. print(f"[ERR] 前端构建失败:\n{result.stderr}")
  105. return False
  106. print(f"[OK] 前端构建完成 -> {FRONTEND_DIR / 'dist'}")
  107. return True
  108. def build_exe():
  109. """使用 PyInstaller 打包为 exe"""
  110. print("\n[2/3] PyInstaller 打包...")
  111. ensure_tensorboard_for_pyinstaller_scan()
  112. # 清理旧的构建产物
  113. for _d in [DIST_DIR, PROJECT_ROOT / "build"]:
  114. if _d.exists():
  115. shutil.rmtree(_d)
  116. # PyInstaller 参数
  117. pyi_args = [
  118. sys.executable, "-m", "PyInstaller",
  119. "--name", "stock_analyzer",
  120. "--onefile",
  121. "--console",
  122. "--clean",
  123. "--noconfirm",
  124. f"--distpath={DIST_DIR}",
  125. f"--workpath={PROJECT_ROOT / 'build' / 'pyinstaller'}",
  126. f"--specpath={PROJECT_ROOT / 'build'}",
  127. # 使 PyInstaller 分析阶段能解析 backend 下的 app.* 包(否则 hidden-import 报 not found)
  128. f"--paths={BACKEND_DIR}",
  129. # 入口
  130. str(PROJECT_ROOT / "run_exe.py"),
  131. # 添加数据目录
  132. "--add-data", f"{FRONTEND_DIR / 'dist'}{os.pathsep}frontend/dist",
  133. "--add-data", f"{PROJECT_ROOT / 'skills'}{os.pathsep}skills",
  134. "--add-data", f"{PROJECT_ROOT / 'agents'}{os.pathsep}agents",
  135. "--add-data", f"{PROJECT_ROOT / 'HelloAgents Optimized' / 'hello_agents'}{os.pathsep}hello_agents",
  136. "--add-data", f"{BACKEND_DIR}{os.pathsep}backend",
  137. # 隐藏导入(动态导入的模块)
  138. "--hidden-import", "app.api.market",
  139. "--hidden-import", "app.api.financial",
  140. "--hidden-import", "app.api.news",
  141. "--hidden-import", "app.api.screener",
  142. "--hidden-import", "app.api.watchlist",
  143. "--hidden-import", "app.api.simulation",
  144. "--hidden-import", "app.api.analysis",
  145. "--hidden-import", "app.api.buffett",
  146. "--hidden-import", "app.api.preferences",
  147. "--hidden-import", "app.services.market_service",
  148. "--hidden-import", "app.services.news_service",
  149. "--hidden-import", "app.services.screener_service",
  150. "--hidden-import", "app.services.analysis_service",
  151. "--hidden-import", "app.services.watchlist_service",
  152. "--hidden-import", "app.services.simulation_service",
  153. "--hidden-import", "app.services.buffett_service",
  154. "--hidden-import", "app.services.preference_service",
  155. "--hidden-import", "app.services.mx_timed_cache",
  156. "--hidden-import", "app.services.dashboard_warmup",
  157. "--hidden-import", "app.models.database",
  158. "--hidden-import", "app.models.preference",
  159. "--hidden-import", "app.models.report",
  160. "--hidden-import", "app.utils.response",
  161. "--hidden-import", "app.utils.mx_http",
  162. "--hidden-import", "app.utils.mx_quota",
  163. "--hidden-import", "app.utils.mx_fixture",
  164. "--hidden-import", "app.utils.mock_trading_normalize",
  165. "--hidden-import", "agents.agent_system",
  166. "--hidden-import", "agents.coordinator_agent",
  167. "--hidden-import", "agents.data_analysis_agent",
  168. "--hidden-import", "agents.sentiment_agent",
  169. "--hidden-import", "agents.advisor_agent",
  170. "--hidden-import", "agents.general_advisor_agent",
  171. "--hidden-import", "agents.tools.mx_data_tool",
  172. "--hidden-import", "agents.tools.mx_search_tool",
  173. # 常用依赖
  174. "--hidden-import", "fastapi",
  175. "--hidden-import", "uvicorn",
  176. "--hidden-import", "uvicorn.loops.auto",
  177. "--hidden-import", "uvicorn.protocols.http.auto",
  178. "--hidden-import", "pydantic",
  179. "--hidden-import", "sqlalchemy",
  180. "--hidden-import", "aiosqlite",
  181. "--hidden-import", "httpx",
  182. "--hidden-import", "pandas",
  183. "--hidden-import", "dotenv",
  184. "--collect-all", "openpyxl",
  185. ]
  186. result = subprocess.run(pyi_args, cwd=str(PROJECT_ROOT))
  187. if result.returncode != 0:
  188. print("[ERR] PyInstaller 打包失败")
  189. return False
  190. print(f"[OK] 打包完成 -> {DIST_DIR / 'stock_analyzer.exe'}")
  191. return True
  192. def copy_assets():
  193. """复制配置模板到输出目录"""
  194. print("\n[3/3] 复制配置文件...")
  195. # 复制 .env 模板
  196. env_template = PROJECT_ROOT / ".env"
  197. if env_template.exists():
  198. # 清理敏感信息
  199. import re
  200. content = env_template.read_text(encoding="utf-8")
  201. # 清除真实 API Key
  202. content = re.sub(r'(LLM_API_KEY=).+', r'\1your-deepseek-api-key-here', content)
  203. content = re.sub(r'(MX_APIKEY=).+', r'\1your-mx-apikey-here', content)
  204. content = re.sub(r'(JWT_SECRET_KEY=).+', r'\1change-this-to-a-random-secret-key', content)
  205. # 与 exe 默认端口、自动打开的浏览器地址一致(127.0.0.1:5174/dashboard)
  206. content = re.sub(r'^BACKEND_PORT=.*$', 'BACKEND_PORT=5174', content, flags=re.MULTILINE)
  207. (DIST_DIR / ".env.example").write_text(content, encoding="utf-8")
  208. print("[OK] .env.example 已生成(请重命名为 .env 并填入 API Key)")
  209. # 创建 data 目录
  210. (DIST_DIR / "data").mkdir(exist_ok=True)
  211. print("[OK] data/ 目录已创建")
  212. def main():
  213. print("=" * 50)
  214. print(" 智能股票分析助手 — exe 打包工具")
  215. print("=" * 50)
  216. # 切换到项目根目录
  217. os.chdir(str(PROJECT_ROOT))
  218. # 环境检查
  219. issues = check_env()
  220. if "--check" in sys.argv:
  221. if issues:
  222. print(f"\n[!] 发现 {len(issues)} 个问题:")
  223. for i in issues:
  224. print(f" - {i}")
  225. else:
  226. print("\n[OK] 打包环境就绪")
  227. return
  228. # 自动安装 PyInstaller
  229. for i in issues:
  230. if "PyInstaller" in i:
  231. print("[*] 正在安装 PyInstaller...")
  232. subprocess.run([sys.executable, "-m", "pip", "install", "pyinstaller"],
  233. check=True)
  234. issues.remove(i)
  235. break
  236. if issues:
  237. non_critical = [i for i in issues if "未构建" not in i]
  238. if non_critical:
  239. print(f"\n[ERR] 请先解决以下问题:")
  240. for i in non_critical:
  241. print(f" - {i}")
  242. return
  243. # 构建前端
  244. if not (FRONTEND_DIR / "dist" / "index.html").exists():
  245. if not build_frontend():
  246. return
  247. else:
  248. if _force_rebuild_frontend():
  249. if not build_frontend():
  250. return
  251. else:
  252. print("\n[1/3] 前端已有构建产物,跳过 npm build(加快打包)")
  253. print(" 若要强制重建前端:")
  254. print(" python scripts/build_exe.py --rebuild-frontend")
  255. if platform.system() == "Windows":
  256. print(" 或 PowerShell:$env:BUILD_EXE='1'; python scripts/build_exe.py")
  257. print(" 或 CMD: set BUILD_EXE=1 && python scripts/build_exe.py")
  258. else:
  259. print(" 或:BUILD_EXE=1 python scripts/build_exe.py")
  260. # PyInstaller 打包
  261. if not build_exe():
  262. return
  263. # 复制配置
  264. copy_assets()
  265. print("\n" + "=" * 50)
  266. print(" [OK] 打包完成!")
  267. print(f" 输出目录: {DIST_DIR}")
  268. print(f" 主程序: {DIST_DIR / 'stock_analyzer.exe'}")
  269. print(f"")
  270. print(f" 使用步骤:")
  271. print(f" 1. 将 {DIST_DIR.name}/ 目录拷贝到目标机器")
  272. print(f" 2. 将 .env.example 重命名为 .env")
  273. print(f" 3. 编辑 .env,填入 API Key")
  274. print(f" 4. 双击 stock_analyzer.exe 启动")
  275. print(f" 5. 浏览器将打开 http://127.0.0.1:5174/dashboard(未配置 BACKEND_PORT 时)")
  276. print("=" * 50)
  277. if __name__ == "__main__":
  278. main()