flnxi-Wu/发布管理.ps1
WuYongJie 1d03717ff9 refactor: 更新Harbor项目结构为flnxi-public(公开)+flnxi(私有)
- docker-compose.prod.yml: runtime镜像改为从flnxi-public拉取
- 发布管理.ps1: Agent推送到flnxi-public公开项目
- Runtime镜像同步到flnxi-public项目
- 素材系统保持在flnxi私有项目
2025-12-15 21:29:49 +08:00

1031 lines
39 KiB
PowerShell
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# ================================================================
# 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 键继续..."
}