param( [switch]$SkipTests, [switch]$SkipFrontendBuild, [string]$WailsPath, [string]$MakensisPath ) $ErrorActionPreference = "Stop" $scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path $repoRoot = (Resolve-Path (Join-Path $scriptDir "..")).Path $binDir = Join-Path $repoRoot "build\bin" $installerDir = Join-Path $repoRoot "build\windows\installer" $runtimeDir = Join-Path $installerDir "runtime" function Resolve-RequiredTool { param( [Parameter(Mandatory = $true)][string]$Name, [string]$FallbackPath, [string]$InstallHint ) $command = Get-Command $Name -ErrorAction SilentlyContinue if ($command) { return $command.Source } if ($FallbackPath -and (Test-Path -LiteralPath $FallbackPath)) { return (Resolve-Path -LiteralPath $FallbackPath).Path } throw "Required tool not found: $Name. $InstallHint" } function Assert-UnderDirectory { param( [Parameter(Mandatory = $true)][string]$Path, [Parameter(Mandatory = $true)][string]$Parent ) $parentPath = (Resolve-Path -LiteralPath $Parent).Path.TrimEnd('\') if (Test-Path -LiteralPath $Path) { $targetPath = (Resolve-Path -LiteralPath $Path).Path } else { $targetPath = [System.IO.Path]::GetFullPath($Path) } if (-not $targetPath.StartsWith($parentPath, [System.StringComparison]::OrdinalIgnoreCase)) { throw "Refusing to operate on $targetPath because it is not under $parentPath" } } function Reset-Directory { param( [Parameter(Mandatory = $true)][string]$Path, [Parameter(Mandatory = $true)][string]$Parent ) Assert-UnderDirectory -Path $Path -Parent $Parent if (Test-Path -LiteralPath $Path) { Remove-Item -LiteralPath $Path -Recurse -Force } New-Item -ItemType Directory -Force -Path $Path | Out-Null } function Write-Utf8NoBom { param( [Parameter(Mandatory = $true)][string]$Path, [Parameter(Mandatory = $true)][string]$Content ) $encoding = New-Object System.Text.UTF8Encoding($false) [System.IO.File]::WriteAllText($Path, $Content, $encoding) } function Copy-RequiredFile { param( [Parameter(Mandatory = $true)][string]$Source, [Parameter(Mandatory = $true)][string]$Destination ) if (-not (Test-Path -LiteralPath $Source)) { throw "Missing release resource: $Source" } $destinationDir = Split-Path -Parent $Destination if ($destinationDir -and -not (Test-Path -LiteralPath $destinationDir)) { New-Item -ItemType Directory -Force -Path $destinationDir | Out-Null } try { Copy-Item -LiteralPath $Source -Destination $Destination -Force -ErrorAction Stop } catch { if (Test-Path -LiteralPath $Destination) { $sourceInfo = Get-Item -LiteralPath $Source $destinationInfo = Get-Item -LiteralPath $Destination if ($sourceInfo.Length -eq $destinationInfo.Length) { Write-Warning "Could not overwrite ${Destination}; existing file has the same size and will be kept. $($_.Exception.Message)" return } } throw } } function Stop-RunningReleaseProcesses { $processes = Get-Process -Name "qiweimanager", "helper", "helper_auto_reply" -ErrorAction SilentlyContinue foreach ($process in $processes) { Write-Host "==> Stopping running release process: $($process.ProcessName) ($($process.Id))" Stop-Process -Id $process.Id -Force -ErrorAction Stop } } Set-Location $repoRoot New-Item -ItemType Directory -Force -Path (Join-Path $repoRoot ".gocache") | Out-Null $env:GOCACHE = (Resolve-Path (Join-Path $repoRoot ".gocache")).Path $wailsFallback = if ($WailsPath) { $WailsPath } else { Join-Path $env:USERPROFILE "go\bin\wails.exe" } $wails = Resolve-RequiredTool -Name "wails.exe" -FallbackPath $wailsFallback -InstallHint "Install Wails CLI on the build machine first." # 自动探测 NSIS 标准安装路径(未在 PATH 时也能找到),优先使用显式传入的 -MakensisPath $makensisFallback = $null $makensisCandidates = @() if ($MakensisPath) { $makensisCandidates += $MakensisPath } $makensisCandidates += @( (Join-Path ${env:ProgramFiles(x86)} "NSIS\makensis.exe"), (Join-Path $env:ProgramFiles "NSIS\makensis.exe"), (Join-Path $env:LOCALAPPDATA "Programs\NSIS\makensis.exe") ) foreach ($candidate in $makensisCandidates) { if ($candidate -and (Test-Path -LiteralPath $candidate)) { $makensisFallback = $candidate; break } } $makensis = Resolve-RequiredTool -Name "makensis.exe" -FallbackPath $makensisFallback -InstallHint "Install NSIS on the build machine first, e.g. run as Administrator: choco install nsis -y; or pass -MakensisPath." $npm = Resolve-RequiredTool -Name "npm.cmd" -InstallHint "Install Node.js on the build machine first." $go = Resolve-RequiredTool -Name "go.exe" -InstallHint "Install Go on the build machine first." $pdftoppm = Get-Command "pdftoppm.exe" -ErrorAction SilentlyContinue $env:PATH = "$(Split-Path -Parent $makensis);$env:PATH" Write-Host "==> Release root: $repoRoot" Write-Host "==> Wails: $wails" Write-Host "==> NSIS: $makensis" if (-not $SkipTests) { Write-Host "==> Running Go tests" & $go test ./... } New-Item -ItemType Directory -Force -Path $binDir | Out-Null Stop-RunningReleaseProcesses Write-Host "==> Building 32-bit helper.exe" $helperOut = Join-Path $binDir "helper.exe" $oldGoArch = $env:GOARCH try { $env:GOARCH = "386" Push-Location (Join-Path $repoRoot "helper") & $go build -trimpath -ldflags "-H windowsgui -s -w" -o $helperOut . } finally { Pop-Location if ($null -eq $oldGoArch) { Remove-Item Env:GOARCH -ErrorAction SilentlyContinue } else { $env:GOARCH = $oldGoArch } } Write-Host "==> Building bundled silk decoder" $silkDecoderOut = Join-Path $binDir "tools\audio\silkdecode.exe" $silkBuilt = $false if (-not (Get-Command gcc -ErrorAction SilentlyContinue)) { Write-Warning "gcc not found; silkdecode requires cgo and will be skipped. Voice message transcoding will be unavailable." } else { New-Item -ItemType Directory -Force -Path (Split-Path $silkDecoderOut) | Out-Null Push-Location (Join-Path $repoRoot "tools\audio\silkdecode") try { $oldCgo = $env:CGO_ENABLED; $env:CGO_ENABLED = "1" & $go build -trimpath -ldflags "-s -w" -o $silkDecoderOut . if ($LASTEXITCODE -eq 0) { $silkBuilt = $true } else { Write-Warning "silkdecode build failed, skipping." } } finally { Pop-Location if ($null -eq $oldCgo) { Remove-Item Env:CGO_ENABLED -ErrorAction SilentlyContinue } else { $env:CGO_ENABLED = $oldCgo } } } if ($pdftoppm) { Write-Host "==> Copying PDF renderer" Copy-RequiredFile -Source $pdftoppm.Source -Destination (Join-Path $binDir "tools\pdf\pdftoppm.exe") } else { Write-Warning "pdftoppm.exe not found; scanned PDF OCR fallback will be unavailable in this build." } Write-Host "==> Copying DLLs to build\bin" $helperDll = Join-Path $repoRoot "Helper_4.1.33.6009.dll" $loaderDll = Join-Path $repoRoot "Loader_4.1.33.6009.dll" Copy-RequiredFile -Source $helperDll -Destination (Join-Path $binDir "Helper_4.1.33.6009.dll") Copy-RequiredFile -Source $loaderDll -Destination (Join-Path $binDir "Loader_4.1.33.6009.dll") foreach ($staleFile in @("helper_auto_reply.exe", "qiweimanager.exe~")) { $path = Join-Path $binDir $staleFile if (Test-Path -LiteralPath $path) { try { Remove-Item -LiteralPath $path -Force -ErrorAction Stop } catch { Write-Warning "Could not remove stale file ${path}: $($_.Exception.Message)" } } } if (-not $SkipFrontendBuild) { Write-Host "==> Building frontend" Push-Location (Join-Path $repoRoot "frontend") try { # 原生命令往 stderr 写警告会在 Stop 策略下被误判为失败,这里改按退出码判断 $oldEap = $ErrorActionPreference; $ErrorActionPreference = "Continue" & $npm run build 2>&1 | ForEach-Object { "$_" } $ErrorActionPreference = $oldEap if ($LASTEXITCODE -ne 0) { throw "frontend build failed (exit $LASTEXITCODE)" } } finally { Pop-Location } } Write-Host "==> Staging NSIS runtime resources" Reset-Directory -Path $runtimeDir -Parent $installerDir New-Item -ItemType Directory -Force -Path (Join-Path $runtimeDir "config\knowledge") | Out-Null New-Item -ItemType Directory -Force -Path (Join-Path $runtimeDir "config\materials") | Out-Null Copy-RequiredFile -Source $helperOut -Destination (Join-Path $runtimeDir "helper.exe") if ($silkBuilt -and (Test-Path -LiteralPath $silkDecoderOut)) { Copy-RequiredFile -Source $silkDecoderOut -Destination (Join-Path $runtimeDir "tools\audio\silkdecode.exe") } else { Write-Warning "silkdecode.exe not bundled (gcc unavailable); voice transcoding disabled in this installer." } if (Test-Path -LiteralPath (Join-Path $binDir "tools\pdf\pdftoppm.exe")) { Copy-RequiredFile -Source (Join-Path $binDir "tools\pdf\pdftoppm.exe") -Destination (Join-Path $runtimeDir "tools\pdf\pdftoppm.exe") } Copy-RequiredFile -Source (Join-Path $binDir "Helper_4.1.33.6009.dll") -Destination (Join-Path $runtimeDir "Helper_4.1.33.6009.dll") Copy-RequiredFile -Source (Join-Path $binDir "Loader_4.1.33.6009.dll") -Destination (Join-Path $runtimeDir "Loader_4.1.33.6009.dll") Copy-Item -LiteralPath (Join-Path $repoRoot "requestdata") -Destination (Join-Path $runtimeDir "requestdata") -Recurse -Force Copy-Item -LiteralPath (Join-Path $repoRoot "eventdata") -Destination (Join-Path $runtimeDir "eventdata") -Recurse -Force $defaultConfig = @' { "callbackConfig": { "callbackUrl": "", "callbackToken": "", "httpPort": "10001", "enableCallback": false, "enableCloudAuth": false, "fileUploadUrl": "", "deviceCode": "" }, "lastUpdated": 0 } '@ Write-Utf8NoBom -Path (Join-Path $runtimeDir "config\config.json") -Content $defaultConfig Write-Utf8NoBom -Path (Join-Path $runtimeDir "config\client_status.json") -Content "{}" $knowledgeKeep = Join-Path $repoRoot "config\knowledge\.keep" if (Test-Path -LiteralPath $knowledgeKeep) { Copy-Item -LiteralPath $knowledgeKeep -Destination (Join-Path $runtimeDir "config\knowledge\.keep") -Force } else { Write-Utf8NoBom -Path (Join-Path $runtimeDir "config\knowledge\.keep") -Content "placeholder for installer-created knowledge directory" } $materialsIndex = Join-Path $repoRoot "config\materials\materials.json" if (Test-Path -LiteralPath $materialsIndex) { Copy-RequiredFile -Source $materialsIndex -Destination (Join-Path $runtimeDir "config\materials\materials.json") } else { Write-Utf8NoBom -Path (Join-Path $runtimeDir "config\materials\materials.json") -Content "{`"materials`":[]}" } Write-Host "==> Building Wails NSIS installer" $oldEap = $ErrorActionPreference; $ErrorActionPreference = "Continue" & $wails build --nsis -webview2 embed -trimpath 2>&1 | ForEach-Object { "$_" } $ErrorActionPreference = $oldEap $installer = Join-Path $binDir "qiweimanager-amd64-installer.exe" if (-not (Test-Path -LiteralPath $installer)) { throw "Installer was not generated: $installer" } # 添加时间戳到安装包文件名 $timestamp = Get-Date -Format "yyyyMMdd_HHmmss" $installerWithTime = Join-Path $binDir "qiweimanager-amd64-installer_$timestamp.exe" Move-Item -LiteralPath $installer -Destination $installerWithTime -Force Write-Host "==> Release complete: $installerWithTime"