Files
kefu/deploy/plaintext-chat-20260918/build_windows_cached_plaintext_chat.ps1
T
2026-09-21 10:34:06 +08:00

159 lines
6.8 KiB
PowerShell

param(
[string]$PythonPath = ""
)
$ErrorActionPreference = "Stop"
$projectRoot = [System.IO.Path]::GetFullPath('C:\wechat_rpa')
$buildEnvironment = Join-Path $projectRoot ".build-venv"
$buildPython = Join-Path $buildEnvironment "Scripts\python.exe"
$requirements = Join-Path $projectRoot "requirements.txt"
$spec = Join-Path $projectRoot "wechat_rpa.spec"
$versionLine = Select-String -LiteralPath (Join-Path $projectRoot "app_version.py") -Pattern '^APP_VERSION\s*=\s*"([^"]+)"$'
if (-not $versionLine) {
throw "Cannot read APP_VERSION from app_version.py"
}
$version = $versionLine.Matches[0].Groups[1].Value
$env:WECOM_BUILD_VERSION = $version
$env:UV_CACHE_DIR = Join-Path $projectRoot ".uv-cache"
function Test-BuildPython([string]$Candidate) {
if (-not $Candidate -or -not (Test-Path -LiteralPath $Candidate)) {
return $false
}
try {
& $Candidate -c "import ssl, _ssl, PyInstaller, PySide6, requests, numpy, PIL, pyautogui, pyperclip, win32gui, mcp, rapidocr_onnxruntime, Crypto.Cipher.AES" *> $null
return $LASTEXITCODE -eq 0
}
catch {
# Windows PowerShell 5 treats native stderr as a terminating error with
# ErrorActionPreference=Stop, even when redirected. Try the next runtime.
return $false
}
}
if ($PythonPath -and [IO.Path]::GetFullPath($PythonPath) -ne $buildPython) { throw 'Use the existing production .build-venv runtime only' }
if (-not (Test-BuildPython $buildPython)) { throw 'Existing build runtime is incomplete; no dependency installation or fallback is performed' }
& $buildPython -B -c 'import sys; assert sys.version_info[:2] == (3,12)'
if ($LASTEXITCODE -ne 0) { throw 'Python 3.12 required' }
$python = $buildPython
Write-Output ("Build Python: " + $python)
Push-Location $projectRoot
try {
& $python (Join-Path $projectRoot "packaging_exporter.py") --project-root $projectRoot
if ($LASTEXITCODE -ne 0) { throw "Archive exporter preflight failed; see the missing source file above" }
& $python -m PyInstaller --noconfirm $spec
if ($LASTEXITCODE -ne 0) { throw "PyInstaller build failed" }
}
finally {
Pop-Location
}
$appName = "ZhenAI-WeCom-Assistant-v$version"
$appDir = Join-Path $projectRoot "dist\$appName"
$exe = Join-Path $appDir "$appName.exe"
if (-not (Test-Path -LiteralPath $exe)) {
throw ("Build completed but EXE was not found: " + $exe)
}
$internalDir = Join-Path $appDir "_internal"
# Qt 与其他视觉库可能分别携带不同版本的 VC Runtime。应用根目录优先级
# 最高,因此明确放置与当前 PySide6/Qt 完全配套的一组,避免 PATH 中第三方
# 工具的 DLL 在打包分析时抢占同名文件。
$pysideRuntimeDir = (& $python -c "import pathlib, PySide6; print(pathlib.Path(PySide6.__file__).resolve().parent)" | Select-Object -Last 1).Trim()
foreach ($runtimeName in @(
"MSVCP140.dll",
"MSVCP140_1.dll",
"MSVCP140_2.dll",
"VCRUNTIME140.dll",
"VCRUNTIME140_1.dll"
)) {
$runtimeSource = Join-Path $pysideRuntimeDir $runtimeName
if (-not (Test-Path -LiteralPath $runtimeSource)) {
throw ("PySide6 runtime file is missing: " + $runtimeName)
}
Copy-Item -LiteralPath $runtimeSource -Destination (Join-Path $internalDir $runtimeName) -Force
}
$requiredRuntimeFiles = @(
"PySide6\QtWebEngineProcess.exe",
"PySide6\resources\qtwebengine_resources.pak",
"PySide6\resources\icudtl.dat",
"PySide6\translations\qtwebengine_locales\zh-CN.pak",
"MSVCP140.dll",
"MSVCP140_1.dll",
"MSVCP140_2.dll",
"VCRUNTIME140.dll",
"VCRUNTIME140_1.dll"
)
foreach ($requiredFile in $requiredRuntimeFiles) {
if (-not (Test-Path -LiteralPath (Join-Path $internalDir $requiredFile))) {
throw ("Required embedded runtime file is missing: " + $requiredFile)
}
}
Write-Output "Embedded browser and VC runtime files verified"
# 自动升级必须由安装目录之外的独立进程执行,否则 Windows 会阻止正在运行的
# 主程序覆盖自身。更新器只依赖标准库,单独打成 onefile,主程序启动它前会再
# 复制到 updates 目录,因此 NSIS 可以安全替换安装目录中的这一份。
$updaterDist = Join-Path $projectRoot "dist\updater-helper"
$updaterWork = Join-Path $projectRoot "build\updater-helper"
New-Item -ItemType Directory -Path $updaterWork -Force | Out-Null
& $python -m PyInstaller --noconfirm --onefile --name ZhenAIUpdater `
--distpath $updaterDist --workpath $updaterWork --specpath $updaterWork `
(Join-Path $projectRoot "updater_main.py")
if ($LASTEXITCODE -ne 0) {
throw "Updater EXE build failed"
}
$updaterExe = Join-Path $updaterDist "ZhenAIUpdater.exe"
if (-not (Test-Path -LiteralPath $updaterExe)) {
throw "Updater EXE was not found"
}
& (Join-Path $projectRoot "sign_artifact.ps1") -Path $updaterExe
if ($LASTEXITCODE -ne 0) { throw "Updater code signing step failed" }
Copy-Item -LiteralPath $updaterExe -Destination (Join-Path $appDir "ZhenAIUpdater.exe") -Force
Write-Output "Independent updater built and bundled"
# 有证书时给主程序签名(无证书自动跳过)。签名要在自检前完成,
# 保证自检跑的就是最终分发的那个文件。
& (Join-Path $projectRoot "sign_artifact.ps1") -Path $exe
if ($LASTEXITCODE -ne 0) { throw "Code signing step failed" }
# 模拟没有开发工具 PATH 的目标电脑,防止漏包 DLL 被构建机意外补齐。
$buildProcessPath = $env:PATH
try {
$env:PATH = (Join-Path $env:SystemRoot "System32") + ";" + $env:SystemRoot
$selfCheck = Start-Process -FilePath $exe -ArgumentList "--packaging-self-check" -WorkingDirectory $appDir -PassThru -WindowStyle Hidden
}
finally {
$env:PATH = $buildProcessPath
}
try {
if (-not $selfCheck.WaitForExit(90000)) {
$selfCheck.Kill()
throw "Packaged Qt WebEngine self-check timed out"
}
if ($selfCheck.ExitCode -ne 0) {
throw ("Packaged Qt WebEngine self-check failed with exit code " + $selfCheck.ExitCode)
}
}
finally {
if ($selfCheck -and -not $selfCheck.HasExited) {
$selfCheck.Kill()
}
}
Write-Output "Packaged Qt WebEngine self-check passed"
Write-Output "Packaged real HTTPS and certificate verification checks passed (clean PATH)"
Write-Output "Packaged automatic decryption and incremental cloud upload checks passed (synthetic data, mocked transport)"
Write-Output "Packaged Messages navigation and OCR checks passed (synthetic UI, mocked input)"
Write-Output "Packaged cross-machine capture, DPI and unread click checks passed (synthetic pixels, mocked input)"
Write-Output "Packaged self-reply feedback and send guards passed (synthetic archive, mocked input)"
Write-Output "Packaged background database detection and database-first reply checks passed (synthetic data, mocked input)"
$hash = (Get-FileHash -LiteralPath $exe -Algorithm SHA256).Hash
Write-Output ("App folder: " + $appDir)
Write-Output ("EXE: " + $exe)
Write-Output ("SHA256: " + $hash)