Files
kefu/wechat_rpa/wechat_rpa.spec
T
2026-09-21 10:34:06 +08:00

173 lines
5.8 KiB
RPMSpec

# -*- mode: python ; coding: utf-8 -*-
import os
import sys
from pathlib import Path
from packaging_native import pin_ssl_binaries, python_ssl_binaries
from packaging_asr import asr_build_inputs
from PyInstaller.utils.hooks import collect_data_files, collect_submodules
from PyInstaller.utils.win32.versioninfo import (
FixedFileInfo,
StringFileInfo,
StringStruct,
StringTable,
VarFileInfo,
VarStruct,
VSVersionInfo,
)
project_root = Path(SPECPATH)
app_version = os.environ.get("WECOM_BUILD_VERSION", "1.0.0").strip() or "1.0.0"
app_name = f"ZhenAI-WeCom-Assistant-v{app_version}"
# 完整的版本信息资源。没有它,EXE 在杀软启发式评分和 SmartScreen 提示里
# 就是一个"来历不明"的匿名程序;有了它至少提示里能显示发布者与产品名。
_version_parts = [int(p) for p in (app_version.split(".") + ["0", "0", "0"])[:4]]
version_resource = VSVersionInfo(
ffi=FixedFileInfo(
filevers=tuple(_version_parts),
prodvers=tuple(_version_parts),
),
kids=[
StringFileInfo(
[
StringTable(
"080404b0",
[
StringStruct("CompanyName", "甄养堂"),
StringStruct("FileDescription", "甄AI客服 · 企业微信客服助手"),
StringStruct("FileVersion", app_version),
StringStruct("InternalName", app_name),
StringStruct("LegalCopyright", "Copyright (C) 2026 甄养堂"),
StringStruct("OriginalFilename", f"{app_name}.exe"),
StringStruct("ProductName", "甄AI客服"),
StringStruct("ProductVersion", app_version),
],
)
]
),
VarFileInfo([VarStruct("Translation", [2052, 1200])]),
],
)
hidden_imports = [
"app_version",
"archive_auto_backup",
"backend_client",
"ssl",
"_ssl",
"_hashlib",
"conversation_store",
"dsh_agent",
"gui_runtime",
"mcp_bridge",
"registration_store",
"runtime_paths",
"wechat_gui",
"wechat_gui_qt",
"wxwork_message_browser",
"wxwork_local_setup",
"wxwork_key",
"PySide6.QtWebEngineCore",
"PySide6.QtWebEngineWidgets",
"PySide6.QtWebChannel",
]
hidden_imports += collect_submodules("mcp")
asr_data, asr_binaries, asr_hidden = asr_build_inputs(project_root)
hidden_imports += asr_hidden
# Only ship exporter code, never local keys, account settings or chat databases.
exporter_root = project_root.parent / "2026-08-19-18-27-34"
exporter_files = [
"wxwork_export_final.py", "wxwork_export_media.py",
"wxwork_voice2text.py", "wxwork_crypto.py",
]
exporter_data = []
for filename in exporter_files:
source = exporter_root / filename
if not source.is_file():
raise FileNotFoundError(f"Required archive exporter is missing: {source}")
exporter_data.append((str(source), "archive_exporter"))
hidden_imports += ["Crypto.Cipher.AES"]
# Explicitly pin the SSL DLLs before and after dependency analysis.
_ssl_binaries = python_ssl_binaries()
a = Analysis(
[str(project_root / "app_main.py")],
pathex=[str(project_root)],
binaries=[(source, ".") for _, source, _ in _ssl_binaries] + asr_binaries,
datas=[
(str(project_root / "edge_light_theme"), "edge_light_theme"),
(str(project_root / "assets" / "brand"), "assets/brand"),
(str(project_root / "assets" / "fonts"), "assets/fonts"),
(str(project_root / "assets" / "ui"), "assets/ui"),
(str(project_root / "dsh_desk.cordis.yml"), "."),
]
# 昵称 OCR 的模型与配置:只收代码不收这些文件的话,打包后 RapidOCR
# 初始化直接失败,会话身份静默退化。
+ collect_data_files("rapidocr_onnxruntime") + exporter_data + asr_data,
hiddenimports=hidden_imports,
hookspath=[],
hooksconfig={},
runtime_hooks=[],
excludes=["PyQt5", "PyQt6", "PySide2"],
noarchive=False,
# 去掉 assert 与 __debug__ 分支;不动 docstring(有库在运行期读它)。
optimize=1,
)
# Codex 桌面工作区会把 PDF/图片工具自己的 native 目录临时放进 PATH。
# PyInstaller 会把 PATH 当作依赖搜索源,曾因此把 Poppler 的 ICU 78 和
# libheif 的 VC/UCRT 误装到应用根目录,覆盖 Qt 应使用的系统 ICU 与配套
# 运行库,最终让 QtCore 报“找不到指定的程序”。这些文件不是项目依赖,
# 构建时必须从收集结果中剔除;真实项目依赖仍来自 Python site-packages。
a.binaries = pin_ssl_binaries(a.binaries, _ssl_binaries)
_workspace_runtime_marker = "\\.cache\\codex-runtimes\\"
a.binaries = [
item
for item in a.binaries
if (
_workspace_runtime_marker not in str(item[1]).lower()
# A venv can itself use the bundled Python. Keep its interpreter DLL
# and stdlib extensions; only unrelated native tool runtimes are excluded.
or Path(item[1]).resolve().is_relative_to(Path(sys.base_prefix).resolve())
)
]
pyz = PYZ(a.pure)
# 目录形态(onedir):单文件 EXE 每次冷启动都要把几百 MB 的 Qt WebEngine
# 解压进临时目录再被杀毒软件逐个扫一遍,低配硬盘上一等就是几十秒。
# 目录形态零解压,冷启动只剩正常的模块加载。
exe = EXE(
pyz,
a.scripts,
[],
exclude_binaries=True,
name=app_name,
debug=False,
bootloader_ignore_signals=False,
strip=False,
upx=False,
console=False,
disable_windowed_traceback=False,
argv_emulation=False,
target_arch=None,
codesign_identity=None,
entitlements_file=None,
icon=str(project_root / "assets" / "brand" / "zhenyangtang-icon.ico"),
version=version_resource,
)
coll = COLLECT(
exe,
a.binaries,
a.datas,
strip=False,
upx=False,
name=app_name,
)