$script:ApplicationReleaseRoot = Split-Path $PSScriptRoot -Parent function Read-DeploymentReleaseRecord { param([string]$Path, [string]$Url='') if (-not $Url) { return (Get-Content $Path -Raw -Encoding UTF8 | ConvertFrom-Json) } try { $address = [Uri]$Url if ($address.Scheme -notin @('http','https') -or $address.UserInfo) { throw 'Invalid release URL' } return ((Invoke-WebRequest -UseBasicParsing -Uri $address -TimeoutSec 20 -Headers @{'Accept'='application/json';'Cache-Control'='no-cache'}).Content | ConvertFrom-Json) } catch { throw '无法读取正式发布源;保留当前服务,不使用历史清单宣称已更新' } } function Get-ApplicationVersionPlan { param([string]$Path) $content = Get-Content $Path -Raw -Encoding UTF8 $selected = @(([string]$env:UPDATE_SERVICES).Trim() -split '\s+' | Where-Object { $_ }) $requested = if ($env:UPDATE_RELEASE_VERSION) { $env:UPDATE_RELEASE_VERSION } else { $env:DEPLOY_RELEASE_VERSION } if ($requested -and $requested -notmatch '^\d+\.\d+\.\d+\.\d{8}(\.\d+)?$') { throw '产品版本必须为完整版本号' } $relative = if ($requested) { "system-update/releases/$requested.json" } else { 'system-update/release-manifest.json' } $sourceMode = if ($null -ne $env:UPDATE_MANIFEST_SOURCE) { $env:UPDATE_MANIFEST_SOURCE } else { Get-EnvValue $content 'UPDATE_MANIFEST_SOURCE' } if (-not $sourceMode) { $sourceMode = 'remote' } $sourceMode = $sourceMode.Trim().ToLowerInvariant() if ($sourceMode -notin @('remote','local')) { throw 'UPDATE_MANIFEST_SOURCE 只支持 remote 或 local' } $releaseUrl = if ($null -ne $env:UPDATE_MANIFEST_URL) { $env:UPDATE_MANIFEST_URL } else { Get-EnvValue $content 'UPDATE_MANIFEST_URL' } if (-not $releaseUrl) { $releaseUrl = 'https://git.51easyai.com/wangbo/easyai/raw/branch/main/system-update/release-manifest.json' } try { $address = [Uri]$releaseUrl if ($address.Scheme -notin @('http','https') -or $address.UserInfo -or $releaseUrl -match '[\x00-\x1f\\]') { throw 'invalid URL' } } catch { throw '发布源必须是有效 HTTP(S) 地址,不能内嵌登录凭据或控制字符' } $manifestPath = Join-Path $script:ApplicationReleaseRoot $relative if ($sourceMode -eq 'remote' -and -not $requested) { $manifest = Read-DeploymentReleaseRecord -Url $releaseUrl } elseif (Test-Path $manifestPath) { $manifest = Read-DeploymentReleaseRecord -Path $manifestPath } elseif ($sourceMode -eq 'remote') { $manifest = Read-DeploymentReleaseRecord -Url ([Uri]::new([Uri]$releaseUrl,"releases/$requested.json").AbsoluteUri) } else { $manifest = Read-DeploymentReleaseRecord -Path $manifestPath } $version = [string]$manifest.productVersion if ($manifest.schemaVersion -ne 1 -or $version -notmatch '^\d+\.\d+\.\d+\.\d{8}(\.\d+)?$' -or $manifest.platform -ne 'linux/amd64' -or (-not $requested -and $manifest.channel -ne 'stable') -or ($requested -and $version -ne $requested)) { throw '发布记录的版本、通道或平台不正确' } $support = Read-DeploymentReleaseRecord -Path (Join-Path $script:ApplicationReleaseRoot 'system-update/deployment-components.json') $records = @{} foreach ($item in $support.components) { $records[$item.key] = $item } $productPath = Join-Path $script:ApplicationReleaseRoot "system-update/releases/$version.json" if ((Test-Path $productPath) -or $sourceMode -eq 'remote') { $product = if (Test-Path $productPath) { Read-DeploymentReleaseRecord -Path $productPath } else { Read-DeploymentReleaseRecord -Url ([Uri]::new([Uri]$releaseUrl,"releases/$version.json").AbsoluteUri) } if ($product.productVersion -ne $version) { throw '配套产物记录与产品版本不一致' } foreach ($item in $product.components) { $records[$item.key] = $item } } foreach ($item in $manifest.components) { $records[$item.key] = $item } foreach ($item in $manifest.deploymentComponents) { $records[$item.key] = $item } $mapping = @( @('easyai-server','server-main','SERVER_VERSION','UPDATE_SERVER_VERSION','comfy-ai/comfy-server'), @('easyai-web','web','WEB_VERSION','UPDATE_WEB_VERSION','comfy-ai/one-ai'), @('ws-gateway','ws-gateway','WS_GATEWAY_VERSION','UPDATE_GATEWAY_VERSION','easyaigc/wsgateway'), @('model-conversion-worker','model-conversion-worker','MODEL_CONVERSION_WORKER_VERSION','UPDATE_WORKER_VERSION','easyaigc/model-conversion-worker'), @('easyai-updater','updater','UPDATER_VERSION','UPDATE_UPDATER_VERSION','easyaigc/easyai-updater'), @('easyai-asg','service-governance','ASG_VERSION','UPDATE_ASG_VERSION','easyaigc/agent-governance'), @('agent-memory','memory-service','AMS_VERSION','UPDATE_AMS_VERSION','easyaigc/agent-memory') ) $plan = @() $mode = Get-EnvValue $content 'SYSTEM_UPDATE_MODE' if (-not $mode) { $mode = $env:EASYAI_UPDATE_MODE_HINT } $legacy = $mode -eq 'legacy_watchtower' foreach ($entry in $mapping) { $service,$component,$key,$option,$repository = $entry $explicit = [Environment]::GetEnvironmentVariable($option) if ($selected.Count -gt 0 -and $service -notin $selected) { if ($explicit) { throw "$option 要求在 UPDATE_SERVICES 中指定 $service" } continue } $image = "registry.cn-shanghai.aliyuncs.com/$repository" $stableDigest = "" $variant = "" if ($explicit) { $pattern = '^\d+\.\d+\.\d+\.\d{8}(\.\d+)?' if ($service -eq 'easyai-web') { $pattern += '(-no-ssr|-hmg-ai|-easyai|-chinacuc)?' } if ($service -eq 'easyai-updater') { $pattern = '^\d+\.\d+\.\d+(\.\d{8}(\.\d+)?)?' } if ($explicit -notmatch ($pattern + '(@sha256:[a-f0-9]{64})?$')) { throw "$option 必须是已发布的固定版本,不能使用浮动标签" } $target = $explicit } else { $item = $records[$component] $oldWeb = Get-EnvValue $content 'WEB_VERSION' if (-not $oldWeb) { $oldWeb = Get-EnvValue $content 'VERSION' } if ($service -eq 'easyai-web' -and $oldWeb -match '(-no-ssr|-hmg-ai|-easyai|-chinacuc)(@.*)?$') { $variant = $Matches[1] $variantKey = if ($variant -eq '-no-ssr') { 'web-no-ssr' } else { $variant.Substring(1) } $item = $records[$variantKey] if (-not $item) { throw "所选发布没有当前网页变体 $variant;请显式指定 UPDATE_WEB_VERSION" } } if (-not $item -or $item.image -ne $image -or $item.digest -notmatch '^sha256:[a-f0-9]{64}$') { throw "发布记录缺少有效配套产物 $component" } $tag = if ($item.tag) { [string]$item.tag } else { 'latest' } if ($tag -notmatch '^[A-Za-z0-9_][A-Za-z0-9_.-]{0,127}$') { throw '配套产物标签不正确' } $target = if ($service -eq 'easyai-updater' -and $tag -eq 'latest') { $tag } else { "$tag@$($item.digest)" } if ($legacy -and -not $requested -and $component -in @('server-main','web','ws-gateway')) { $target = 'latest' + $variant $stableDigest = [string]$item.digest } } $plan += [PSCustomObject]@{ Service=$service; Key=$key; Version=$target; Image=$image } if ($component -in @('server-main','web','ws-gateway')) { $plan += [PSCustomObject]@{ Service=''; Key=$key.Replace('_VERSION','_STABLE_DIGEST'); Version=$stableDigest; Image='' } } } if ($selected.Count -eq 0 -or ('easyai-server' -in $selected -and 'easyai-web' -in $selected)) { $plan += [PSCustomObject]@{ Service=''; Key='VERSION'; Version=$version; Image='' } } return $plan } function Set-ApplicationVersions { param([string]$Path, [object[]]$Plan) $content = Get-Content $Path -Raw -Encoding UTF8 foreach ($item in $Plan) { $content = Set-EnvValue $content $item.Key $item.Version } [System.IO.File]::WriteAllText($Path, $content, [System.Text.UTF8Encoding]::new($false)) foreach ($item in $Plan) { Write-Host "📌 已持久化应用版本: $($item.Key)=$($item.Version)" } } function Test-ApplicationVersionImages { param([object[]]$Plan, [string[]]$ComposeCommand) foreach ($item in $Plan) { if (-not $item.Service) { continue } $args = @($ComposeCommand | Select-Object -Skip 1) + @('config','--format','json',$item.Service) $executable = $ComposeCommand[0] $configuration = & $executable @args if ($LASTEXITCODE -ne 0) { throw '无法读取应用的有效镜像定义;尚未拉取或重建服务' } try { # Compose includes dependencies even when a service is selected. # Keep the configuration in memory; do not log its environment. $resolved = ($configuration -join "`n") | ConvertFrom-Json -ErrorAction Stop $actual = $resolved.services.PSObject.Properties[$item.Service].Value.image } catch { throw '无法读取应用的有效镜像定义;尚未拉取或重建服务' } if ("$actual".Trim() -ne "$($item.Image):$($item.Version)") { throw "$($item.Service) 的有效镜像未使用环境中的固定版本;请通过部署仓库的受控迁移恢复镜像定义,尚未拉取或重建服务" } } } function Set-DeploymentReleaseSource { param([string]$Path) $content = Get-Content $Path -Raw -Encoding UTF8 if ($null -ne $env:UPDATE_MANIFEST_SOURCE) { $mode = if ($env:UPDATE_MANIFEST_SOURCE) { $env:UPDATE_MANIFEST_SOURCE.Trim().ToLowerInvariant() } else { 'remote' } $content = Set-EnvValue $content 'UPDATE_MANIFEST_SOURCE' $mode } if ($null -ne $env:UPDATE_MANIFEST_URL) { $content = Set-EnvValue $content 'UPDATE_MANIFEST_URL' $env:UPDATE_MANIFEST_URL } [System.IO.File]::WriteAllText($Path, $content, [System.Text.UTF8Encoding]::new($false)) }