收紧私有化部署配置与旧发布入口

This commit is contained in:
WuYongJie 2026-05-25 16:53:50 +08:00
parent c759d0a37b
commit 1f46544991
16 changed files with 909 additions and 436 deletions

View File

@ -1,52 +1,10 @@
node_modules
.git
.gitignore
.env
.env.*
.DS_Store
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
# Next.js build artifacts (we will copy from builder stage explicitly)
.next/cache
# Exclude all docs and scripts from production image
docs/**
scripts/**
# 但保留 docker-entrypoint.sh
!scripts/docker-entrypoint.sh
# Exclude local uploads and temp files
uploads/**
tmp/**
temp/**
# Exclude miscellaneous local files
*.log
*.md
!README.md
# 允许权威部署规范被查看
!docs/PORTABLE_DEPLOYMENT_GUIDE.md
*.bat
*.ps1
*.sh
# VCS/IDE
.vscode/**
.idea/**
*.swp
# ============================================ # ============================================
# Docker 构建排除文件配置 # Docker 构建排除文件配置
# 只包含功能性代码和必要文件 # 优化构建上下文大小,加速构建
# ============================================ # ============================================
# 依赖包(在 Docker 中重新安装) # 依赖包(在 Docker 中重新安装)
node_modules node_modules
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnp .pnp
.pnp.js .pnp.js
@ -56,153 +14,88 @@ out
build build
dist dist
# 测试 # 测试相关
coverage coverage
*.test.ts **/*.test.ts
*.test.tsx **/*.test.tsx
*.spec.ts **/*.spec.ts
*.spec.tsx **/*.spec.tsx
test
tests
__tests__ __tests__
# 调试和日志 # 日志文件
*.log *.log
logs logs
*.log.*
npm-debug.log* npm-debug.log*
yarn-debug.log* yarn-debug.log*
yarn-error.log* yarn-error.log*
# 本地环境变量(服务器上已有 .env) # 本地环境变量(不要打包到镜像中,运行时通过 Docker 环境变量配置)
# 注意:.env.production 需要在构建时使用,所以不排除
.env.local
.env.development.local
.env.test.local
.env.production.local
.env .env
.env.*
.env.local
.env.*.local
.env.example .env.example
.env.template .env.production
.env*.local .env.production.local
# 但排除其他敏感文件 env-backups
server.env config-bianliang.env
env-backups/
# 版本控制 # 版本控制
.git .git
.gitignore .gitignore
.gitattributes .gitattributes
.gitmodules
# IDE 和编辑器配置 # IDE 配置
.vscode .vscode
.idea .idea
.cursor .cursor
.trae
.kiro
*.swp *.swp
*.swo *.swo
*.sublime-project
*.sublime-workspace
.editorconfig
.eslintcache
# 文档文件(不部署文档) # 文档(保留 VERSION.txt)
docs docs
*.md *.md
README.md
LICENSE
CHANGELOG.md
CONTRIBUTING.md
*.txt
!VERSION.txt !VERSION.txt
# 脚本文件(不部署脚本) # 脚本(保留 docker-entrypoint.sh)
scripts scripts
!scripts/docker-entrypoint.sh !scripts/docker-entrypoint.sh
*.sh
!scripts/docker-entrypoint.sh
*.ps1
*.bat
*.py
*.js
!next.config.js
!postcss.config.js
!tailwind.config.ts
# 临时文件和缓存 # 临时文件
uploads uploads
public/uploads public/uploads
backups backups
*.tmp *.tmp
*.temp *.temp
.cache .cache
.tmp
tmp tmp
temp temp
*.cache
.DS_Store .DS_Store
Thumbs.db Thumbs.db
# 压缩包和发布包 # 压缩包和部署产物
*.tar.gz *.tar.gz
*.zip *.zip
*.rar
*.7z
*.tar *.tar
*.tgz *.bak-*
release-* *.ps1.bak-*
dist-*
deploy-artifacts deploy-artifacts
docker-images
gongxiangp2-source-reference-20260309-202917
gongxiangp2-source-reference-20260413-172354
gongxiangp2-source-reference-old-style-20260513-132942
license-server
flnxi-agent
flnxi-agent-go
# 数据库备份文件 # 数据库文件
*.sql *.sql
*.db *.db
*.sqlite *.sqlite
*.sqlite3
# 其他文件 # 其他
*.rpm
*.deb
*.pem *.pem
*.key *.key
*.cert
*.crt
frp_* frp_*
docker-daemon-config.json
# 部署相关文件
deploy.sh
auto-deploy.sh
update-deploy.sh
deploy-to-server.sh
deploy-to-server.ps1
一键部署.bat
部署前检查清单.bat
安全部署.ps1
安全部署.sh
# 修复和工具脚本
修复PowerShell编码.ps1
快速修复编码.bat
*.修复*.bat
*.修复*.ps1
*.修复*.sh
# 配置文件(保留必要的,排除开发用的)
# 注意:以下文件会被包含(功能性代码)
# - package.json
# - package-lock.json
# - Dockerfile
# - docker-compose.yml
# - docker-compose.prod.yml
# - next.config.js
# - tsconfig.json
# - tailwind.config.ts
# - postcss.config.js
# - prisma/schema.prisma
# - prisma/migrations/ (迁移文件是功能性的)
# - app/
# - components/
# - lib/
# - types/
# - middleware.ts
# - public/ (静态资源)

View File

@ -2,14 +2,39 @@
NODE_ENV=development NODE_ENV=development
# Public URLs # Public URLs
SERVER_IP=192.168.x.x
APP_PUBLIC_URL=https://your-domain.example.com APP_PUBLIC_URL=https://your-domain.example.com
NEXTAUTH_URL=https://your-domain.example.com NEXTAUTH_URL=https://your-domain.example.com
NEXTAUTH_SECRET=replace-with-strong-secret NEXTAUTH_SECRET=replace-with-strong-secret
LICENSE_SERVER_URL=https://your-license-server.example.com LICENSE_SERVER_URL=https://your-license-server.example.com
LICENSE_PROJECT_CODE=flnxi-sucai LICENSE_PROJECT_CODE=flnxi-sucai
# Versioned deployment images
# Customer delivery must pin the full application image tag. Do not rely on latest.
APP_IMAGE=registry.example.com/flnxi/material/flnxi-material:1.0.0
# Optional: pin runtime images to your private Registry for offline or controlled delivery.
POSTGRES_IMAGE=postgres:15-alpine
MINIO_IMAGE=minio/minio:RELEASE.2025-04-22T22-12-26Z
MINIO_MC_IMAGE=minio/mc:RELEASE.2025-04-16T18-13-26Z
# Shared-machine isolation (recommended on test servers with multiple projects)
COMPOSE_PROJECT_NAME=flnxi_sucai
APP_CONTAINER_NAME=flnxi-material
POSTGRES_CONTAINER_NAME=flnxi-postgres
MINIO_CONTAINER_NAME=flnxi-minio
MINIO_INIT_CONTAINER_NAME=flnxi-minio-init
POSTGRES_VOLUME_NAME=flnxi-postgres-data
MINIO_VOLUME_NAME=flnxi-minio-data
APP_NETWORK_NAME=flnxi-app-network
APP_PORT=4320
MINIO_API_PORT=9000
MINIO_CONSOLE_PORT=9001
# Database settings # Database settings
DATABASE_URL=postgresql://username:password@host:5432/database DATABASE_URL=postgresql://username:password@host:5432/database
POSTGRES_USER=postgres
POSTGRES_PASSWORD=replace-with-strong-postgres-password
POSTGRES_DB=flnxi
# Object storage (S3 compatible) # Object storage (S3 compatible)
STORAGE_PROVIDER=s3 STORAGE_PROVIDER=s3
@ -19,8 +44,11 @@ S3_ACCESS_KEY_ID=your-access-key
S3_SECRET_ACCESS_KEY=your-secret-key S3_SECRET_ACCESS_KEY=your-secret-key
S3_REGION=us-east-1 S3_REGION=us-east-1
S3_PUBLIC_DOMAIN=https://s3.example.com/your-bucket S3_PUBLIC_DOMAIN=https://s3.example.com/your-bucket
S3_SET_ACL=0
S3_FILE_PATH=materials/ S3_FILE_PATH=materials/
S3_ENV_PREFIX= S3_ENV_PREFIX=
MINIO_ROOT_USER=replace-with-minio-admin-user
MINIO_ROOT_PASSWORD=replace-with-strong-minio-admin-password
# Vector database (optional) # Vector database (optional)
VECTOR_DB=pgvector VECTOR_DB=pgvector

View File

@ -1,20 +0,0 @@
DATABASE_URL=postgresql://gongxiangpan:ytPWwGYRptMmKezb@45.249.208.118:5432/gongxiangpan
DATABASE_DRIVER=node
# NextAuth
NEXTAUTH_SECRET=HzJhmmDRx8OS+ipZE/ersdsod/vk1FyoaLIhQSHygFk=
NEXTAUTH_URL=http://45.249.208.118:4320
APP_PUBLIC_URL=http://45.249.208.118:4320
# MinIO S3 Storage
S3_ENDPOINT=https://s3.1080p.hk
S3_BUCKET=jsxiangche
S3_ACCESS_KEY_ID=AEvT1p0wSNjow3Asu9NV
S3_SECRET_ACCESS_KEY=u8nzFl1S6dNoT5PQaQhkHz8Du6iX0hVnPswtvHhs
S3_REGION=us-east-1
S3_PUBLIC_DOMAIN=https://s3.1080p.hk/jsxiangche
S3_ENABLE_PATH_STYLE=1
S3_FILE_PATH=由壹点AI/
# Local storage for zip files
UPLOAD_DIR=./uploads

View File

@ -158,7 +158,7 @@ RUN if [ ! -d "node_modules/.prisma" ]; then \
fi fi
# 构建 Next.js 应用(standalone 模式) # 构建 Next.js 应用(standalone 模式)
# 允许构建时出现预渲染错误(这些页面会在运行时动态渲染) # 商业交付镜像必须在构建阶段暴露失败,不能吞掉构建错误继续出包。
RUN set -ex && \ RUN set -ex && \
if [ -f "${PRISMA_ENGINE_CACHE_PATH}" ]; then \ if [ -f "${PRISMA_ENGINE_CACHE_PATH}" ]; then \
export PRISMA_QUERY_ENGINE_LIBRARY="${PRISMA_ENGINE_CACHE_PATH}" && \ export PRISMA_QUERY_ENGINE_LIBRARY="${PRISMA_ENGINE_CACHE_PATH}" && \
@ -172,7 +172,7 @@ RUN if [ ! -d "node_modules/.prisma" ]; then \
else \ else \
unset PRISMA_SCHEMA_ENGINE_BINARY; \ unset PRISMA_SCHEMA_ENGINE_BINARY; \
fi && \ fi && \
npm run build || (echo "构建完成,但部分页面预渲染失败(这是预期的,这些页面将在运行时动态渲染)" && true) npm run build
# 验证构建产物(允许 ESLint 警告,但不允许致命错误) # 验证构建产物(允许 ESLint 警告,但不允许致命错误)
RUN if [ ! -f ".next/standalone/server.js" ]; then \ RUN if [ ! -f ".next/standalone/server.js" ]; then \

View File

@ -0,0 +1,22 @@
import fs from 'fs'
import path from 'path'
describe('docker-compose.prod.yml', () => {
const compose = fs.readFileSync(
path.join(process.cwd(), 'docker-compose.prod.yml'),
'utf8'
)
it('MinIO 初始化默认关闭素材桶匿名访问', () => {
expect(compose).toContain('S3_SET_ACL: ${S3_SET_ACL:-0}')
expect(compose).toContain('S3_PUBLIC_READ: ${S3_PUBLIC_READ:-0}')
expect(compose).toContain('mc anonymous set none "myminio/$${S3_BUCKET}"')
expect(compose).toContain('MinIO bucket anonymous access disabled')
})
it('只有显式兼容公开模式时才打开匿名下载', () => {
expect(compose).toContain('WARNING: S3 public read compatibility is enabled')
expect(compose).toContain('mc anonymous set download "myminio/$${S3_BUCKET}"')
expect(compose).toContain('- S3_SET_ACL=${S3_SET_ACL:-0}')
})
})

View File

@ -61,7 +61,7 @@ NEXT_PUBLIC_DINGTALK_CALLBACK_URL=
# 图片远程域名白名单(Next/Image) # 图片远程域名白名单(Next/Image)
# ----------------------------- # -----------------------------
# 用于 next.config.js 中的 remotePatterns。逗号分隔主机名; # 用于 next.config.js 中的 remotePatterns。逗号分隔主机名;
# 默认已包含 s3.1080p.hk,如使用自有域名请在此追加。 # 如使用自有对象存储或 CDN 域名,请在此追加。
# 例如:cdn.company.com,images.company.com # 例如:cdn.company.com,images.company.com
IMAGE_REMOTE_HOSTS= IMAGE_REMOTE_HOSTS=

View File

@ -1,23 +1,23 @@
# ================================================================ # ================================================================
# FLNXI素材库 - 部署脚本 v1.2 # FLNXI素材库 - 历史内部部署脚本 v1.2
# 功能:本地构建 -> 推送到Registry -> 远程部署 # 状态:已废弃,不作为客户安装、升级或正式发布入口
# 特性:自动加载基础镜像、智能重试、镜像清理、趣味提示 # 当前正式发布入口请使用 deploy-registry.ps1
# ================================================================ # ================================================================
param( param(
[string]$Server = "45.249.208.118", [string]$Server = "",
[string]$Username = "root", [string]$Username = "root",
[SecureString]$Password = $null, [SecureString]$Password = $null,
[string]$KeyPath = "", [string]$KeyPath = "",
[string]$ImageName = "flnxi-material", [string]$ImageName = "flnxi-material",
[string]$Tag = "pro-latest", [string]$Tag = "",
[string]$RemotePath = "/root/deploy", [string]$RemotePath = "/root/deploy",
[string]$EnvFile = "", [string]$EnvFile = "",
[string]$Registry = "harbor", # 默认使用 Harbor [string]$Registry = "harbor", # 默认使用 Harbor
[string]$HarborUrl = "harbor.flnxi.com", [string]$HarborUrl = "",
[string]$HarborProjectName = "flnxi", # Harbor 项目名称,例如: flnxi, sucai, demo, library [string]$HarborProjectName = "flnxi", # Harbor 项目名称,例如: flnxi, sucai, demo, library
[string]$HarborUsername = "admin", [string]$HarborUsername = "admin",
[string]$HarborPassword = "Harbor12345", [string]$HarborPassword = "",
[switch]$SkipCheck, [switch]$SkipCheck,
[switch]$SkipBuild, [switch]$SkipBuild,
[switch]$SkipUpload, [switch]$SkipUpload,
@ -26,7 +26,8 @@ param(
[switch]$CleanImages, [switch]$CleanImages,
[int]$KeepImageCount = 5, [int]$KeepImageCount = 5,
[switch]$CleanDockerDesktop = $true, # 默认启用清理 Docker Desktop 未使用资源 [switch]$CleanDockerDesktop = $true, # 默认启用清理 Docker Desktop 未使用资源
[switch]$NoCache # 禁用 Docker 构建缓存(默认使用缓存加速构建) [switch]$NoCache, # 禁用 Docker 构建缓存(默认使用缓存加速构建)
[switch]$AcknowledgeLegacyInternalDeploy
) )
# 脚本配置 # 脚本配置
@ -41,6 +42,13 @@ try {
# 控制台编码设置失败时忽略 # 控制台编码设置失败时忽略
} }
if (-not $Help -and -not $AcknowledgeLegacyInternalDeploy) {
Write-Host "[ERROR] deploy-new-fixed.ps1 是历史内部部署脚本,默认禁止执行。" -ForegroundColor Red
Write-Host "当前客户/测试发布请使用 deploy-registry.ps1。" -ForegroundColor Yellow
Write-Host "如确需排查历史链路,请补充 -AcknowledgeLegacyInternalDeploy,并显式传入 -Server、-Tag、-HarborUrl 和 Registry 凭据。" -ForegroundColor Yellow
exit 1
}
$script:StageTracker = @{ $script:StageTracker = @{
Total = 0 Total = 0
Current = 0 Current = 0
@ -173,21 +181,24 @@ if ($Help) {
Write-Host $Message -ForegroundColor $ForegroundColor Write-Host $Message -ForegroundColor $ForegroundColor
} }
Write-ColorOutput "新建简化部署脚本" "Green" Write-ColorOutput "历史内部部署脚本(已废弃)" "Yellow"
Write-ColorOutput "========================================" "Green" Write-ColorOutput "========================================" "Green"
Write-ColorOutput "此脚本不作为客户安装、升级或正式发布入口;当前请使用 deploy-registry.ps1。" "Yellow"
Write-ColorOutput "默认禁止执行,确需排查历史链路时必须追加 -AcknowledgeLegacyInternalDeploy。" "Yellow"
Write-ColorOutput "" "White"
Write-ColorOutput "用法:" "White" Write-ColorOutput "用法:" "White"
Write-ColorOutput " .\deploy-new-fixed.ps1 [参数]" "Yellow" Write-ColorOutput " .\deploy-new-fixed.ps1 -AcknowledgeLegacyInternalDeploy [参数]" "Yellow"
Write-ColorOutput "" "White" Write-ColorOutput "" "White"
Write-ColorOutput "参数:" "White" Write-ColorOutput "参数:" "White"
Write-ColorOutput " -Server [地址] 服务器地址 (默认: 45.249.208.118)" "Gray" Write-ColorOutput " -Server [地址] 服务器地址,必须显式传入" "Gray"
Write-ColorOutput " -Username [用户] SSH用户名 (默认: root)" "Gray" Write-ColorOutput " -Username [用户] SSH用户名 (默认: root)" "Gray"
Write-ColorOutput " -KeyPath [路径] SSH私钥路径 (推荐)" "Gray" Write-ColorOutput " -KeyPath [路径] SSH私钥路径 (推荐)" "Gray"
Write-ColorOutput " -EnvFile [路径] 指定构建期使用的环境变量文件" "Gray" Write-ColorOutput " -EnvFile [路径] 指定构建期使用的环境变量文件" "Gray"
Write-ColorOutput " -Registry [类型] Registry类型: default 或 harbor (默认: default)" "Gray" Write-ColorOutput " -Registry [类型] Registry类型: default 或 harbor (默认: default)" "Gray"
Write-ColorOutput " -HarborUrl [地址] Harbor地址 (默认: harbor.flnxi.com)" "Gray" Write-ColorOutput " -HarborUrl [地址] Harbor地址,必须显式传入" "Gray"
Write-ColorOutput " -HarborProjectName [名称] Harbor项目名称 (默认: flnxi)" "Gray" Write-ColorOutput " -HarborProjectName [名称] Harbor项目名称 (默认: flnxi)" "Gray"
Write-ColorOutput " -HarborUsername [用户] Harbor用户名 (默认: admin)" "Gray" Write-ColorOutput " -HarborUsername [用户] Harbor用户名 (默认: admin)" "Gray"
Write-ColorOutput " -HarborPassword [密码] Harbor密码 (默认: Harbor12345)" "Gray" Write-ColorOutput " -HarborPassword [密码] Harbor密码,必须显式传入" "Gray"
Write-ColorOutput " -SkipCheck 跳过代码检查" "Gray" Write-ColorOutput " -SkipCheck 跳过代码检查" "Gray"
Write-ColorOutput " -SkipBuild 跳过Docker构建" "Gray" Write-ColorOutput " -SkipBuild 跳过Docker构建" "Gray"
Write-ColorOutput " -SkipUpload 跳过镜像推送和远程部署" "Gray" Write-ColorOutput " -SkipUpload 跳过镜像推送和远程部署" "Gray"
@ -199,11 +210,7 @@ if ($Help) {
Write-ColorOutput " -Help 显示此帮助信息" "Gray" Write-ColorOutput " -Help 显示此帮助信息" "Gray"
Write-ColorOutput "" "White" Write-ColorOutput "" "White"
Write-ColorOutput "示例:" "White" Write-ColorOutput "示例:" "White"
Write-ColorOutput " .\deploy-new-fixed.ps1" "Yellow" Write-ColorOutput " .\deploy-new-fixed.ps1 -AcknowledgeLegacyInternalDeploy -Server 192.168.1.20 -Tag 1.0.1 -HarborUrl registry.example.com" "Yellow"
Write-ColorOutput " .\deploy-new-fixed.ps1 -KeyPath 'C:\Users\YourUsername\.ssh\youyidian_prod_ed25519'" "Yellow"
Write-ColorOutput " .\deploy-new-fixed.ps1 -Registry harbor" "Yellow"
Write-ColorOutput " .\deploy-new-fixed.ps1 -SkipCheck -SkipUpload" "Yellow"
Write-ColorOutput " .\deploy-new-fixed.ps1 -CleanImages -KeepImageCount 5" "Yellow"
exit 0 exit 0
} }
@ -322,14 +329,14 @@ APP_PUBLIC_URL=http://$Server:4320
NEXTAUTH_SECRET=$(New-Guid) NEXTAUTH_SECRET=$(New-Guid)
# 存储配置 # 存储配置
S3_ENDPOINT=https://s3.1080p.hk S3_ENDPOINT=https://s3.example.com
S3_BUCKET=jsxiangche S3_BUCKET=placeholder-bucket
S3_ACCESS_KEY_ID=AEvT1p0wSNjow3Asu9NV S3_ACCESS_KEY_ID=placeholder-access-key
S3_SECRET_ACCESS_KEY=u8nzFl1S6dNoT5PQaQhkHz8Du6iX0hVnPswtvHhs S3_SECRET_ACCESS_KEY=placeholder-secret-key
S3_REGION=us-east-1 S3_REGION=us-east-1
S3_PUBLIC_DOMAIN=https://s3.1080p.hk/jsxiangche S3_PUBLIC_DOMAIN=https://s3.example.com/placeholder-bucket
S3_ENABLE_PATH_STYLE=1 S3_ENABLE_PATH_STYLE=1
S3_FILE_PATH=由壹点AI/ S3_FILE_PATH=materials/
# 本地上传目录 # 本地上传目录
UPLOAD_DIR=./uploads UPLOAD_DIR=./uploads
@ -414,6 +421,91 @@ function Get-TempFileName {
return "${Prefix}_${timestamp}_${processId}_${random}.txt" return "${Prefix}_${timestamp}_${processId}_${random}.txt"
} }
function ConvertTo-PlainText {
param([SecureString]$SecureValue)
if (-not $SecureValue) {
return ""
}
$credential = New-Object System.Management.Automation.PSCredential("placeholder", $SecureValue)
return $credential.GetNetworkCredential().Password
}
function Get-PlinkPath {
try {
$cmd = Get-Command "plink.exe" -ErrorAction Stop
return $cmd.Source
} catch {
return $null
}
}
function Start-RemoteProcess {
param(
[string]$Server,
[string]$Username,
[SecureString]$Password = $null,
[string]$KeyPath = "",
[string]$Command,
[string]$StdoutFile = "",
[string]$StderrFile = "",
[int]$ConnectTimeoutSeconds = 30
)
$target = "${Username}@${Server}"
$startParams = @{
NoNewWindow = $true
PassThru = $true
Wait = $true
}
if (-not [string]::IsNullOrWhiteSpace($StdoutFile)) {
$startParams.RedirectStandardOutput = $StdoutFile
}
if (-not [string]::IsNullOrWhiteSpace($StderrFile)) {
$startParams.RedirectStandardError = $StderrFile
}
if ($KeyPath -and (Test-Path $KeyPath)) {
$sshArgs = @(
"-o", "BatchMode=yes",
"-o", "StrictHostKeyChecking=no",
"-o", "ConnectTimeout=$ConnectTimeoutSeconds",
"-o", "ServerAliveInterval=10",
"-o", "ServerAliveCountMax=3",
"-i", $KeyPath,
$target,
$Command
)
return Start-Process -FilePath "ssh" -ArgumentList $sshArgs @startParams
}
if ($Password) {
$plinkPath = Get-PlinkPath
if (-not $plinkPath) {
throw "未找到 plink.exe,无法使用密码执行非交互 SSH。"
}
$plainPassword = ConvertTo-PlainText -SecureValue $Password
$plinkArgs = @(
"-ssh",
"-batch",
"-pw", $plainPassword,
$target,
$Command
)
return Start-Process -FilePath $plinkPath -ArgumentList $plinkArgs @startParams
}
$sshArgs = @(
"-o", "StrictHostKeyChecking=no",
"-o", "ConnectTimeout=$ConnectTimeoutSeconds",
$target,
$Command
)
return Start-Process -FilePath "ssh" -ArgumentList $sshArgs @startParams
}
# 测试SSH连接(带重试机制) # 测试SSH连接(带重试机制)
function Test-SSHConnection { function Test-SSHConnection {
param( param(
@ -441,28 +533,29 @@ function Test-SSHConnection {
$stderrFile = Get-TempFileName -Prefix "ssh_stderr" $stderrFile = Get-TempFileName -Prefix "ssh_stderr"
try { try {
# 使用 OpenSSH(增加超时时间到20秒)
$sshArgs = @("-o", "BatchMode=yes", "-o", "ConnectTimeout=20", "-o", "ServerAliveInterval=5", "-o", "ServerAliveCountMax=3")
if ($KeyPath -and (Test-Path $KeyPath)) { if ($KeyPath -and (Test-Path $KeyPath)) {
$sshArgs += @("-i", $KeyPath)
if ($attempt -eq 1) { if ($attempt -eq 1) {
Write-Info "使用SSH密钥: $KeyPath" Write-Info "使用SSH密钥: $KeyPath"
} }
} elseif ($Password) { } elseif ($Password) {
# 注意:SecureString 密码认证需要额外处理,当前主要使用 SSH 密钥
$sshArgs += @("-o", "PasswordAuthentication=yes")
if ($attempt -eq 1) { if ($attempt -eq 1) {
Write-Warning "密码认证支持有限,建议使用 SSH 密钥" Write-Info "使用 PuTTY plink 进行密码认证"
} }
} else { } else {
if ($attempt -eq 1) { if ($attempt -eq 1) {
Write-Warning "未提供密钥,使用交互式认证" Write-Warning "未提供密钥,使用交互式认证"
} }
$sshArgs += @("-o", "StrictHostKeyChecking=no")
} }
$process = Start-Process -FilePath "ssh" -ArgumentList ($sshArgs + @($target, "echo 'SSH连接测试成功'")) -NoNewWindow -PassThru -Wait -RedirectStandardOutput $stdoutFile -RedirectStandardError $stderrFile $process = Start-RemoteProcess `
-Server $Server `
-Username $Username `
-Password $Password `
-KeyPath $KeyPath `
-Command "echo 'SSH连接测试成功'" `
-StdoutFile $stdoutFile `
-StderrFile $stderrFile `
-ConnectTimeoutSeconds 20
if ($process.ExitCode -eq 0) { if ($process.ExitCode -eq 0) {
Write-Success "SSH连接测试成功" Write-Success "SSH连接测试成功"
@ -543,6 +636,7 @@ function Clean-RemoteDockerImages {
param( param(
[string]$Server, [string]$Server,
[string]$Username, [string]$Username,
[SecureString]$Password = $null,
[string]$KeyPath, [string]$KeyPath,
[string]$ImageName, [string]$ImageName,
[int]$KeepCount = 3 [int]$KeepCount = 3
@ -562,14 +656,14 @@ echo '--- 清理后的镜像列表 ---'
docker images | grep '$ImageName' | head -10 docker images | grep '$ImageName' | head -10
"@ "@
$sshArgs = @("-o", "BatchMode=yes", "-o", "StrictHostKeyChecking=no", "-o", "ConnectTimeout=30")
if ($KeyPath -and (Test-Path $KeyPath)) {
$sshArgs += @("-i", $KeyPath)
}
try { try {
$sshArgs += @("${Username}@${Server}", $cleanCmd) $process = Start-RemoteProcess `
$process = Start-Process -FilePath "ssh" -ArgumentList $sshArgs -NoNewWindow -PassThru -Wait -Server $Server `
-Username $Username `
-Password $Password `
-KeyPath $KeyPath `
-Command $cleanCmd `
-ConnectTimeoutSeconds 30
if ($process.ExitCode -eq 0) { if ($process.ExitCode -eq 0) {
Write-Success "服务器镜像清理完成" Write-Success "服务器镜像清理完成"
return $true return $true
@ -1017,27 +1111,31 @@ function Main {
# 因为镜像已经在本地构建完成,所有层(包括基础镜像层)都已包含在镜像中 # 因为镜像已经在本地构建完成,所有层(包括基础镜像层)都已包含在镜像中
# 在服务器上拉取镜像并部署(带重试机制) # 在服务器上拉取镜像并部署(带重试机制)
Write-Info "在服务器上拉取镜像并部署..." Write-Info "在服务器上拉取镜像并部署..."
$composeCmd = "docker compose -f docker-compose.prod.yml"
# 如果使用 Harbor,需要在服务器上先登录 # 如果使用 Harbor,需要在服务器上先登录
if ($Registry -eq "harbor") { if ($Registry -eq "harbor") {
$harborLoginCmd = "docker login $HarborUrl -u $HarborUsername -p $HarborPassword" $harborLoginCmd = "docker login $HarborUrl -u $HarborUsername -p $HarborPassword"
$deployCmd = "cd $RemotePath && $harborLoginCmd && docker-compose stop app && docker-compose rm -f app && docker pull $registryImage && docker tag $registryImage $fullImage && docker-compose up -d --no-deps --pull never app" $deployCmd = "cd $RemotePath && $harborLoginCmd && $composeCmd stop app && $composeCmd rm -f app && docker pull $registryImage && docker tag $registryImage $fullImage && $composeCmd up -d --no-deps --pull never app"
} else { } else {
# 先停止并删除旧容器,然后拉取新镜像,重新启动(使用 --no-deps --pull never 避免拉取依赖服务和重复拉取) # 先停止并删除旧容器,然后拉取新镜像,重新启动(使用 --no-deps --pull never 避免拉取依赖服务和重复拉取)
$deployCmd = "cd $RemotePath && docker-compose stop app && docker-compose rm -f app && docker pull $registryImage && docker tag $registryImage $fullImage && docker-compose up -d --no-deps --pull never app" $deployCmd = "cd $RemotePath && $composeCmd stop app && $composeCmd rm -f app && docker pull $registryImage && docker tag $registryImage $fullImage && $composeCmd up -d --no-deps --pull never app"
} }
$sshArgs = @("-o", "BatchMode=yes", "-o", "StrictHostKeyChecking=no", "-o", "ConnectTimeout=30", "-o", "ServerAliveInterval=10", "-o", "ServerAliveCountMax=3") $remoteCommandAvailable = $false
if ($KeyPath -and (Test-Path $KeyPath)) { if ($KeyPath -and (Test-Path $KeyPath)) {
$sshArgs += @("-i", $KeyPath)
Write-Info "使用 OpenSSH 执行远程部署命令" Write-Info "使用 OpenSSH 执行远程部署命令"
$remoteCommandAvailable = $true
} elseif ($Password) { } elseif ($Password) {
Write-Warning "未提供 SSH 密钥,无法在非交互模式下使用密码。请手动执行部署命令。" if (Get-PlinkPath) {
$sshArgs = $null Write-Info "使用 PuTTY plink 执行远程部署命令"
$remoteCommandAvailable = $true
} else {
Write-Warning "本机未找到 plink.exe,无法使用密码自动部署。请手动执行部署命令。"
}
} else { } else {
Write-Warning "缺少 SSH 凭据,请手动执行部署命令。" Write-Warning "缺少 SSH 凭据,请手动执行部署命令。"
$sshArgs = $null
} }
if ($sshArgs) { if ($remoteCommandAvailable) {
$deployMaxRetries = 2 $deployMaxRetries = 2
$deployRetryDelay = 5 $deployRetryDelay = 5
$deployAttempt = 0 $deployAttempt = 0
@ -1050,9 +1148,14 @@ function Main {
Start-Sleep -Seconds $deployRetryDelay Start-Sleep -Seconds $deployRetryDelay
} }
$currentSshArgs = $sshArgs + @("${Username}@${Server}", $deployCmd)
try { try {
$sshProcess = Start-Process -FilePath "ssh" -ArgumentList $currentSshArgs -NoNewWindow -PassThru -Wait $sshProcess = Start-RemoteProcess `
-Server $Server `
-Username $Username `
-Password $Password `
-KeyPath $KeyPath `
-Command $deployCmd `
-ConnectTimeoutSeconds 30
if ($sshProcess.ExitCode -eq 0) { if ($sshProcess.ExitCode -eq 0) {
Write-Success "部署成功" Write-Success "部署成功"
$deploySuccess = $true $deploySuccess = $true
@ -1076,7 +1179,7 @@ function Main {
} }
} }
if (-not $sshTest -or -not $sshArgs) { if (-not $sshTest -or -not $remoteCommandAvailable) {
Write-Warning "远程部署阶段未完成,已提供手动命令指引" Write-Warning "远程部署阶段未完成,已提供手动命令指引"
} elseif ($sshTest) { } elseif ($sshTest) {
Write-Info "开始对线上应用做一次健康检查..." Write-Info "开始对线上应用做一次健康检查..."
@ -1095,8 +1198,8 @@ function Main {
Clean-LocalDockerImages -ImageName $ImageName -KeepTag $Tag -KeepCount $KeepImageCount Clean-LocalDockerImages -ImageName $ImageName -KeepTag $Tag -KeepCount $KeepImageCount
# 清理服务器镜像 # 清理服务器镜像
if ($sshTest -and $sshArgs) { if ($sshTest -and $remoteCommandAvailable) {
Clean-RemoteDockerImages -Server $Server -Username $Username -KeyPath $KeyPath -ImageName $ImageName -KeepCount $KeepImageCount Clean-RemoteDockerImages -Server $Server -Username $Username -Password $Password -KeyPath $KeyPath -ImageName $ImageName -KeepCount $KeepImageCount
} }
# 清理 Registry 镜像(仅适用于旧 Registry) # 清理 Registry 镜像(仅适用于旧 Registry)
@ -1136,18 +1239,18 @@ function Main {
Write-Info "如果自动部署失败,请在服务器上手动执行:" Write-Info "如果自动部署失败,请在服务器上手动执行:"
Write-Info " cd $RemotePath" Write-Info " cd $RemotePath"
Write-Info " docker login $HarborUrl -u $HarborUsername -p $HarborPassword" Write-Info " docker login $HarborUrl -u $HarborUsername -p $HarborPassword"
Write-Info " docker-compose stop app && docker-compose rm -f app" Write-Info " docker compose -f docker-compose.prod.yml stop app && docker compose -f docker-compose.prod.yml rm -f app"
Write-Info " docker pull $registryImage" Write-Info " docker pull $registryImage"
Write-Info " docker tag $registryImage $fullImage" Write-Info " docker tag $registryImage $fullImage"
Write-Info " docker-compose up -d --no-deps --pull never app" Write-Info " docker compose -f docker-compose.prod.yml up -d --no-deps --pull never app"
} else { } else {
Write-Info "镜像已推送到 Registry: $registryImage" Write-Info "镜像已推送到 Registry: $registryImage"
Write-Info "如果自动部署失败,请在服务器上手动执行:" Write-Info "如果自动部署失败,请在服务器上手动执行:"
Write-Info " cd $RemotePath" Write-Info " cd $RemotePath"
Write-Info " docker-compose stop app && docker-compose rm -f app" Write-Info " docker compose -f docker-compose.prod.yml stop app && docker compose -f docker-compose.prod.yml rm -f app"
Write-Info " docker pull $registryImage" Write-Info " docker pull $registryImage"
Write-Info " docker tag $registryImage $fullImage" Write-Info " docker tag $registryImage $fullImage"
Write-Info " docker-compose up -d --no-deps --pull never app" Write-Info " docker compose -f docker-compose.prod.yml up -d --no-deps --pull never app"
} }
Write-Info " # 如需执行数据库迁移,请手动运行: docker exec flnxi-material npx prisma migrate deploy" Write-Info " # 如需执行数据库迁移,请手动运行: docker exec flnxi-material npx prisma migrate deploy"
} }
@ -1167,4 +1270,3 @@ function Main {
# 执行主函数 # 执行主函数
Main Main

View File

@ -3,21 +3,22 @@
# 支持内网穿透,资源存储在MinIO存储桶 # 支持内网穿透,资源存储在MinIO存储桶
# 从私有镜像仓库拉取镜像,无需源码 # 从私有镜像仓库拉取镜像,无需源码
# ============================================ # ============================================
# Harbor 项目结构: # Registry 项目结构:
# - flnxi-public (公开): Agent、runtime 依赖镜像 # - flnxi/runtime:PostgreSQL、MinIO 等运行依赖镜像
# - flnxi (私有): 素材系统镜像,需 token 拉取 # - flnxi/material 或 flnxi/material-dev:素材系统正式版 / 测试版镜像
# 客户交付必须在 .env 中显式填写 APP_IMAGE,避免误拉旧仓库或 latest。
# ============================================ # ============================================
services: services:
# PostgreSQL 数据库 # PostgreSQL 数据库
postgres: postgres:
# 从 Harbor 的 flnxi-public 项目拉取(公开,无需登录) # 可由 .env 覆盖为私有 Registry runtime 镜像;默认走 Docker Hub 官方镜像。
image: ${POSTGRES_IMAGE:-harbor.flnxi.com/flnxi-public/runtime/postgres:15-alpine} image: ${POSTGRES_IMAGE:-postgres:15-alpine}
container_name: flnxi-postgres container_name: ${POSTGRES_CONTAINER_NAME:-flnxi-postgres}
restart: unless-stopped restart: unless-stopped
environment: environment:
POSTGRES_USER: ${POSTGRES_USER:-postgres} POSTGRES_USER: ${POSTGRES_USER:-postgres}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-youyidian123456} POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?POSTGRES_PASSWORD is required}
POSTGRES_DB: ${POSTGRES_DB:-flnxi} POSTGRES_DB: ${POSTGRES_DB:-flnxi}
POSTGRES_INITDB_ARGS: "--encoding=UTF-8" POSTGRES_INITDB_ARGS: "--encoding=UTF-8"
volumes: volumes:
@ -33,14 +34,14 @@ services:
# MinIO 对象存储 # MinIO 对象存储
# 使用特定版本号确保稳定性(符合项目宗旨:稳定性优先) # 使用特定版本号确保稳定性(符合项目宗旨:稳定性优先)
minio: minio:
# 从 Harbor 的 flnxi-public 项目拉取(公开,无需登录) # 可由 .env 覆盖为私有 Registry runtime 镜像;默认走官方镜像。
image: ${MINIO_IMAGE:-harbor.flnxi.com/flnxi-public/runtime/minio:RELEASE.2025-04-22T22-12-26Z} image: ${MINIO_IMAGE:-minio/minio:RELEASE.2025-04-22T22-12-26Z}
container_name: flnxi-minio container_name: ${MINIO_CONTAINER_NAME:-flnxi-minio}
restart: unless-stopped restart: unless-stopped
command: server /data --console-address ":9001" command: server /data --console-address ":9001"
environment: environment:
MINIO_ROOT_USER: ${MINIO_ROOT_USER:-minioadmin} MINIO_ROOT_USER: ${MINIO_ROOT_USER:?MINIO_ROOT_USER is required}
MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD:-minioadmin123456} MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD:?MINIO_ROOT_PASSWORD is required}
volumes: volumes:
- minio_data:/data - minio_data:/data
ports: ports:
@ -55,32 +56,47 @@ services:
retries: 3 retries: 3
# MinIO 初始化服务(创建bucket) # MinIO 初始化服务(创建bucket)
# 使用 latest 标签确保可用性(mc 客户端版本兼容性好)
minio-init: minio-init:
# 从 Harbor 的 flnxi-public 项目拉取(公开,无需登录) # mc 客户端用于初始化桶;离线或客户现场可在 .env 中覆盖为私有 Registry 镜像。
image: ${MINIO_MC_IMAGE:-harbor.flnxi.com/flnxi-public/runtime/minio-mc:latest} image: ${MINIO_MC_IMAGE:-minio/mc:RELEASE.2025-04-16T18-13-26Z}
container_name: flnxi-minio-init container_name: ${MINIO_INIT_CONTAINER_NAME:-flnxi-minio-init}
environment:
MINIO_ROOT_USER: ${MINIO_ROOT_USER:?MINIO_ROOT_USER is required}
MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD:?MINIO_ROOT_PASSWORD is required}
S3_BUCKET: ${S3_BUCKET:-flnxi-materials}
S3_SET_ACL: ${S3_SET_ACL:-0}
S3_PUBLIC_READ: ${S3_PUBLIC_READ:-0}
depends_on: depends_on:
minio: minio:
condition: service_healthy condition: service_healthy
entrypoint: > entrypoint: |
/bin/sh -c " /bin/sh -c '
set -e;
sleep 5; sleep 5;
mc alias set myminio http://minio:9000 ${MINIO_ROOT_USER:-minioadmin} ${MINIO_ROOT_PASSWORD:-minioadmin123456}; mc alias set myminio http://minio:9000 "$${MINIO_ROOT_USER}" "$${MINIO_ROOT_PASSWORD}";
mc mb myminio/${S3_BUCKET:-flnxi-materials} --ignore-existing; mc mb "myminio/$${S3_BUCKET}" --ignore-existing;
mc anonymous set download myminio/${S3_BUCKET:-flnxi-materials}; PUBLIC_READ="$$(printf "%s" "$${S3_SET_ACL:-0}:$${S3_PUBLIC_READ:-0}" | tr "[:upper:]" "[:lower:]")";
echo 'MinIO bucket created and configured successfully'; case "$$PUBLIC_READ" in
exit 0; 1:*|true:*|yes:*|*:1|*:true|*:yes)
" echo "WARNING: S3 public read compatibility is enabled; bucket anonymous download will be allowed";
mc anonymous set download "myminio/$${S3_BUCKET}";
;;
*)
mc anonymous set none "myminio/$${S3_BUCKET}";
echo "MinIO bucket anonymous access disabled";
;;
esac;
echo "MinIO bucket created and configured successfully";
'
networks: networks:
- app-network - app-network
# 主应用(Pro版 - 从私有镜像仓库拉取,需 token) # 主应用(Pro版 - 从私有镜像仓库拉取,需 token)
app: app:
# 从 Harbor 的 flnxi 项目拉取(私有,需许可证 token 登录) # 客户交付必须显式填写完整 APP_IMAGE,例如:
# Agent 会在验证许可证后自动获取 token 并拉取此镜像 # APP_IMAGE=registry.example.com/flnxi/material/flnxi-material:1.0.1
image: ${APP_IMAGE:-harbor.flnxi.com/flnxi/flnxi-material:pro-${IMAGE_TAG:-latest}} image: ${APP_IMAGE:?APP_IMAGE is required, for example registry.example.com/flnxi/material/flnxi-material:1.0.1}
container_name: flnxi-material container_name: ${APP_CONTAINER_NAME:-flnxi-material}
restart: unless-stopped restart: unless-stopped
ports: ports:
- "${APP_PORT:-4320}:3000" - "${APP_PORT:-4320}:3000"
@ -93,33 +109,34 @@ services:
# - SERVER_IP 必须在安装脚本或 .env 中配置为「本机在局域网中的访问 IP」,例如:192.168.41.144 # - SERVER_IP 必须在安装脚本或 .env 中配置为「本机在局域网中的访问 IP」,例如:192.168.41.144
# - 对于一键安装用户,install.sh 会自动生成 .env.production 并写入 SERVER_IP,这里直接引用即可 # - 对于一键安装用户,install.sh 会自动生成 .env.production 并写入 SERVER_IP,这里直接引用即可
- NODE_ENV=production - NODE_ENV=production
# DATABASE_URL 从 .env 文件自动读取(install.sh 会生成正确编码的 URL) # DATABASE_URL 从 .env 文件自动读取;未配置时按同一份 .env 中的数据库变量拼接
# 如果 .env 文件中没有 DATABASE_URL,则使用默认值(向后兼容) - DATABASE_URL=${DATABASE_URL:-postgresql://${POSTGRES_USER:-postgres}:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB:-flnxi}}
- DATABASE_URL=${DATABASE_URL:-postgresql://${POSTGRES_USER:-postgres}:${POSTGRES_PASSWORD:-youyidian123456}@postgres:5432/${POSTGRES_DB:-flnxi}}
- DATABASE_DRIVER=node - DATABASE_DRIVER=node
- STORAGE_PROVIDER=s3 - STORAGE_PROVIDER=s3
- S3_ENDPOINT=http://minio:9000 - S3_ENDPOINT=http://minio:9000
- S3_BUCKET=${S3_BUCKET:-flnxi-materials} - S3_BUCKET=${S3_BUCKET:-flnxi-materials}
- S3_ACCESS_KEY_ID=${MINIO_ROOT_USER:-minioadmin} - S3_ACCESS_KEY_ID=${MINIO_ROOT_USER}
- S3_SECRET_ACCESS_KEY=${MINIO_ROOT_PASSWORD:-minioadmin123456} - S3_SECRET_ACCESS_KEY=${MINIO_ROOT_PASSWORD}
- S3_REGION=us-east-1 - S3_REGION=us-east-1
# 对外访问 MinIO 的地址(供前端和分享链接使用),必须是局域网 / 公网可访问的 IP 或域名 # 对外访问 MinIO 的地址(供前端和分享链接使用),必须是局域网 / 公网可访问的 IP 或域名
# 这里不再默认 127.0.0.1,避免在 NAS 等环境下写死 localhost # 这里不再默认 127.0.0.1,避免在 NAS 等环境下写死 localhost
- S3_PUBLIC_DOMAIN=http://${SERVER_IP}:${MINIO_API_PORT:-9000}/${S3_BUCKET:-flnxi-materials} - S3_PUBLIC_DOMAIN=http://${SERVER_IP}:${MINIO_API_PORT:-9000}/${S3_BUCKET:-flnxi-materials}
- S3_ENABLE_PATH_STYLE=1 - S3_ENABLE_PATH_STYLE=1
- S3_SET_ACL=1 - S3_SET_ACL=${S3_SET_ACL:-0}
- S3_PREVIEW_URL_EXPIRE_IN=7200 - S3_PREVIEW_URL_EXPIRE_IN=7200
- S3_FILE_PATH=materials/ - S3_FILE_PATH=materials/
- NEXT_PUBLIC_S3_FILE_PATH=materials/ - NEXT_PUBLIC_S3_FILE_PATH=materials/
# NextAuth 的外部访问地址,从 .env(.production) 中注入(不在这里拼接,以免 SERVER_IP 为空时产生 http://:4320) # NextAuth 的外部访问地址,从 .env(.production) 中注入(不在这里拼接,以免 SERVER_IP 为空时产生 http://:4320)
# install.sh / 部署脚本会写入 NEXTAUTH_URL,例如:http://192.168.41.144:4320 # install.sh / 部署脚本会写入 NEXTAUTH_URL,例如:http://192.168.41.144:4320
- NEXTAUTH_URL=${NEXTAUTH_URL} - NEXTAUTH_URL=${NEXTAUTH_URL}
- NEXTAUTH_SECRET=${NEXTAUTH_SECRET:-change-this-to-a-random-string-in-production} - NEXTAUTH_SECRET=${NEXTAUTH_SECRET:?NEXTAUTH_SECRET is required}
# 初始管理员账号(供 /api/system/init 使用) # 初始管理员账号(供 /api/system/init 使用)
- INIT_ADMIN_EMAIL=${INIT_ADMIN_EMAIL:-admin@flnxi.com} - INIT_ADMIN_EMAIL=${INIT_ADMIN_EMAIL:-admin@local.flnxi}
- INIT_ADMIN_PASSWORD=${INIT_ADMIN_PASSWORD:-88888888} - INIT_ADMIN_PASSWORD=${INIT_ADMIN_PASSWORD:-}
- SYSTEM_INIT_TOKEN=${SYSTEM_INIT_TOKEN:-}
volumes: volumes:
- ./uploads:/app/uploads - ./uploads:/app/uploads
- ./data:/app/data
depends_on: depends_on:
postgres: postgres:
condition: service_healthy condition: service_healthy
@ -130,19 +147,22 @@ services:
networks: networks:
- app-network - app-network
healthcheck: healthcheck:
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:3000/api/health"] test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://127.0.0.1:3000/api/health"]
interval: 30s interval: 30s
timeout: 10s timeout: 10s
retries: 3 retries: 3
volumes: volumes:
postgres_data: postgres_data:
name: ${POSTGRES_VOLUME_NAME:-flnxi-postgres-data}
driver: local driver: local
minio_data: minio_data:
name: ${MINIO_VOLUME_NAME:-flnxi-minio-data}
driver: local driver: local
networks: networks:
app-network: app-network:
name: ${APP_NETWORK_NAME:-flnxi-app-network}
driver: bridge driver: bridge

View File

@ -0,0 +1,24 @@
services:
postgres:
image: ${POSTGRES_IMAGE:-postgres:16}
pull_policy: never
minio:
image: ${MINIO_IMAGE:-minio/minio:RELEASE.2025-04-22T22-12-26Z}
pull_policy: never
minio-init:
image: ${MINIO_MC_IMAGE:-minio/mc:RELEASE.2025-04-16T18-13-26Z}
pull_policy: never
app:
image: ${APP_IMAGE:-flnxi-sucai-test-local:${IMAGE_TAG:-latest}}
pull_policy: never
build:
context: .
dockerfile: Dockerfile
args:
DATABASE_URL: ${DATABASE_URL:-postgresql://${POSTGRES_USER:-postgres}:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB:-flnxi}}
NEXTAUTH_URL: ${NEXTAUTH_URL}
APP_PUBLIC_URL: ${APP_PUBLIC_URL}
APP_VERSION: ${APP_VERSION:-source-test}

View File

@ -28,7 +28,7 @@ services:
# DATABASE_DRIVER=${DATABASE_DRIVER} # DATABASE_DRIVER=${DATABASE_DRIVER}
# 站点公共URL(可选,优先用于生成邀请链接等) # 站点公共URL(可选,优先用于生成邀请链接等)
# APP_PUBLIC_URL=${APP_PUBLIC_URL} # APP_PUBLIC_URL=${APP_PUBLIC_URL}
- KEY_VAULTS_SECRET=/7DcksaICAECGjB38O3s8AE0wj2NoGzwIog0iUUMugY= - KEY_VAULTS_SECRET=${KEY_VAULTS_SECRET:-}
- VECTOR_DB=pgvector - VECTOR_DB=pgvector
- PGVECTOR_DB_URL=${DATABASE_URL} - PGVECTOR_DB_URL=${DATABASE_URL}
- STORAGE_PROVIDER=s3 - STORAGE_PROVIDER=s3
@ -46,11 +46,11 @@ services:
# NextAuth配置从.env文件自动读取 # NextAuth配置从.env文件自动读取
# NEXTAUTH_URL=${NEXTAUTH_URL} # NEXTAUTH_URL=${NEXTAUTH_URL}
# NEXTAUTH_SECRET=${NEXTAUTH_SECRET} # NEXTAUTH_SECRET=${NEXTAUTH_SECRET}
- GEMINI_API_KEY=sk-NqhrkeBudn6pUtStnOuOnGyDfasogvFSsex4YgjDXJU3SK3E - GEMINI_API_KEY=${GEMINI_API_KEY:-}
- GEMINI_API_BASE_URL=https://api.huandutech.com/v1 - GEMINI_API_BASE_URL=${GEMINI_API_BASE_URL:-}
- GEMINI_MODEL=gemini-2.0-flash-exp - GEMINI_MODEL=${GEMINI_MODEL:-}
- DINGTALK_CLIENT_ID=ding1bs1gxwrcfu16x0f - DINGTALK_CLIENT_ID=${DINGTALK_CLIENT_ID:-}
- DINGTALK_CLIENT_SECRET=qUmj_gBDzpPEYR3RA3hRqKzwu11a986O-wKfP8V5lDHXn7XJrXxW79KIeP5G-mcY - DINGTALK_CLIENT_SECRET=${DINGTALK_CLIENT_SECRET:-}
volumes: volumes:
- ./uploads:/app/uploads - ./uploads:/app/uploads
healthcheck: healthcheck:

View File

@ -0,0 +1,38 @@
import fs from 'fs'
import path from 'path'
describe('deploy-registry.ps1', () => {
const script = fs.readFileSync(
path.join(process.cwd(), 'deploy-registry.ps1'),
'utf8'
)
it('远程部署默认包含升级前数据库备份和 .env 备份', () => {
expect(script).toContain('[switch]$NoRemoteDbBackup')
expect(script).toContain('env_backup="$backup_dir/.env.before-upgrade"')
expect(script).toContain("docker compose -f \"$compose_file\" exec -T postgres sh -c 'pg_dump")
expect(script).toContain('test -s "$db_backup"')
})
it('远程部署失败时默认会恢复旧 APP_IMAGE 或旧容器镜像', () => {
expect(script).toContain('[switch]$NoRollback')
expect(script).toContain('rollback()')
expect(script).toContain('trap rollback ERR')
expect(script).toContain('rollback_image="$old_container_image"')
expect(script).toContain('docker compose -f "$compose_file" up -d --no-deps --pull never --force-recreate "$compose_service" || true')
})
it('部署后校验运行镜像、版本文件和数据库迁移状态', () => {
expect(script).toContain('verify_running_image')
expect(script).toContain('actual_image="$(docker inspect "$container_id" --format')
expect(script).toContain('运行镜像不匹配')
expect(script).toContain('运行版本不匹配')
expect(script).toContain('npx prisma migrate status --schema=prisma/schema.prisma')
})
it('远端 shell 变量应以单引号字符串保留,避免被 PowerShell 提前展开', () => {
expect(script).toContain('$lines.Add(\'docker compose -f "$compose_file" up -d --pull never\')')
expect(script).toContain('$lines.Add(\'docker compose -f "$compose_file" ps\')')
expect(script).not.toContain('$lines.Add("docker compose -f `"$compose_file`"')
})
})

View File

@ -0,0 +1,69 @@
import fs from 'fs'
import os from 'os'
import path from 'path'
import { spawnSync } from 'child_process'
describe('scripts/docker-entrypoint.sh', () => {
const entrypoint = fs.readFileSync(
path.join(process.cwd(), 'scripts/docker-entrypoint.sh'),
'utf8'
)
it('数据库迁移失败时必须停止容器启动', () => {
expect(entrypoint).toContain('npx prisma migrate deploy')
expect(entrypoint).toContain('数据库迁移失败')
expect(entrypoint).toMatch(/数据库迁移失败[\s\S]*exit 1/)
expect(entrypoint).not.toContain('数据库迁移失败,尝试继续启动')
})
it('迁移命令真实失败时不会继续启动 server.js', () => {
if (process.platform !== 'linux') {
return
}
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'flnxi-entrypoint-'))
const binDir = path.join(tmpDir, 'bin')
fs.mkdirSync(binDir)
fs.writeFileSync(
path.join(binDir, 'npx'),
[
'#!/bin/sh',
'if [ "$1 $2 $3" = "prisma db execute" ]; then',
' exit 0',
'fi',
'if [ "$1 $2 $3" = "prisma migrate deploy" ]; then',
' exit 42',
'fi',
'exit 1',
'',
].join('\n'),
{ mode: 0o755 }
)
fs.writeFileSync(
path.join(binDir, 'node'),
[
'#!/bin/sh',
`echo started > "${path.join(tmpDir, 'server-started').replace(/\\/g, '/')}"`,
'exit 0',
'',
].join('\n'),
{ mode: 0o755 }
)
const scriptPath = path.join(process.cwd(), 'scripts/docker-entrypoint.sh')
const result = spawnSync('sh', [scriptPath], {
cwd: tmpDir,
env: {
...process.env,
PATH: `${binDir}${path.delimiter}${process.env.PATH || ''}`,
},
encoding: 'utf8',
})
expect(result.status).toBe(1)
expect(`${result.stdout}\n${result.stderr}`).toContain('数据库迁移失败')
expect(fs.existsSync(path.join(tmpDir, 'server-started'))).toBe(false)
})
})

View File

@ -1,5 +1,6 @@
# 素材系统镜像构建脚本 # 素材系统镜像构建脚本(历史/内部备用)
# 支持测试版和正式版分离 # 当前客户交付主线请使用根目录 deploy-registry.ps1。
# 本脚本保留测试/正式分层的旧约定,但默认 Harbor/latest 口径不再作为客户升级依据。
param( param(
[Parameter(Mandatory=$true)] [Parameter(Mandatory=$true)]
@ -10,7 +11,8 @@ param(
[string]$HarborHost = "harbor.flnxi.com", [string]$HarborHost = "harbor.flnxi.com",
[string]$HarborProject = "flnxi", [string]$HarborProject = "flnxi",
[switch]$Push = $false, [switch]$Push = $false,
[switch]$NoBuild = $false [switch]$NoBuild = $false,
[switch]$AcknowledgeLegacyHarborPush = $false
) )
# Harbor 镜像路径规划: # Harbor 镜像路径规划:
@ -30,6 +32,16 @@ param(
$ErrorActionPreference = "Stop" $ErrorActionPreference = "Stop"
Write-Host "[WARN] build-material-image.ps1 is a legacy/internal fallback entry." -ForegroundColor Yellow
Write-Host "[WARN] Customer release, upgrade, and rollback should use root deploy-registry.ps1 with a pinned APP_IMAGE tag." -ForegroundColor Yellow
Write-Host ""
if ($Push -and (-not $AcknowledgeLegacyHarborPush)) {
Write-Host "ERROR: this script still pushes to legacy Harbor/latest targets and must not be used as the customer release entry." -ForegroundColor Red
Write-Host "For internal legacy maintenance only, add -AcknowledgeLegacyHarborPush explicitly." -ForegroundColor Yellow
exit 1
}
# 根据环境确定镜像路径 # 根据环境确定镜像路径
$ImageName = "flnxi-material" $ImageName = "flnxi-material"
switch ($Environment) { switch ($Environment) {
@ -78,6 +90,28 @@ try {
exit 1 exit 1
} }
# 优先加载本地基础镜像(避免网络问题)
$LocalImagePath = "docker-images/node/node-18-alpine.tar"
if (Test-Path $LocalImagePath) {
Write-Host "检查本地基础镜像..." -ForegroundColor Gray
$existingImage = docker images node:18-alpine --format "{{.ID}}" 2>$null
if (-not $existingImage) {
Write-Host "加载本地基础镜像: $LocalImagePath" -ForegroundColor Cyan
docker load -i $LocalImagePath
if ($LASTEXITCODE -eq 0) {
Write-Host "本地基础镜像加载成功 ✓" -ForegroundColor Green
} else {
Write-Host "警告: 本地基础镜像加载失败,将尝试在线拉取" -ForegroundColor Yellow
}
} else {
Write-Host "本地基础镜像已存在 ✓" -ForegroundColor Green
}
} else {
Write-Host "提示: 未找到本地基础镜像 ($LocalImagePath),将使用在线拉取" -ForegroundColor Yellow
Write-Host " 建议: 运行 'docker save node:18-alpine -o $LocalImagePath' 保存基础镜像" -ForegroundColor Gray
}
Write-Host ""
# 构建镜像 # 构建镜像
if (-not $NoBuild) { if (-not $NoBuild) {
Write-Host "开始构建镜像..." -ForegroundColor Cyan Write-Host "开始构建镜像..." -ForegroundColor Cyan
@ -149,10 +183,11 @@ Write-Host " 最新标签: $LatestImageName" -ForegroundColor Gray
Write-Host "" Write-Host ""
if ($Environment -eq "release") { if ($Environment -eq "release") {
Write-Host "提示: 正式版镜像已发布,客户可通过以下地址拉取:" -ForegroundColor Yellow Write-Host "NOTICE: release image built. Customer delivery must pin the full version image, not latest:" -ForegroundColor Yellow
Write-Host " $HarborHost/$HarborProject/material/flnxi-material:latest" -ForegroundColor White Write-Host " $FullImageName" -ForegroundColor White
Write-Host "Current release/upgrade/rollback entry is root deploy-registry.ps1." -ForegroundColor Yellow
} else { } else {
Write-Host "提示: 测试版镜像已发布到 material-dev 目录" -ForegroundColor Yellow Write-Host "NOTICE: test image built under material-dev." -ForegroundColor Yellow
Write-Host " 测试完成后,使用以下命令发布正式版:" -ForegroundColor Gray Write-Host " After testing, publish release with:" -ForegroundColor Gray
Write-Host " .\scripts\build-material-image.ps1 -Environment release -Version $Tag -Push" -ForegroundColor White Write-Host " .\deploy-registry.ps1 -Action PublishRelease -Version $Tag -RegistryHost <registry-host> -RegistryPassword `$registryPassword" -ForegroundColor White
} }

View File

@ -39,7 +39,9 @@ echo "[2/3] 执行数据库迁移..."
if npx prisma migrate deploy; then if npx prisma migrate deploy; then
echo "✓ 数据库迁移完成" echo "✓ 数据库迁移完成"
else else
echo "⚠ 数据库迁移失败,尝试继续启动..." echo "✗ 数据库迁移失败,已停止启动以保护数据一致性"
echo "请先检查迁移日志、数据库连接和备份状态,修复后再重新启动容器"
exit 1
fi fi
# 3. 启动应用(系统初始化会在首次请求时自动执行) # 3. 启动应用(系统初始化会在首次请求时自动执行)

View File

@ -0,0 +1,198 @@
param(
[string]$OutputPath,
[string]$ServerIp = "192.168.42.134",
[int]$AppPort = 4320,
[int]$MinioApiPort = 39200,
[int]$MinioConsolePort = 39201,
[int]$AgentPort = 3080,
[int]$AgentGoPort = 9091,
[string]$ComposeProjectName = "flnxi_sucai_test",
[string]$ContainerPrefix = "flnxi-sucai-test",
[string]$LicenseServerUrl,
[string]$LicenseProjectCode = "flnxi-sucai",
[string]$InitAdminEmail = "admin@test.flnxi.local"
)
Set-StrictMode -Version Latest
$ErrorActionPreference = "Stop"
if ([string]::IsNullOrWhiteSpace($LicenseServerUrl)) {
throw "必须显式传入 -LicenseServerUrl,避免把过期或错误的许可证地址写进测试环境。"
}
function New-RandomString {
param(
[int]$Length = 32,
[string]$Alphabet = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
)
$bytes = New-Object byte[] ($Length * 2)
[System.Security.Cryptography.RandomNumberGenerator]::Fill($bytes)
$builder = New-Object System.Text.StringBuilder
foreach ($byte in $bytes) {
if ($builder.Length -ge $Length) {
break
}
[void]$builder.Append($Alphabet[$byte % $Alphabet.Length])
}
return $builder.ToString()
}
function Get-LicensePublicKey {
param([string]$ProjectRoot)
$candidateFiles = @(
(Join-Path $ProjectRoot "license-server\.env"),
(Join-Path $ProjectRoot ".env.production"),
(Join-Path $ProjectRoot ".env"),
(Join-Path $ProjectRoot ".env.local")
)
foreach ($file in $candidateFiles) {
if (-not (Test-Path $file)) {
continue
}
$content = Get-Content -Raw $file
$licenseMatch = [regex]::Match($content, '(?m)^LICENSE_PUBLIC_KEY=(.+)$')
if ($licenseMatch.Success) {
return $licenseMatch.Groups[1].Value.Trim().Trim('"')
}
$rsaMatch = [regex]::Match($content, '(?m)^RSA_PUBLIC_KEY=(.+)$')
if ($rsaMatch.Success) {
return $rsaMatch.Groups[1].Value.Trim().Trim('"')
}
$privateMatch = [regex]::Match($content, '(?m)^RSA_PRIVATE_KEY=(.+)$')
if ($privateMatch.Success) {
$privateKey = $privateMatch.Groups[1].Value.Trim().Trim('"')
$pem = $privateKey.Replace('\n', [Environment]::NewLine)
try {
$rsa = [System.Security.Cryptography.RSA]::Create()
try {
$rsa.ImportFromPem($pem.ToCharArray())
$publicPem = $rsa.ExportSubjectPublicKeyInfoPem()
return ([regex]::Replace($publicPem, "(\r\n|\n|\r)", '\n')).Trim()
} finally {
$rsa.Dispose()
}
} catch {
throw "检测到 RSA_PRIVATE_KEY,但自动推导公钥失败:$($_.Exception.Message)"
}
}
}
throw "未找到可用的 LICENSE_PUBLIC_KEY / RSA_PUBLIC_KEY,无法生成测试环境 .env。"
}
if ([string]::IsNullOrWhiteSpace($OutputPath)) {
throw "必须提供 -OutputPath。"
}
$projectRoot = Split-Path -Parent $PSScriptRoot
$licensePublicKey = Get-LicensePublicKey -ProjectRoot $projectRoot
$postgresUser = "flnxi_test"
$postgresDb = "flnxi_sucai_test"
$postgresPassword = New-RandomString -Length 24
$minioRootUser = "flnxitest"
$minioRootPassword = New-RandomString -Length 24
$nextAuthSecret = New-RandomString -Length 48
$localEncryptionKey = New-RandomString -Length 48
$systemInitToken = New-RandomString -Length 40
$s3Bucket = "flnxi-sucai-test"
$appUrl = "http://${ServerIp}:${AppPort}"
$s3PublicDomain = "http://${ServerIp}:${MinioApiPort}/${s3Bucket}"
$envContent = @"
# FLNXI 素材库共享测试机环境变量
# 用途:192.168.42.134 测试机
# 生成时间:$(Get-Date -Format "yyyy-MM-dd HH:mm:ss")
NODE_ENV=production
# Shared-machine isolation
COMPOSE_PROJECT_NAME=$ComposeProjectName
APP_CONTAINER_NAME=${ContainerPrefix}-app
POSTGRES_CONTAINER_NAME=${ContainerPrefix}-postgres
MINIO_CONTAINER_NAME=${ContainerPrefix}-minio
MINIO_INIT_CONTAINER_NAME=${ContainerPrefix}-minio-init
POSTGRES_VOLUME_NAME=${ContainerPrefix}-postgres-data
MINIO_VOLUME_NAME=${ContainerPrefix}-minio-data
APP_NETWORK_NAME=${ContainerPrefix}-network
APP_PORT=$AppPort
MINIO_API_PORT=$MinioApiPort
MINIO_CONSOLE_PORT=$MinioConsolePort
AGENT_PORT=$AgentPort
AGENT_GO_PORT=$AgentGoPort
# Public URLs
SERVER_IP=$ServerIp
APP_PUBLIC_URL=$appUrl
NEXTAUTH_URL=$appUrl
NEXTAUTH_SECRET=$nextAuthSecret
# License
LICENSE_SERVER_URL=$LicenseServerUrl
LICENSE_PROJECT_CODE=$LicenseProjectCode
LICENSE_PUBLIC_KEY="$licensePublicKey"
LOCAL_ENCRYPTION_KEY=$localEncryptionKey
# Database
POSTGRES_USER=$postgresUser
POSTGRES_PASSWORD=$postgresPassword
POSTGRES_DB=$postgresDb
DATABASE_URL=postgresql://${postgresUser}:${postgresPassword}@postgres:5432/${postgresDb}
DATABASE_DRIVER=node
# Object storage
STORAGE_PROVIDER=s3
S3_ENDPOINT=http://minio:9000
S3_BUCKET=$s3Bucket
S3_ACCESS_KEY_ID=$minioRootUser
S3_SECRET_ACCESS_KEY=$minioRootPassword
S3_REGION=us-east-1
S3_PUBLIC_DOMAIN=$s3PublicDomain
S3_ENABLE_PATH_STYLE=1
S3_FILE_PATH=materials/
S3_ENV_PREFIX=test
MINIO_ROOT_USER=$minioRootUser
MINIO_ROOT_PASSWORD=$minioRootPassword
# Access control and URL rewriting
PUBLIC_DOMAINS=$ServerIp
NEXT_PUBLIC_PUBLIC_DOMAINS=$ServerIp
ALLOWED_ORIGINS=$appUrl
IMAGE_REMOTE_HOSTS=$ServerIp
# Initial system bootstrap
SYSTEM_INIT_TOKEN=$systemInitToken
INIT_ADMIN_EMAIL=$InitAdminEmail
INIT_ADMIN_PASSWORD=
# Optional features disabled by default on shared test server
DINGTALK_CLIENT_ID=
DINGTALK_CLIENT_SECRET=
NEXT_PUBLIC_DINGTALK_CALLBACK_URL=
GEMINI_API_KEY=
GEMINI_API_BASE_URL=https://api.huandutech.com/v1
GEMINI_MODEL=gemini-2.0-flash-exp
NEXT_PUBLIC_DEFAULT_HEADER_LOGO_URL=
NEXT_PUBLIC_DEFAULT_LOGIN_LOGO_URL=
NEXT_PUBLIC_INTRANET_URL=$appUrl
NEXT_PUBLIC_PORTAL_BASE_URL=$appUrl
UPLOAD_DIR=./uploads
"@
$parent = Split-Path -Parent $OutputPath
if ($parent -and -not (Test-Path $parent)) {
New-Item -ItemType Directory -Path $parent -Force | Out-Null
}
[System.IO.File]::WriteAllText($OutputPath, $envContent, [System.Text.UTF8Encoding]::new($false))
Write-Host "共享测试机环境文件已生成: $OutputPath"

View File

@ -1,28 +1,42 @@
# ================================================================ # ================================================================
# FLNXI 素材系统 - 发布管理工具 v2.0 # FLNXI 素材系统 - 历史内部发布管理工具 v2.0
# 功能:统一管理测试版部署、正式版发布、Agent 更新等操作 # 状态:已废弃,不作为客户安装、升级或正式发布入口
# 特性:进度条显示、健康检查、智能重试、趣味提示 # 当前正式发布入口请使用 deploy-registry.ps1
# ================================================================ # ================================================================
param( param(
[switch]$Help [switch]$Help,
[switch]$AcknowledgeLegacyInternalPublish
) )
$ErrorActionPreference = "Stop" $ErrorActionPreference = "Stop"
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 [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 = @{ $Config = @{
Server = "45.249.208.118" Server = ""
SshKey = "$env:USERPROFILE\.ssh\youyidian_prod_ed25519" SshKey = ""
HarborHost = "harbor.flnxi.com" HarborHost = ""
HarborProject = "flnxi" # 私有项目(素材系统) HarborProject = "flnxi" # 公开项目(素材系统)
HarborPublicProject = "flnxi-public" # 公开项目(Agent、runtime) HarborPublicProject = "flnxi-public" # 公开项目(Agent、runtime)
DockerHubUser = "flnxi" DockerHubUser = "flnxi"
HealthCheckPort = 4320 HealthCheckPort = 4320
HealthCheckTimeout = 45 HealthCheckTimeout = 45
SshRetryCount = 3 SshRetryCount = 3
SshRetryDelay = 2 SshRetryDelay = 2
# 新许可证服务器配置
LicenseServer = ""
LicenseServerUser = "root"
LicenseServerSshKey = ""
LicenseServerPort = 9527
} }
# 趣味提示 # 趣味提示
@ -118,6 +132,11 @@ function Get-CurrentVersion {
} }
function Get-AgentVersion { 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") { if (Test-Path "flnxi-agent/VERSION.txt") {
return (Get-Content "flnxi-agent/VERSION.txt" -Raw).Trim() return (Get-Content "flnxi-agent/VERSION.txt" -Raw).Trim()
} }
@ -246,7 +265,7 @@ function Show-Menu {
Write-Color "║ [1] 构建并部署测试版 ║" "White" Write-Color "║ [1] 构建并部署测试版 ║" "White"
Write-Color "║ ║" "Cyan" Write-Color "║ ║" "Cyan"
Write-Color "║ === 正式发布 (需要确认) === ║" "Yellow" Write-Color "║ === 正式发布 (需要确认) === ║" "Yellow"
Write-Color "║ [2] 发布素材系统正式版 (flnxi 私有) ⚠️ ║" "Yellow" Write-Color "║ [2] 发布素材系统正式版 (flnxi 公开) ⚠️ ║" "Yellow"
Write-Color "║ [3] 发布 Agent (flnxi-public 公开) ⚠️ ║" "Yellow" Write-Color "║ [3] 发布 Agent (flnxi-public 公开) ⚠️ ║" "Yellow"
Write-Color "║ ║" "Cyan" Write-Color "║ ║" "Cyan"
Write-Color "║ === 基础设施 === ║" "White" Write-Color "║ === 基础设施 === ║" "White"
@ -282,6 +301,27 @@ function Deploy-TestVersion {
# 启用 BuildKit 以使用缓存加速 # 启用 BuildKit 以使用缓存加速
$env:DOCKER_BUILDKIT = "1" $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 "开始构建 (BuildKit 缓存已启用)..."
Write-Info "提示: 首次构建较慢,后续构建会利用缓存加速" Write-Info "提示: 首次构建较慢,后续构建会利用缓存加速"
@ -369,6 +409,27 @@ function Publish-ReleaseVersion {
# 启用 BuildKit # 启用 BuildKit
$env:DOCKER_BUILDKIT = "1" $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 "构建正式版镜像 (BuildKit 缓存已启用)..."
$buildArgs = @( $buildArgs = @(
@ -402,12 +463,13 @@ function Publish-ReleaseVersion {
} }
} }
# 3. 发布 Agent 安装程序 # 3. 发布 Agent 安装程序 (Go 版本)
function Publish-Agent { function Publish-Agent {
Write-Title "发布 Agent 安装程序" Write-Title "发布 Agent 安装程序 (Go 版本)"
$currentVersion = Get-AgentVersion $currentVersion = Get-AgentVersion
Write-Info "当前 Agent 版本: $currentVersion" Write-Info "当前 Agent 版本: $currentVersion"
Write-Info "技术栈: Go (镜像约 70MB)"
# Harbor 公开项目镜像配置 # Harbor 公开项目镜像配置
# 格式: harbor.flnxi.com/flnxi-public/agent:版本号 # 格式: harbor.flnxi.com/flnxi-public/agent:版本号
@ -441,16 +503,16 @@ function Publish-Agent {
} }
Write-Success "Harbor 登录状态: OK" Write-Success "Harbor 登录状态: OK"
# 更新版本文件 # 更新版本文件 (Go 版本目录)
$newVersion | Out-File -FilePath "flnxi-agent/VERSION.txt" -Encoding UTF8 -NoNewline $newVersion | Out-File -FilePath "flnxi-agent-go/VERSION.txt" -Encoding UTF8 -NoNewline
Write-Success "Agent 版本文件已更新: $newVersion" Write-Success "Agent 版本文件已更新: $newVersion"
# 启用 BuildKit # 启用 BuildKit
$env:DOCKER_BUILDKIT = "1" $env:DOCKER_BUILDKIT = "1"
# 构建 Agent 镜像 # 构建 Agent 镜像 (Go 版本)
Write-Info "构建 Agent 镜像 (BuildKit 缓存已启用)..." Write-Info "构建 Agent 镜像 (Go 版本, BuildKit 缓存已启用)..."
Push-Location "flnxi-agent" Push-Location "flnxi-agent-go"
try { try {
$buildArgs = @( $buildArgs = @(
@ -458,7 +520,6 @@ function Publish-Agent {
"--progress=plain", "--progress=plain",
"-t", "${harborRepo}:${newVersion}", "-t", "${harborRepo}:${newVersion}",
"-t", "${harborRepo}:latest", "-t", "${harborRepo}:latest",
"--build-arg", "AGENT_VERSION=$newVersion",
"." "."
) )
@ -469,6 +530,10 @@ function Publish-Agent {
} }
Write-Success "构建完成" Write-Success "构建完成"
# 显示镜像大小
$imageSize = docker images "${harborRepo}:${newVersion}" --format "{{.Size}}"
Write-Info "镜像大小: $imageSize"
# 推送到 Harbor flnxi-public 项目 # 推送到 Harbor flnxi-public 项目
Write-Info "推送到 Harbor (flnxi-public)..." Write-Info "推送到 Harbor (flnxi-public)..."
docker push "${harborRepo}:${newVersion}" docker push "${harborRepo}:${newVersion}"
@ -479,8 +544,12 @@ function Publish-Agent {
Write-Host "" Write-Host ""
Write-Color "═══════════════════════════════════════════════════════════" "Cyan" Write-Color "═══════════════════════════════════════════════════════════" "Cyan"
Write-Info "镜像地址: ${harborRepo}:latest" Write-Info "镜像地址: ${harborRepo}:latest"
Write-Info "镜像大小: $imageSize (Go 版本,比 Node.js 版本减少 ~70%)"
Write-Info "客户安装命令:" Write-Info "客户安装命令:"
Write-Color " docker run -d -p 3080:3080 -v /var/run/docker.sock:/var/run/docker.sock ${harborRepo}:latest" "White" 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" Write-Color "═══════════════════════════════════════════════════════════" "Cyan"
} else { } else {
Write-Error "推送失败" Write-Error "推送失败"
@ -872,7 +941,7 @@ function Show-Status {
Write-Color "=== Harbor 项目结构 ===" "Yellow" Write-Color "=== Harbor 项目结构 ===" "Yellow"
Write-Info "flnxi-public (公开): Agent、runtime 依赖" Write-Info "flnxi-public (公开): Agent、runtime 依赖"
Write-Info "flnxi (私有): 素材系统镜像" Write-Info "flnxi (公开): 素材系统镜像"
Write-Host "" Write-Host ""
Write-Color "=== Harbor 镜像 ===" "Yellow" Write-Color "=== Harbor 镜像 ===" "Yellow"
@ -885,7 +954,7 @@ function Show-Status {
# 列出私有项目镜像 # 列出私有项目镜像
Write-Host "" Write-Host ""
Write-Color "flnxi/ (私有 - 素材系统):" "White" Write-Color "flnxi/ (公开 - 素材系统):" "White"
docker images "$($Config.HarborHost)/$($Config.HarborProject)/*" --format " {{.Repository}}:{{.Tag}}\t{{.CreatedSince}}" 2>$null docker images "$($Config.HarborHost)/$($Config.HarborProject)/*" --format " {{.Repository}}:{{.Tag}}\t{{.CreatedSince}}" 2>$null
# 列出公开项目镜像 # 列出公开项目镜像
@ -912,26 +981,19 @@ function Show-Status {
if ($Help) { if ($Help) {
Write-Color "╔═══════════════════════════════════════════════════════════╗" "Cyan" Write-Color "╔═══════════════════════════════════════════════════════════╗" "Cyan"
Write-Color "║ FLNXI 素材系统 - 发布管理工具 v2.0 ║" "Cyan" Write-Color "║ FLNXI 素材系统 - 历史内部发布工具 v2.0 ║" "Cyan"
Write-Color "╠═══════════════════════════════════════════════════════════╣" "Cyan" Write-Color "╠═══════════════════════════════════════════════════════════╣" "Cyan"
Write-Color "║ 用法: ║" "White" Write-Color "║ 用法: ║" "White"
Write-Color "║ .\发布管理.ps1 启动交互式菜单 ║" "Gray" Write-Color "║ .\deploy-registry.ps1 当前版本化发布入口 ║" "Gray"
Write-Color "║ .\发布管理.ps1 -AcknowledgeLegacyInternalPublish ║" "Gray"
Write-Color "║ 仅用于历史链路排查 ║" "Gray"
Write-Color "║ .\发布管理.ps1 -Help 显示此帮助 ║" "Gray" Write-Color "║ .\发布管理.ps1 -Help 显示此帮助 ║" "Gray"
Write-Color "╠═══════════════════════════════════════════════════════════╣" "Cyan" Write-Color "╠═══════════════════════════════════════════════════════════╣" "Cyan"
Write-Color "║ 功能: ║" "White" Write-Color "║ 说明: ║" "White"
Write-Color "║ [1] 构建并部署测试版 (含健康检查) ║" "White" Write-Color "║ 该脚本包含旧服务器、旧 Harbor、latest 和内部清理逻辑, ║" "Gray"
Write-Color "║ [2] 发布素材系统正式版 (需确认) ⚠️ ║" "Yellow" Write-Color "║ 不作为客户安装、升级或正式发布入口。 ║" "Gray"
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 "╠═══════════════════════════════════════════════════════════╣" "Cyan"
Write-Color "║ 特性: ║" "White" Write-Color "║ 当前入口: deploy-registry.ps1 + docker-compose.prod.yml ║" "Green"
Write-Color "║ ✓ 进度条显示 ✓ 健康检查 ✓ SSH智能重试 ║" "Green"
Write-Color "║ ✓ 国内镜像源加速 ✓ 趣味提示 ✓ 镜像清理 ║" "Green"
Write-Color "╚═══════════════════════════════════════════════════════════╝" "Cyan" Write-Color "╚═══════════════════════════════════════════════════════════╝" "Cyan"
exit 0 exit 0
} }