[CmdletBinding()]
param(
[Parameter(ValueFromRemainingArguments = $true)]
[string[]]$InstallArgs
)
$ErrorActionPreference = 'Stop'
$ProgressPreference = 'SilentlyContinue'
if ($PSVersionTable.PSEdition -eq 'Desktop') {
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
}
$staticBaseUrl = if ($env:BOT_FORGE_BASE_URL) {
$env:BOT_FORGE_BASE_URL.TrimEnd('/')
} else {
$null
}
$releaseTag = if ($env:BOT_FORGE_RELEASE_TAG) {
$env:BOT_FORGE_RELEASE_TAG
} else {
'v1.0.2'
}
$releaseAttachmentsBaseUrl = "https://api.gitcode.com/api/v5/repos/xuanwu/bot-forge/releases/$releaseTag/attach_files"
$architecture = try {
[System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture.ToString()
} catch {
if ($env:PROCESSOR_ARCHITEW6432) {
$env:PROCESSOR_ARCHITEW6432
} else {
$env:PROCESSOR_ARCHITECTURE
}
}
$platform = switch ($architecture) {
{ $_ -in 'X64', 'AMD64' } { 'windows-x86_64'; break }
{ $_ -in 'Arm64', 'ARM64' } { 'windows-aarch64'; break }
default { throw "不支持的 Windows 架构:$architecture" }
}
$localAppData = if ($env:LOCALAPPDATA) {
$env:LOCALAPPDATA
} else {
[Environment]::GetFolderPath([Environment+SpecialFolder]::LocalApplicationData)
}
$roamingAppData = if ($env:APPDATA) {
$env:APPDATA
} else {
[Environment]::GetFolderPath([Environment+SpecialFolder]::ApplicationData)
}
if (-not $localAppData -or -not $roamingAppData) {
throw '无法确定 Windows 应用数据目录;请设置 BOT_FORGE_HOME 和 BOT_FORGE_CONFIG_DIR。'
}
$dataDir = if ($env:BOT_FORGE_HOME) {
$env:BOT_FORGE_HOME
} else {
Join-Path $localAppData 'bot-forge'
}
$binDir = if ($env:BOT_FORGE_BIN_DIR) {
$env:BOT_FORGE_BIN_DIR
} else {
Join-Path $dataDir 'bin'
}
$configDir = if ($env:BOT_FORGE_CONFIG_DIR) {
$env:BOT_FORGE_CONFIG_DIR
} else {
Join-Path $roamingAppData 'bot-forge'
}
$binaryRelativePath = "$platform-bot-forge.exe"
$configRelativePath = 'bot-forge.toml'
$checksumsRelativePath = 'SHA256SUMS'
$checksumsSha256 = $env:BOT_FORGE_CHECKSUMS_SHA256
$binaryPath = Join-Path $binDir 'bot-forge.exe'
$configPath = Join-Path $configDir 'bot-forge.toml'
$tempDir = Join-Path ([System.IO.Path]::GetTempPath()) "bot-forge-$PID-$([guid]::NewGuid().ToString('N'))"
$exitCode = 1
function Download-File([string]$RelativePath, [string]$Destination) {
$uri = if ($staticBaseUrl) {
"$staticBaseUrl/$RelativePath"
} else {
"$releaseAttachmentsBaseUrl/$RelativePath/download"
}
Invoke-WebRequest -UseBasicParsing -Uri $uri -OutFile $Destination
}
function Get-ExpectedChecksum([string]$RelativePath, [string]$ChecksumsPath) {
foreach ($line in Get-Content -LiteralPath $ChecksumsPath) {
$match = [regex]::Match($line, '^\s*([A-Fa-f0-9]{64})\s+\*?(.+?)\s*$')
if ($match.Success -and $match.Groups[2].Value -eq $RelativePath) {
return $match.Groups[1].Value.ToLowerInvariant()
}
}
throw "SHA256SUMS 中缺少 $RelativePath 的校验值。"
}
function Test-DownloadedFile([string]$RelativePath, [string]$Path, [string]$ChecksumsPath) {
$expected = Get-ExpectedChecksum $RelativePath $ChecksumsPath
$actual = (Get-FileHash -LiteralPath $Path -Algorithm SHA256).Hash.ToLowerInvariant()
if ($actual -ne $expected) {
throw "文件校验失败:$RelativePath"
}
}
try {
New-Item -ItemType Directory -Force -Path $dataDir, $binDir, $configDir, $tempDir | Out-Null
$checksumsPath = Join-Path $tempDir 'SHA256SUMS'
$downloadedBinary = Join-Path $tempDir 'bot-forge.exe'
$downloadedConfig = Join-Path $tempDir 'bot-forge.toml'
Write-Host "正在下载 bot-forge($platform)..."
Download-File $checksumsRelativePath $checksumsPath
if (-not $checksumsSha256 -or $checksumsSha256 -notmatch '^[A-Fa-f0-9]{64}$') {
throw '必须通过 BOT_FORGE_CHECKSUMS_SHA256 提供可信的 SHA256SUMS 摘要。'
}
$actualChecksumsHash = (Get-FileHash -LiteralPath $checksumsPath -Algorithm SHA256).Hash.ToLowerInvariant()
if ($actualChecksumsHash -ne $checksumsSha256.ToLowerInvariant()) {
throw 'SHA256SUMS 发布摘要校验失败。'
}
Download-File $binaryRelativePath $downloadedBinary
Download-File $configRelativePath $downloadedConfig
Test-DownloadedFile $binaryRelativePath $downloadedBinary $checksumsPath
Test-DownloadedFile $configRelativePath $downloadedConfig $checksumsPath
$stagedBinary = Join-Path $binDir ".bot-forge.new-$([guid]::NewGuid().ToString('N')).exe"
$binaryReplacementBackup = Join-Path $binDir ".bot-forge.old-$([guid]::NewGuid().ToString('N')).exe"
Copy-Item -LiteralPath $downloadedBinary -Destination $stagedBinary
try {
if (Test-Path -LiteralPath $binaryPath) {
[System.IO.File]::Replace($stagedBinary, $binaryPath, $binaryReplacementBackup)
} else {
[System.IO.File]::Move($stagedBinary, $binaryPath)
}
} finally {
Remove-Item -LiteralPath $stagedBinary -Force -ErrorAction SilentlyContinue
Remove-Item -LiteralPath $binaryReplacementBackup -Force -ErrorAction SilentlyContinue
}
Write-Host '正在验证服务器下发的云端配置...'
& $binaryPath config validate --config $downloadedConfig
if ($LASTEXITCODE -ne 0) {
throw "服务器下发的 bot-forge.toml 配置验证失败,退出码:$LASTEXITCODE"
}
if (Test-Path -LiteralPath $configPath) {
$configBackupPath = "${configPath}.bot-forge.bak"
Copy-Item -LiteralPath $configPath -Destination $configBackupPath -Force
Write-Host "已有配置已备份:$configBackupPath"
}
$stagedConfig = Join-Path $configDir ".bot-forge.toml.new-$([guid]::NewGuid().ToString('N'))"
$configReplacementBackup = Join-Path $configDir ".bot-forge.toml.old-$([guid]::NewGuid().ToString('N'))"
Copy-Item -LiteralPath $downloadedConfig -Destination $stagedConfig
try {
if (Test-Path -LiteralPath $configPath) {
[System.IO.File]::Replace($stagedConfig, $configPath, $configReplacementBackup)
} else {
[System.IO.File]::Move($stagedConfig, $configPath)
}
} finally {
Remove-Item -LiteralPath $stagedConfig -Force -ErrorAction SilentlyContinue
Remove-Item -LiteralPath $configReplacementBackup -Force -ErrorAction SilentlyContinue
}
Write-Host "云端配置已写入:$configPath"
Write-Host '下载和校验完成,开始运行 bot-forge。'
& $binaryPath install --config $configPath @InstallArgs
$exitCode = $LASTEXITCODE
} finally {
if (Test-Path -LiteralPath $tempDir) {
Remove-Item -LiteralPath $tempDir -Recurse -Force
}
}
exit $exitCode