# ================================================================ # FLNXI 素材系统 - 历史内部发布管理工具 v2.0 # 状态:已废弃,不作为客户安装、升级或正式发布入口 # 当前正式发布入口请使用 deploy-registry.ps1 # ================================================================ param( [switch]$Help, [switch]$AcknowledgeLegacyInternalPublish ) $ErrorActionPreference = "Stop" [Console]::OutputEncoding = [System.Text.Encoding]::UTF8 if (-not $Help -and -not $AcknowledgeLegacyInternalPublish) { Write-Host "[ERROR] 发布管理.ps1 是历史内部发布控制台,默认禁止执行。" -ForegroundColor Red Write-Host "当前客户/测试发布请使用 deploy-registry.ps1。" -ForegroundColor Yellow Write-Host "该脚本包含旧服务器、旧 Harbor、许可证服务和 Docker 清理逻辑,不应作为客户交付入口。" -ForegroundColor Yellow Write-Host "如确需排查历史链路,请追加 -AcknowledgeLegacyInternalPublish。" -ForegroundColor Yellow exit 1 } # 配置 $Config = @{ Server = "" SshKey = "" HarborHost = "" HarborProject = "flnxi" # 公开项目(素材系统) HarborPublicProject = "flnxi-public" # 公开项目(Agent、runtime) DockerHubUser = "flnxi" HealthCheckPort = 4320 HealthCheckTimeout = 45 SshRetryCount = 3 SshRetryDelay = 2 # 新许可证服务器配置 LicenseServer = "" LicenseServerUser = "root" LicenseServerSshKey = "" LicenseServerPort = 9527 } # 趣味提示 $script:FunTips = @( "记得喝口咖啡,让部署的快乐多续一会 ☕", "猫咪提示:部署途中别忘了伸个懒腰 🐱", "数据正在轨道中穿梭,耐心是宇航员的必备素质 🚀", "小彩蛋:部署顺利指数 +1%,好运常伴你 🍀", "镜像像快递一样打包上路,收件人:Production 📦", "每一个 build 都是一次漂亮的起飞 ✈️", "FLNXI素材库正在打包,准备惊艳登场 🎉", "部署速度:光速模式已启动 ⚡", "夜深了?没关系,部署脚本陪你到天亮 🌙", "部署完成后,记得给自己一个小奖励 🎁" ) # 进度追踪器 $script:StageTracker = @{ Total = 0; Current = 0 } function Get-FunTip { return Get-Random -InputObject $script:FunTips } # 颜色输出 function Write-Color { param([string]$Text, [string]$Color = "White") Write-Host $Text -ForegroundColor $Color } function Write-Title { param([string]$Text) Write-Host "" Write-Color "═══════════════════════════════════════════════════════════" "Cyan" Write-Color " $Text" "Cyan" Write-Color "═══════════════════════════════════════════════════════════" "Cyan" } function Write-Success { param([string]$Text) Write-Color "[✓] $Text" "Green" } function Write-Warning { param([string]$Text) Write-Color "[!] $Text" "Yellow" } function Write-Error { param([string]$Text) Write-Color "[✗] $Text" "Red" } function Write-Info { param([string]$Text) Write-Color "[i] $Text" "Gray" } # 进度条显示 function Show-Progress { param([int]$Current, [int]$Total, [string]$Status = "") $percent = if ($Total -gt 0) { [Math]::Round(($Current / $Total) * 100) } else { 0 } $barLength = 30 $filled = [Math]::Floor($percent / 100 * $barLength) $bar = ("█" * $filled).PadRight($barLength, "░") Write-Color " [$bar] $percent% $Status" "Magenta" } # 阶段开始 function Enter-Stage { param([string]$Title, [string]$Subtitle = "") $script:StageTracker.Current++ $current = $script:StageTracker.Current $total = $script:StageTracker.Total Write-Host "" Write-Color "🚀 $Title ($current/$total)" "Magenta" Show-Progress -Current ($current - 1) -Total $total if ($Subtitle) { Write-Info $Subtitle } } # 阶段完成庆祝 function Complete-Stage { param([string]$Title) $tip = Get-FunTip Write-Success "$Title 完成!" Write-Color " $tip" "DarkGray" } # 确认操作 function Confirm-Action { param([string]$Message, [string]$Warning = "") Write-Host "" if ($Warning) { Write-Color "⚠️ 警告: $Warning" "Yellow" Write-Host "" } Write-Color $Message "White" $confirm = Read-Host "确认执行? (输入 'yes' 继续)" return $confirm -eq "yes" } # 获取版本号 function Get-CurrentVersion { if (Test-Path "VERSION.txt") { return (Get-Content "VERSION.txt" -Raw).Trim() } return "未知" } function Get-AgentVersion { # 优先读取 Go 版本 if (Test-Path "flnxi-agent-go/VERSION.txt") { return (Get-Content "flnxi-agent-go/VERSION.txt" -Raw).Trim() } # 兼容旧版 Node.js 版本 if (Test-Path "flnxi-agent/VERSION.txt") { return (Get-Content "flnxi-agent/VERSION.txt" -Raw).Trim() } return "1.0.0" } # SSH 连接测试(带重试) function Test-SSHConnection { $target = "root@$($Config.Server)" $attempt = 0 while ($attempt -lt $Config.SshRetryCount) { $attempt++ if ($attempt -gt 1) { Write-Info "SSH 连接重试 ($attempt/$($Config.SshRetryCount))..." Start-Sleep -Seconds $Config.SshRetryDelay } $result = ssh -i $Config.SshKey -o "BatchMode=yes" -o "ConnectTimeout=10" $target "echo ok" 2>&1 if ($LASTEXITCODE -eq 0) { Write-Success "SSH 连接成功" return $true } } Write-Error "SSH 连接失败,已重试 $($Config.SshRetryCount) 次" return $false } # 健康检查 function Test-RemoteHealth { param([int]$Port = 4320, [int]$MaxWait = 45) $healthUrl = "http://$($Config.Server):$Port/api/health" Write-Info "开始健康检查: $healthUrl" $elapsed = 0 $interval = 3 while ($elapsed -lt $MaxWait) { try { $resp = Invoke-WebRequest -Uri $healthUrl -Method GET -TimeoutSec 5 -ErrorAction Stop if ($resp.StatusCode -eq 200) { Write-Success "健康检查通过,应用已就绪 (${elapsed}s)" $tip = Get-FunTip Write-Color " $tip" "DarkGray" return $true } } catch { Write-Info "应用尚未就绪 (${elapsed}s),继续等待..." } Start-Sleep -Seconds $interval $elapsed += $interval } Write-Warning "健康检查超时 (${MaxWait}s),请手动检查" return $false } # 清理本地 Docker 镜像 function Clean-LocalImages { param([string]$ImagePattern = "flnxi", [int]$KeepCount = 5) Write-Info "清理本地 Docker 镜像..." # 清理悬空镜像 Write-Info " 清理悬空镜像..." docker image prune -f 2>&1 | Out-Null Write-Success " 悬空镜像已清理" # 清理旧版本镜像 Write-Info " 清理 $ImagePattern 相关的旧镜像 (保留最新 $KeepCount 个)..." $images = docker images --format "{{.ID}}`t{{.Repository}}:{{.Tag}}`t{{.CreatedAt}}" | Where-Object { $_ -match $ImagePattern } if ($images) { $sorted = $images | Sort-Object { ($_ -split "`t")[2] } -Descending $toRemove = $sorted | Select-Object -Skip $KeepCount $removedCount = 0 foreach ($img in $toRemove) { $parts = $img -split "`t" $imgId = $parts[0] $imgName = $parts[1] Write-Info " 删除: $imgName" docker rmi -f $imgId 2>&1 | Out-Null $removedCount++ } if ($removedCount -gt 0) { Write-Success " 已清理 $removedCount 个旧镜像" } else { Write-Info " 没有需要清理的旧镜像" } } } # 清理远程服务器镜像 function Clean-RemoteImages { param([int]$KeepCount = 5) Write-Info "清理服务器上的 Docker 镜像..." $cleanCmd = @" docker image prune -f docker images --format '{{.ID}}\t{{.Repository}}:{{.Tag}}' | grep 'flnxi' | tail -n +$($KeepCount + 1) | awk '{print \$1}' | xargs -r docker rmi -f 2>/dev/null || true echo '清理完成' "@ ssh -i $Config.SshKey "root@$($Config.Server)" $cleanCmd if ($LASTEXITCODE -eq 0) { Write-Success "服务器镜像清理完成" } else { Write-Warning "服务器镜像清理可能未完全成功" } } # 显示菜单 function Show-Menu { Clear-Host Write-Color "╔═══════════════════════════════════════════════════════════╗" "Cyan" Write-Color "║ FLNXI 素材系统 - 发布管理工具 v2.0 ║" "Cyan" Write-Color "╠═══════════════════════════════════════════════════════════╣" "Cyan" Write-Color "║ ║" "Cyan" Write-Color "║ === 开发测试 === ║" "White" Write-Color "║ [1] 构建并部署测试版 ║" "White" Write-Color "║ ║" "Cyan" Write-Color "║ === 正式发布 (需要确认) === ║" "Yellow" Write-Color "║ [2] 发布素材系统正式版 (flnxi 公开) ⚠️ ║" "Yellow" Write-Color "║ [3] 发布 Agent (flnxi-public 公开) ⚠️ ║" "Yellow" Write-Color "║ ║" "Cyan" Write-Color "║ === 基础设施 === ║" "White" Write-Color "║ [4] 同步 Runtime 镜像 (flnxi-public 公开) ║" "White" Write-Color "║ [5] 部署许可证服务器 ⚠️ ║" "Yellow" Write-Color "║ ║" "Cyan" Write-Color "║ === 维护 === ║" "White" Write-Color "║ [6] 查看版本和镜像状态 ║" "White" Write-Color "║ [7] 清理本地旧镜像 ║" "White" Write-Color "║ [8] 清理服务器旧镜像 ║" "White" Write-Color "║ [9] 深度清理 Docker (释放磁盘空间) 🧹 ║" "Red" Write-Color "║ ║" "Cyan" Write-Color "║ [0] 退出 ║" "Gray" Write-Color "║ ║" "Cyan" Write-Color "╚═══════════════════════════════════════════════════════════╝" "Cyan" Write-Host "" Write-Color "当前素材系统版本: $(Get-CurrentVersion) | Agent 版本: $(Get-AgentVersion)" "Gray" Write-Color "服务器: $($Config.Server) | Harbor: $($Config.HarborHost)" "DarkGray" Write-Host "" } # ==================== 功能实现 ==================== # 1. 构建并部署测试版 function Deploy-TestVersion { Write-Title "构建并部署测试版" $tag = "test-$(Get-Date -Format 'yyyyMMdd')" Write-Info "测试版标签: $tag" Write-Info "目标: harbor.flnxi.com/flnxi/material-dev/flnxi-material:$tag" Write-Host "" # 启用 BuildKit 以使用缓存加速 $env:DOCKER_BUILDKIT = "1" # 优先加载本地基础镜像(避免网络问题) $LocalImagePath = "docker-images/node/node-18-alpine.tar" if (Test-Path $LocalImagePath) { Write-Info "检查本地基础镜像..." $existingImage = docker images node:18-alpine --format "{{.ID}}" 2>$null if (-not $existingImage) { Write-Info "加载本地基础镜像: $LocalImagePath" docker load -i $LocalImagePath if ($LASTEXITCODE -eq 0) { Write-Success "本地基础镜像加载成功" } else { Write-Warning "本地基础镜像加载失败,将尝试在线拉取" } } else { Write-Success "本地基础镜像已存在" } } else { Write-Warning "未找到本地基础镜像 ($LocalImagePath),将使用在线拉取" Write-Info "建议: 运行 'docker save node:18-alpine -o $LocalImagePath' 保存基础镜像" } Write-Info "开始构建 (BuildKit 缓存已启用)..." Write-Info "提示: 首次构建较慢,后续构建会利用缓存加速" # 构建镜像 $buildArgs = @( "build", "--progress=plain", "-t", "flnxi-material:$tag", "-t", "$($Config.HarborHost)/$($Config.HarborProject)/material-dev/flnxi-material:$tag", "-t", "$($Config.HarborHost)/$($Config.HarborProject)/material-dev/flnxi-material:latest", "." ) docker @buildArgs if ($LASTEXITCODE -ne 0) { Write-Error "构建失败" return } Write-Success "构建完成" # 推送到 Harbor material-dev Write-Info "推送到 Harbor (material-dev)..." docker push "$($Config.HarborHost)/$($Config.HarborProject)/material-dev/flnxi-material:$tag" docker push "$($Config.HarborHost)/$($Config.HarborProject)/material-dev/flnxi-material:latest" if ($LASTEXITCODE -ne 0) { Write-Error "推送失败" return } Write-Success "推送完成" # 部署到测试服务器 Write-Info "部署到测试服务器..." $deployCmd = @" cd /root/deploy && \ docker pull $($Config.HarborHost)/$($Config.HarborProject)/material-dev/flnxi-material:latest && \ docker compose down && \ docker compose up -d "@ ssh -i $Config.SshKey "root@$($Config.Server)" $deployCmd if ($LASTEXITCODE -eq 0) { Write-Success "部署命令执行成功" # 健康检查 Write-Info "等待服务启动..." Start-Sleep -Seconds 5 $healthy = Test-RemoteHealth -Port $Config.HealthCheckPort -MaxWait $Config.HealthCheckTimeout if ($healthy) { Complete-Stage "测试版部署" } Write-Info "访问地址: http://$($Config.Server):4320" } else { Write-Error "部署失败" } } # 2. 发布素材系统正式版 function Publish-ReleaseVersion { Write-Title "发布素材系统正式版" $version = Get-CurrentVersion Write-Info "当前版本: $version" $newVersion = Read-Host "请输入新版本号 (当前: $version)" if ([string]::IsNullOrWhiteSpace($newVersion)) { Write-Warning "取消发布" return } # 确认 if (-not (Confirm-Action ` "将发布正式版 $newVersion 到 Harbor" ` "此操作将发布正式版镜像,所有客户将可以拉取更新!")) { Write-Warning "取消发布" return } # 更新版本文件 $newVersion | Out-File -FilePath "VERSION.txt" -Encoding UTF8 -NoNewline Write-Success "版本文件已更新: $newVersion" # 启用 BuildKit $env:DOCKER_BUILDKIT = "1" # 优先加载本地基础镜像(避免网络问题) $LocalImagePath = "docker-images/node/node-18-alpine.tar" if (Test-Path $LocalImagePath) { Write-Info "检查本地基础镜像..." $existingImage = docker images node:18-alpine --format "{{.ID}}" 2>$null if (-not $existingImage) { Write-Info "加载本地基础镜像: $LocalImagePath" docker load -i $LocalImagePath if ($LASTEXITCODE -eq 0) { Write-Success "本地基础镜像加载成功" } else { Write-Warning "本地基础镜像加载失败,将尝试在线拉取" } } else { Write-Success "本地基础镜像已存在" } } else { Write-Warning "未找到本地基础镜像 ($LocalImagePath),将使用在线拉取" Write-Info "建议: 运行 'docker save node:18-alpine -o $LocalImagePath' 保存基础镜像" } # 构建正式版镜像 Write-Info "构建正式版镜像 (BuildKit 缓存已启用)..." $buildArgs = @( "build", "--progress=plain", "-t", "flnxi-material:$newVersion", "-t", "$($Config.HarborHost)/$($Config.HarborProject)/material/flnxi-material:$newVersion", "-t", "$($Config.HarborHost)/$($Config.HarborProject)/material/flnxi-material:latest", "--build-arg", "APP_VERSION=$newVersion", "." ) docker @buildArgs if ($LASTEXITCODE -ne 0) { Write-Error "构建失败" return } Write-Success "构建完成" # 推送到 Harbor material (正式版) Write-Info "推送到 Harbor (material - 正式版)..." docker push "$($Config.HarborHost)/$($Config.HarborProject)/material/flnxi-material:$newVersion" docker push "$($Config.HarborHost)/$($Config.HarborProject)/material/flnxi-material:latest" if ($LASTEXITCODE -eq 0) { Write-Success "正式版 $newVersion 发布完成!" Write-Info "客户可通过以下地址拉取:" Write-Info " $($Config.HarborHost)/$($Config.HarborProject)/material/flnxi-material:latest" } else { Write-Error "推送失败" } } # 3. 发布 Agent 安装程序 (Go 版本) function Publish-Agent { Write-Title "发布 Agent 安装程序 (Go 版本)" $currentVersion = Get-AgentVersion Write-Info "当前 Agent 版本: $currentVersion" Write-Info "技术栈: Go (镜像约 70MB)" # Harbor 公开项目镜像配置 # 格式: harbor.flnxi.com/flnxi-public/agent:版本号 $harborRepo = "$($Config.HarborHost)/$($Config.HarborPublicProject)/agent" $newVersion = Read-Host "请输入新版本号 (当前: $currentVersion)" if ([string]::IsNullOrWhiteSpace($newVersion)) { Write-Warning "取消发布" return } Write-Host "" Write-Info "镜像将推送到 Harbor flnxi-public 项目 (公开,无需登录即可拉取):" Write-Info " ${harborRepo}:${newVersion}" Write-Info " ${harborRepo}:latest" Write-Host "" # 确认 if (-not (Confirm-Action ` "将发布 Agent $newVersion 到 Harbor (flnxi-public)" ` "此操作将更新 Harbor 上的安装程序,影响所有新安装的客户!")) { Write-Warning "取消发布" return } # 检查 Harbor 登录 $loginCheck = docker login $Config.HarborHost 2>&1 if ($LASTEXITCODE -ne 0) { Write-Warning "请先登录 Harbor: docker login $($Config.HarborHost)" return } Write-Success "Harbor 登录状态: OK" # 更新版本文件 (Go 版本目录) $newVersion | Out-File -FilePath "flnxi-agent-go/VERSION.txt" -Encoding UTF8 -NoNewline Write-Success "Agent 版本文件已更新: $newVersion" # 启用 BuildKit $env:DOCKER_BUILDKIT = "1" # 构建 Agent 镜像 (Go 版本) Write-Info "构建 Agent 镜像 (Go 版本, BuildKit 缓存已启用)..." Push-Location "flnxi-agent-go" try { $buildArgs = @( "build", "--progress=plain", "-t", "${harborRepo}:${newVersion}", "-t", "${harborRepo}:latest", "." ) docker @buildArgs if ($LASTEXITCODE -ne 0) { Write-Error "构建失败" return } Write-Success "构建完成" # 显示镜像大小 $imageSize = docker images "${harborRepo}:${newVersion}" --format "{{.Size}}" Write-Info "镜像大小: $imageSize" # 推送到 Harbor flnxi-public 项目 Write-Info "推送到 Harbor (flnxi-public)..." docker push "${harborRepo}:${newVersion}" docker push "${harborRepo}:latest" if ($LASTEXITCODE -eq 0) { Write-Success "Agent $newVersion 发布完成!" Write-Host "" Write-Color "═══════════════════════════════════════════════════════════" "Cyan" Write-Info "镜像地址: ${harborRepo}:latest" Write-Info "镜像大小: $imageSize (Go 版本,比 Node.js 版本减少 ~70%)" Write-Info "客户安装命令:" Write-Color " docker run -d -p 9090:9090 -v /var/run/docker.sock:/var/run/docker.sock ${harborRepo}:latest" "White" Write-Host "" Write-Info "一键安装脚本:" Write-Color " curl -sSL https://install.flnxi.com | bash" "White" Write-Color "═══════════════════════════════════════════════════════════" "Cyan" } else { Write-Error "推送失败" } } finally { Pop-Location } } # 4. 同步 Runtime 镜像 function Sync-RuntimeImages { Write-Title "同步 Runtime 镜像到 Harbor (flnxi-public)" # Runtime 镜像列表(含国内镜像源) # 推送到 flnxi-public 公开项目,无需登录即可拉取 $runtimeImages = @( @{ Source = "postgres:16-alpine"; Mirror = "registry.cn-hangzhou.aliyuncs.com/library/postgres:16-alpine"; Target = "runtime/postgres:16-alpine" }, @{ Source = "postgres:15-alpine"; Mirror = "registry.cn-hangzhou.aliyuncs.com/library/postgres:15-alpine"; Target = "runtime/postgres:15-alpine" }, @{ Source = "minio/minio:latest"; Mirror = "quay.io/minio/minio:latest"; Target = "runtime/minio:latest" }, @{ Source = "minio/minio:RELEASE.2025-04-22T22-12-26Z"; Mirror = "quay.io/minio/minio:RELEASE.2025-04-22T22-12-26Z"; Target = "runtime/minio:RELEASE.2025-04-22T22-12-26Z" }, @{ Source = "minio/mc:latest"; Mirror = "quay.io/minio/mc:latest"; Target = "runtime/minio-mc:latest" }, @{ Source = "redis:7-alpine"; Mirror = "registry.cn-hangzhou.aliyuncs.com/library/redis:7-alpine"; Target = "runtime/redis:7-alpine" }, @{ Source = "nginx:alpine"; Mirror = "registry.cn-hangzhou.aliyuncs.com/library/nginx:alpine"; Target = "runtime/nginx:alpine" }, @{ Source = "node:20-alpine"; Mirror = "registry.cn-hangzhou.aliyuncs.com/library/node:20-alpine"; Target = "runtime/node:20-alpine" } ) Write-Info "将同步以下镜像到 Harbor flnxi-public 项目 (公开,无需登录即可拉取):" foreach ($img in $runtimeImages) { Write-Info " - $($img.Source)" } Write-Host "" # 检查 Harbor 登录 $loginCheck = docker login $Config.HarborHost 2>&1 if ($LASTEXITCODE -ne 0) { Write-Warning "请先登录 Harbor: docker login $($Config.HarborHost)" return } Write-Success "Harbor 登录状态: OK" Write-Host "" $successCount = 0 $failCount = 0 $total = $runtimeImages.Count foreach ($img in $runtimeImages) { $index = $runtimeImages.IndexOf($img) + 1 $source = $img.Source $mirror = $img.Mirror # 推送到 flnxi-public 公开项目 $target = "$($Config.HarborHost)/$($Config.HarborPublicProject)/$($img.Target)" Write-Color "[$index/$total] $source -> $target" "White" # 优先尝试国内镜像源 $pullSuccess = $false if ($mirror) { Write-Info " 尝试国内镜像源: $mirror" docker pull $mirror 2>&1 | Out-Null if ($LASTEXITCODE -eq 0) { docker tag $mirror $source 2>&1 | Out-Null Write-Success " 国内镜像源拉取成功" $pullSuccess = $true } else { Write-Warning " 国内镜像源失败,尝试官方源..." } } # 回退到官方源 if (-not $pullSuccess) { Write-Info " 从官方源拉取: $source" docker pull $source 2>&1 | Out-Null if ($LASTEXITCODE -eq 0) { Write-Success " 官方源拉取成功" $pullSuccess = $true } } if (-not $pullSuccess) { Write-Error " 拉取失败" $failCount++ continue } # 打标签并推送 Write-Info " 打标签并推送到 Harbor..." docker tag $source $target 2>&1 | Out-Null docker push $target 2>&1 | Out-Null if ($LASTEXITCODE -eq 0) { Write-Success " 完成 ✓" $successCount++ } else { Write-Error " 推送失败" $failCount++ } Write-Host "" } Write-Color "═══════════════════════════════════════════════════════════" "Cyan" Write-Color "同步完成 - 成功: $successCount, 失败: $failCount" $(if ($failCount -eq 0) { "Green" } else { "Yellow" }) } # 5. 部署许可证服务器 function Deploy-LicenseServer { Write-Title "部署许可证服务器" Write-Info "目标服务器: $($Config.Server)" Write-Info "部署路径: /opt/license-server" Write-Host "" # 检查是否已有 .env 文件 $envExists = $false try { $checkEnv = ssh -i $Config.SshKey "root@$($Config.Server)" "test -f /opt/license-server/.env && echo 'exists'" 2>$null $envExists = $checkEnv -eq "exists" } catch {} if ($envExists) { Write-Info "检测到已有配置文件 (.env)" Write-Host "" Write-Color "请选择操作:" "Yellow" Write-Color " [1] 更新代码并重启 (保留现有配置)" "White" Write-Color " [2] 全新部署 (重新生成密钥)" "White" Write-Color " [0] 取消" "Gray" Write-Host "" $deployChoice = Read-Host "选择" switch ($deployChoice) { "1" { Deploy-LicenseServer-Update } "2" { Deploy-LicenseServer-Fresh } default { Write-Warning "取消部署"; return } } } else { # 首次部署 if (-not (Confirm-Action ` "将首次部署许可证服务器到 $($Config.Server)" ` "此操作将生成新的 RSA 密钥对!")) { Write-Warning "取消部署" return } Deploy-LicenseServer-Fresh } } # 更新许可证服务器(保留配置) function Deploy-LicenseServer-Update { Write-Info "更新许可证服务器代码..." # 测试 SSH 连接 if (-not (Test-SSHConnection)) { return } # 上传文件(排除 .env 和 node_modules) Write-Info "上传项目文件..." # 创建临时目录 $tempDir = Join-Path $env:TEMP "license-server-deploy-$(Get-Date -Format 'yyyyMMddHHmmss')" New-Item -ItemType Directory -Path $tempDir -Force | Out-Null # 复制必要文件 $filesToCopy = @( "docker-compose.yml", "Dockerfile", "package.json", "package-lock.json", "next.config.js", "tsconfig.json", "tailwind.config.ts", "postcss.config.js" ) foreach ($file in $filesToCopy) { $src = "license-server/$file" if (Test-Path $src) { Copy-Item -Path $src -Destination $tempDir -Force } } # 复制目录 $dirsToCopy = @("prisma", "app", "lib", "scripts") foreach ($dir in $dirsToCopy) { $src = "license-server/$dir" if (Test-Path $src) { Copy-Item -Path $src -Destination $tempDir -Recurse -Force } } # 创建 public 目录(如果不存在) $publicDir = "license-server/public" if (Test-Path $publicDir) { Copy-Item -Path $publicDir -Destination $tempDir -Recurse -Force } else { New-Item -ItemType Directory -Path "$tempDir/public" -Force | Out-Null } # 上传到服务器 scp -i $Config.SshKey -r "$tempDir/*" "root@$($Config.Server):/opt/license-server/" if ($LASTEXITCODE -ne 0) { Write-Error "文件上传失败" Remove-Item -Recurse -Force $tempDir -ErrorAction SilentlyContinue return } Write-Success "文件上传完成" # 清理临时目录 Remove-Item -Recurse -Force $tempDir -ErrorAction SilentlyContinue # 重新构建和启动 Write-Info "重新构建和启动服务..." $deployCmd = @" cd /opt/license-server && \ docker compose down && \ docker compose build --no-cache && \ docker compose up -d && \ sleep 15 && \ docker compose exec -T license-server npx prisma db push --accept-data-loss 2>/dev/null || \ docker compose exec -T license-server npx prisma db push "@ ssh -i $Config.SshKey "root@$($Config.Server)" $deployCmd if ($LASTEXITCODE -eq 0) { Write-Success "许可证服务器更新完成!" Write-Info "访问地址: http://$($Config.Server):3001" Write-Info "管理后台: http://$($Config.Server):3001/admin" } else { Write-Error "更新失败,请检查日志" } } # 全新部署许可证服务器 function Deploy-LicenseServer-Fresh { Write-Info "全新部署许可证服务器..." # 测试 SSH 连接 if (-not (Test-SSHConnection)) { return } # Step 1: 生成 RSA 密钥对 Write-Info "生成 RSA 密钥对..." # 使用 OpenSSL 生成密钥(Windows 上需要安装 OpenSSL 或使用 Git Bash) $keysDir = "license-server/keys" if (-not (Test-Path $keysDir)) { New-Item -ItemType Directory -Path $keysDir -Force | Out-Null } # 尝试使用 openssl $opensslPath = $null $gitBashOpenssl = "C:\Program Files\Git\usr\bin\openssl.exe" if (Test-Path $gitBashOpenssl) { $opensslPath = $gitBashOpenssl } elseif (Get-Command openssl -ErrorAction SilentlyContinue) { $opensslPath = "openssl" } if ($opensslPath) { Write-Info "使用 OpenSSL 生成密钥..." & $opensslPath genrsa -out "$keysDir/private.key" 2048 2>$null & $opensslPath rsa -in "$keysDir/private.key" -pubout -out "$keysDir/public.key" 2>$null if ($LASTEXITCODE -eq 0) { Write-Success "RSA 密钥生成成功" } else { Write-Error "密钥生成失败" return } } else { Write-Warning "未找到 OpenSSL,请手动生成密钥或安装 Git for Windows" Write-Info "可以在服务器上运行: cd /opt/license-server && npx ts-node scripts/generate-keys.ts" return } # 读取密钥 $privateKey = Get-Content "$keysDir/private.key" -Raw $publicKey = Get-Content "$keysDir/public.key" -Raw # 生成其他密钥 $jwtSecret = -join ((48..57) + (65..90) + (97..122) | Get-Random -Count 32 | ForEach-Object {[char]$_}) $aesKey = -join ((48..57) + (65..70) + (97..102) | Get-Random -Count 64 | ForEach-Object {[char]$_}) $adminPassword = -join ((48..57) + (65..90) + (97..122) | Get-Random -Count 18 | ForEach-Object {[char]$_}) # Step 2: 创建 .env 文件 Write-Info "创建环境配置文件..." $privateKeyEscaped = $privateKey.Replace("`r`n", "\n").Replace("`n", "\n") $envContent = @" # 许可证服务器环境变量 # 生成时间: $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') # 数据库连接(由 docker-compose 配置) DATABASE_URL=postgresql://license:license_secret@postgres:5432/license_db # RSA 私钥 RSA_PRIVATE_KEY="$privateKeyEscaped" # JWT 密钥 JWT_SECRET=$jwtSecret # AES 加密密钥 AES_SECRET_KEY=$aesKey # 管理员初始密码 ADMIN_INITIAL_PASSWORD=$adminPassword "@ $envContent | Out-File -FilePath "license-server/.env" -Encoding UTF8 -NoNewline Write-Success ".env 文件已创建" # Step 3: 上传所有文件 Write-Info "上传项目文件..." # 创建远程目录 ssh -i $Config.SshKey "root@$($Config.Server)" "mkdir -p /opt/license-server" # 上传文件 scp -i $Config.SshKey -r "license-server/*" "root@$($Config.Server):/opt/license-server/" if ($LASTEXITCODE -ne 0) { Write-Error "文件上传失败" return } Write-Success "文件上传完成" # Step 4: 构建和启动 Write-Info "构建和启动服务..." $deployCmd = @" cd /opt/license-server && \ docker compose down 2>/dev/null || true && \ docker compose build --no-cache && \ docker compose up -d && \ echo '等待服务启动...' && \ sleep 20 && \ docker compose exec -T license-server npx prisma db push && \ echo '初始化管理员账号...' && \ docker compose exec -T license-server node -e " const { PrismaClient } = require('@prisma/client'); const bcrypt = require('bcryptjs'); const prisma = new PrismaClient(); const initialPassword = '$adminPassword'; async function init() { const existing = await prisma.admin.findFirst(); if (!existing) { const hash = await bcrypt.hash(initialPassword, 10); await prisma.admin.create({ data: { username: 'admin', password: hash, role: 'SUPER_ADMIN' } }); console.log('管理员账号已创建: admin / ' + initialPassword); } else { console.log('管理员账号已存在'); } } init().catch(console.error).finally(() => prisma.\$disconnect()); " "@ ssh -i $Config.SshKey "root@$($Config.Server)" $deployCmd if ($LASTEXITCODE -eq 0) { Write-Success "许可证服务器部署完成!" Write-Host "" Write-Color "═══════════════════════════════════════════════════════════" "Cyan" Write-Color " 部署信息" "Cyan" Write-Color "═══════════════════════════════════════════════════════════" "Cyan" Write-Info "访问地址: http://$($Config.Server):3001" Write-Info "管理后台: http://$($Config.Server):3001/admin" Write-Info "初始账号: admin / $adminPassword" Write-Host "" Write-Color "⚠️ 重要:请将以下公钥更新到素材系统" "Yellow" Write-Color " 文件: lib/license/crypto-service.ts" "Yellow" Write-Host "" Write-Color "─────────────────────────────────────────────────────────────" "Gray" Write-Host $publicKey Write-Color "─────────────────────────────────────────────────────────────" "Gray" Write-Host "" Write-Info "公钥已保存到: license-server/keys/public.key" } else { Write-Error "部署失败,请检查日志" Write-Info "查看日志: ssh -i $($Config.SshKey) root@$($Config.Server) 'docker compose -f /opt/license-server/docker-compose.yml logs'" } } # 6. 查看版本和镜像状态 function Show-Status { Write-Title "版本和镜像状态" Write-Color "=== 本地版本 ===" "Yellow" Write-Info "素材系统版本: $(Get-CurrentVersion)" Write-Info "Agent 版本: $(Get-AgentVersion)" Write-Host "" Write-Color "=== Harbor 项目结构 ===" "Yellow" Write-Info "flnxi-public (公开): Agent、runtime 依赖" Write-Info "flnxi (公开): 素材系统镜像" Write-Host "" Write-Color "=== Harbor 镜像 ===" "Yellow" Write-Info "检查 Harbor 镜像..." # 检查 Harbor 登录状态 $loginCheck = docker login $Config.HarborHost 2>&1 if ($LASTEXITCODE -eq 0) { Write-Success "Harbor 已登录" # 列出私有项目镜像 Write-Host "" Write-Color "flnxi/ (公开 - 素材系统):" "White" docker images "$($Config.HarborHost)/$($Config.HarborProject)/*" --format " {{.Repository}}:{{.Tag}}\t{{.CreatedSince}}" 2>$null # 列出公开项目镜像 Write-Host "" Write-Color "flnxi-public/agent (公开 - Agent):" "White" docker images "$($Config.HarborHost)/$($Config.HarborPublicProject)/agent*" --format " {{.Repository}}:{{.Tag}}\t{{.CreatedSince}}" 2>$null Write-Host "" Write-Color "flnxi-public/runtime (公开 - 依赖镜像):" "White" docker images "$($Config.HarborHost)/$($Config.HarborPublicProject)/runtime/*" --format " {{.Repository}}:{{.Tag}}\t{{.CreatedSince}}" 2>$null } else { Write-Warning "未登录 Harbor,请先执行: docker login $($Config.HarborHost)" } Write-Host "" Write-Color "=== 服务器状态 ===" "Yellow" Write-Info "检查服务器 $($Config.Server)..." $statusCmd = "docker ps --format 'table {{.Names}}\t{{.Status}}\t{{.Ports}}' | grep -E 'flnxi|license' || echo '无运行中的服务'" ssh -i $Config.SshKey "root@$($Config.Server)" $statusCmd 2>$null } # ==================== 主程序 ==================== if ($Help) { Write-Color "╔═══════════════════════════════════════════════════════════╗" "Cyan" Write-Color "║ FLNXI 素材系统 - 历史内部发布工具 v2.0 ║" "Cyan" Write-Color "╠═══════════════════════════════════════════════════════════╣" "Cyan" Write-Color "║ 用法: ║" "White" Write-Color "║ .\deploy-registry.ps1 当前版本化发布入口 ║" "Gray" Write-Color "║ .\发布管理.ps1 -AcknowledgeLegacyInternalPublish ║" "Gray" Write-Color "║ 仅用于历史链路排查 ║" "Gray" Write-Color "║ .\发布管理.ps1 -Help 显示此帮助 ║" "Gray" Write-Color "╠═══════════════════════════════════════════════════════════╣" "Cyan" Write-Color "║ 说明: ║" "White" Write-Color "║ 该脚本包含旧服务器、旧 Harbor、latest 和内部清理逻辑, ║" "Gray" Write-Color "║ 不作为客户安装、升级或正式发布入口。 ║" "Gray" Write-Color "╠═══════════════════════════════════════════════════════════╣" "Cyan" Write-Color "║ 当前入口: deploy-registry.ps1 + docker-compose.prod.yml ║" "Green" Write-Color "╚═══════════════════════════════════════════════════════════╝" "Cyan" exit 0 } # 7. 清理本地旧镜像 function Menu-CleanLocal { Write-Title "清理本地旧镜像" $keepCount = Read-Host "保留最新几个镜像? (默认: 5)" if ([string]::IsNullOrWhiteSpace($keepCount)) { $keepCount = 5 } Clean-LocalImages -ImagePattern "flnxi" -KeepCount ([int]$keepCount) Complete-Stage "本地镜像清理" } # 8. 清理服务器旧镜像 function Menu-CleanRemote { Write-Title "清理服务器旧镜像" # 先测试 SSH 连接 if (-not (Test-SSHConnection)) { return } $keepCount = Read-Host "保留最新几个镜像? (默认: 5)" if ([string]::IsNullOrWhiteSpace($keepCount)) { $keepCount = 5 } Clean-RemoteImages -KeepCount ([int]$keepCount) Complete-Stage "服务器镜像清理" } # 9. 深度清理 Docker(释放磁盘空间) function Menu-DeepClean { Write-Title "深度清理 Docker (释放磁盘空间)" Write-Warning "此操作将清理:" Write-Info " - 所有未使用的镜像" Write-Info " - 所有停止的容器" Write-Info " - 所有未使用的网络" Write-Info " - 所有构建缓存" Write-Host "" # 显示当前磁盘使用 Write-Color "=== 清理前 ===" "Yellow" docker system df Write-Host "" if (-not (Confirm-Action "确认执行深度清理?" "这将删除所有未使用的 Docker 资源!")) { Write-Warning "取消清理" return } Write-Info "正在清理..." # 清理所有未使用的资源 docker system prune -a -f --volumes # 清理构建缓存 docker builder prune -a -f Write-Host "" Write-Color "=== 清理后 ===" "Yellow" docker system df Write-Host "" Write-Success "深度清理完成!" Write-Info "提示: 如果 vhdx 文件仍然很大,可以在 Docker Desktop 设置中" Write-Info " 选择 'Troubleshoot' -> 'Clean / Purge data' 来压缩磁盘" Complete-Stage "深度清理" } # 主循环 while ($true) { Show-Menu $choice = Read-Host "请选择操作" switch ($choice) { "1" { Deploy-TestVersion } "2" { Publish-ReleaseVersion } "3" { Publish-Agent } "4" { Sync-RuntimeImages } "5" { Deploy-LicenseServer } "6" { Show-Status } "7" { Menu-CleanLocal } "8" { Menu-CleanRemote } "9" { Menu-DeepClean } "0" { Write-Color "再见!$(Get-FunTip)" "Cyan" exit 0 } default { Write-Warning "无效选项,请重新选择" } } Write-Host "" Read-Host "按 Enter 键继续..." }