- docker-compose.prod.yml: runtime镜像改为从flnxi-public拉取 - 发布管理.ps1: Agent推送到flnxi-public公开项目 - Runtime镜像同步到flnxi-public项目 - 素材系统保持在flnxi私有项目
1031 lines
39 KiB
PowerShell
1031 lines
39 KiB
PowerShell
# ================================================================
|
||
# FLNXI 素材系统 - 发布管理工具 v2.0
|
||
# 功能:统一管理测试版部署、正式版发布、Agent 更新等操作
|
||
# 特性:进度条显示、健康检查、智能重试、趣味提示
|
||
# ================================================================
|
||
|
||
param(
|
||
[switch]$Help
|
||
)
|
||
|
||
$ErrorActionPreference = "Stop"
|
||
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
||
|
||
# 配置
|
||
$Config = @{
|
||
Server = "45.249.208.118"
|
||
SshKey = "$env:USERPROFILE\.ssh\youyidian_prod_ed25519"
|
||
HarborHost = "harbor.flnxi.com"
|
||
HarborProject = "flnxi" # 私有项目(素材系统)
|
||
HarborPublicProject = "flnxi-public" # 公开项目(Agent、runtime)
|
||
DockerHubUser = "flnxi"
|
||
HealthCheckPort = 4320
|
||
HealthCheckTimeout = 45
|
||
SshRetryCount = 3
|
||
SshRetryDelay = 2
|
||
}
|
||
|
||
# 趣味提示
|
||
$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 {
|
||
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"
|
||
|
||
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"
|
||
|
||
# 构建正式版镜像
|
||
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 安装程序
|
||
function Publish-Agent {
|
||
Write-Title "发布 Agent 安装程序"
|
||
|
||
$currentVersion = Get-AgentVersion
|
||
Write-Info "当前 Agent 版本: $currentVersion"
|
||
|
||
# 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"
|
||
|
||
# 更新版本文件
|
||
$newVersion | Out-File -FilePath "flnxi-agent/VERSION.txt" -Encoding UTF8 -NoNewline
|
||
Write-Success "Agent 版本文件已更新: $newVersion"
|
||
|
||
# 启用 BuildKit
|
||
$env:DOCKER_BUILDKIT = "1"
|
||
|
||
# 构建 Agent 镜像
|
||
Write-Info "构建 Agent 镜像 (BuildKit 缓存已启用)..."
|
||
Push-Location "flnxi-agent"
|
||
|
||
try {
|
||
$buildArgs = @(
|
||
"build",
|
||
"--progress=plain",
|
||
"-t", "${harborRepo}:${newVersion}",
|
||
"-t", "${harborRepo}:latest",
|
||
"--build-arg", "AGENT_VERSION=$newVersion",
|
||
"."
|
||
)
|
||
|
||
docker @buildArgs
|
||
if ($LASTEXITCODE -ne 0) {
|
||
Write-Error "构建失败"
|
||
return
|
||
}
|
||
Write-Success "构建完成"
|
||
|
||
# 推送到 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 "客户安装命令:"
|
||
Write-Color " docker run -d -p 3080:3080 -v /var/run/docker.sock:/var/run/docker.sock ${harborRepo}:latest" "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]$_})
|
||
|
||
# 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=admin123
|
||
"@
|
||
|
||
$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();
|
||
async function init() {
|
||
const existing = await prisma.admin.findFirst();
|
||
if (!existing) {
|
||
const hash = await bcrypt.hash('admin123', 10);
|
||
await prisma.admin.create({ data: { username: 'admin', password: hash, role: 'SUPER_ADMIN' } });
|
||
console.log('管理员账号已创建: admin / admin123');
|
||
} 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 / admin123"
|
||
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 "║ .\发布管理.ps1 启动交互式菜单 ║" "Gray"
|
||
Write-Color "║ .\发布管理.ps1 -Help 显示此帮助 ║" "Gray"
|
||
Write-Color "╠═══════════════════════════════════════════════════════════╣" "Cyan"
|
||
Write-Color "║ 功能: ║" "White"
|
||
Write-Color "║ [1] 构建并部署测试版 (含健康检查) ║" "White"
|
||
Write-Color "║ [2] 发布素材系统正式版 (需确认) ⚠️ ║" "Yellow"
|
||
Write-Color "║ [3] 发布 Agent 安装程序 (需确认) ⚠️ ║" "Yellow"
|
||
Write-Color "║ [4] 同步 Runtime 镜像 (国内镜像源加速) ║" "White"
|
||
Write-Color "║ [5] 部署许可证服务器 (需确认) ⚠️ ║" "Yellow"
|
||
Write-Color "║ [6] 查看版本和镜像状态 ║" "White"
|
||
Write-Color "║ [7] 清理本地旧镜像 ║" "White"
|
||
Write-Color "║ [8] 清理服务器旧镜像 ║" "White"
|
||
Write-Color "║ [9] 深度清理 Docker (释放磁盘空间) ║" "Red"
|
||
Write-Color "╠═══════════════════════════════════════════════════════════╣" "Cyan"
|
||
Write-Color "║ 特性: ║" "White"
|
||
Write-Color "║ ✓ 进度条显示 ✓ 健康检查 ✓ SSH智能重试 ║" "Green"
|
||
Write-Color "║ ✓ 国内镜像源加速 ✓ 趣味提示 ✓ 镜像清理 ║" "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 键继续..."
|
||
}
|