param([string]$ProjectRoot = (Split-Path $PSScriptRoot -Parent)) $ErrorActionPreference = 'Stop' Set-Location $ProjectRoot . (Join-Path $ProjectRoot 'scripts/Initialize-SecurityEnv.ps1') $content = [System.IO.File]::ReadAllText((Join-Path $ProjectRoot '.env')) if ((Get-EnvValue $content 'INFRASTRUCTURE_AUTO_UPDATE_ENABLED') -eq 'false') { return } $state = Join-Path $ProjectRoot 'data/infrastructure-update' [System.IO.Directory]::CreateDirectory($state) | Out-Null try { $lock = [System.IO.File]::Open((Join-Path $state 'task.lock'), 'OpenOrCreate', 'ReadWrite', 'None') } catch [System.IO.IOException] { return } $log = Join-Path $state 'last-run.log' $previousConfig = $env:DOCKER_CONFIG try { [System.IO.File]::WriteAllText($log, "$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') 检查 Watchtower 和 updater 更新`n") $configFile = Get-EnvValue $content 'DOCKER_CONFIG_FILE' if (-not [string]::IsNullOrWhiteSpace($configFile) -and (Test-Path $configFile -PathType Leaf)) { $env:DOCKER_CONFIG = Split-Path $configFile -Parent } & docker compose version *> $null $v2 = $LASTEXITCODE -eq 0 function Invoke-InfrastructureCompose { param([string[]]$ComposeArguments) if ($v2) { $output = & docker compose @ComposeArguments 2>&1 } else { $output = & docker-compose @ComposeArguments 2>&1 } $code = $LASTEXITCODE $output | Out-File $log -Append -Encoding utf8 if ($code -ne 0) { throw '组件更新命令未成功,现有容器状态需查看日志' } } if ($v2) { $available = @(& docker compose config --services) } else { $available = @(& docker-compose config --services) } if ($LASTEXITCODE -ne 0 -or $available -notcontains 'watchtower') { throw '无法读取 Watchtower 部署配置' } $failed = @() foreach ($service in @('watchtower', 'easyai-updater')) { # Do not enable updater in profile-disabled legacy deployments. if ($available -notcontains $service) { "$service 未启用,跳过维护" | Out-File $log -Append -Encoding utf8 continue } try { "检查 $service" | Out-File $log -Append -Encoding utf8 Invoke-InfrastructureCompose -ComposeArguments @('pull', $service) Invoke-InfrastructureCompose -ComposeArguments @('up', '-d', '--no-deps', $service) $healthy = $false for ($attempt=0; $attempt -lt 12; $attempt++) { if ($v2) { $containerId = & docker compose ps -q $service } else { $containerId = & docker-compose ps -q $service } if ($LASTEXITCODE -ne 0) { throw "无法读取 $service 状态" } if ($containerId) { $status = & docker inspect -f '{{.State.Running}} {{if .State.Health}}{{.State.Health.Status}}{{else}}none{{end}}' $containerId if ($LASTEXITCODE -ne 0) { throw "无法读取 $service 状态" } if ($status -eq 'true healthy') { $healthy = $true; break } if ($status -eq 'true none') { if ($service -eq 'watchtower') { & docker exec $containerId /watchtower --health-check *> $null } else { & docker exec $containerId node -e "require('http').get('http://127.0.0.1:3015/health',r=>{r.resume();process.exit(r.statusCode===200?0:1)}).on('error',()=>process.exit(1))" *> $null } if ($LASTEXITCODE -eq 0) { $healthy = $true; break } } } Start-Sleep -Seconds 5 } if (-not $healthy) { throw "$service 暂未恢复可用" } "$service 已恢复可用" | Out-File $log -Append -Encoding utf8 } catch { "$service 本次维护未成功,下次定时任务将继续检查:$($_.Exception.Message)" | Out-File $log -Append -Encoding utf8 $failed += $service } } if ($failed.Count -gt 0) { throw "更新组件维护未全部成功:$($failed -join ', ')" } } finally { $env:DOCKER_CONFIG = $previousConfig $lock.Dispose() }