收紧私有化部署配置与旧发布入口
This commit is contained in:
parent
c759d0a37b
commit
1f46544991
173
.dockerignore
173
.dockerignore
@ -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 中重新安装)
|
||||
node_modules
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
.pnp
|
||||
.pnp.js
|
||||
|
||||
@ -56,153 +14,88 @@ out
|
||||
build
|
||||
dist
|
||||
|
||||
# 测试
|
||||
# 测试相关
|
||||
coverage
|
||||
*.test.ts
|
||||
*.test.tsx
|
||||
*.spec.ts
|
||||
*.spec.tsx
|
||||
test
|
||||
tests
|
||||
**/*.test.ts
|
||||
**/*.test.tsx
|
||||
**/*.spec.ts
|
||||
**/*.spec.tsx
|
||||
__tests__
|
||||
|
||||
# 调试和日志
|
||||
# 日志文件
|
||||
*.log
|
||||
logs
|
||||
*.log.*
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# 本地环境变量(服务器上已有 .env)
|
||||
# 注意:.env.production 需要在构建时使用,所以不排除
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
# 本地环境变量(不要打包到镜像中,运行时通过 Docker 环境变量配置)
|
||||
.env
|
||||
.env.*
|
||||
.env.local
|
||||
.env.*.local
|
||||
.env.example
|
||||
.env.template
|
||||
.env*.local
|
||||
# 但排除其他敏感文件
|
||||
server.env
|
||||
env-backups/
|
||||
.env.production
|
||||
.env.production.local
|
||||
env-backups
|
||||
config-bianliang.env
|
||||
|
||||
# 版本控制
|
||||
.git
|
||||
.gitignore
|
||||
.gitattributes
|
||||
.gitmodules
|
||||
|
||||
# IDE 和编辑器配置
|
||||
# IDE 配置
|
||||
.vscode
|
||||
.idea
|
||||
.cursor
|
||||
.trae
|
||||
.kiro
|
||||
*.swp
|
||||
*.swo
|
||||
*.sublime-project
|
||||
*.sublime-workspace
|
||||
.editorconfig
|
||||
.eslintcache
|
||||
|
||||
# 文档文件(不部署文档)
|
||||
# 文档(保留 VERSION.txt)
|
||||
docs
|
||||
*.md
|
||||
README.md
|
||||
LICENSE
|
||||
CHANGELOG.md
|
||||
CONTRIBUTING.md
|
||||
*.txt
|
||||
!VERSION.txt
|
||||
|
||||
# 脚本文件(不部署脚本)
|
||||
# 脚本(保留 docker-entrypoint.sh)
|
||||
scripts
|
||||
!scripts/docker-entrypoint.sh
|
||||
*.sh
|
||||
!scripts/docker-entrypoint.sh
|
||||
*.ps1
|
||||
*.bat
|
||||
*.py
|
||||
*.js
|
||||
!next.config.js
|
||||
!postcss.config.js
|
||||
!tailwind.config.ts
|
||||
|
||||
# 临时文件和缓存
|
||||
# 临时文件
|
||||
uploads
|
||||
public/uploads
|
||||
backups
|
||||
*.tmp
|
||||
*.temp
|
||||
.cache
|
||||
.tmp
|
||||
tmp
|
||||
temp
|
||||
*.cache
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# 压缩包和发布包
|
||||
# 压缩包和部署产物
|
||||
*.tar.gz
|
||||
*.zip
|
||||
*.rar
|
||||
*.7z
|
||||
*.tar
|
||||
*.tgz
|
||||
release-*
|
||||
dist-*
|
||||
*.bak-*
|
||||
*.ps1.bak-*
|
||||
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
|
||||
*.db
|
||||
*.sqlite
|
||||
*.sqlite3
|
||||
|
||||
# 其他文件
|
||||
*.rpm
|
||||
*.deb
|
||||
# 其他
|
||||
*.pem
|
||||
*.key
|
||||
*.cert
|
||||
*.crt
|
||||
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/ (静态资源)
|
||||
|
||||
28
.env.example
28
.env.example
@ -2,14 +2,39 @@
|
||||
NODE_ENV=development
|
||||
|
||||
# Public URLs
|
||||
SERVER_IP=192.168.x.x
|
||||
APP_PUBLIC_URL=https://your-domain.example.com
|
||||
NEXTAUTH_URL=https://your-domain.example.com
|
||||
NEXTAUTH_SECRET=replace-with-strong-secret
|
||||
LICENSE_SERVER_URL=https://your-license-server.example.com
|
||||
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_URL=postgresql://username:password@host:5432/database
|
||||
POSTGRES_USER=postgres
|
||||
POSTGRES_PASSWORD=replace-with-strong-postgres-password
|
||||
POSTGRES_DB=flnxi
|
||||
|
||||
# Object storage (S3 compatible)
|
||||
STORAGE_PROVIDER=s3
|
||||
@ -19,8 +44,11 @@ S3_ACCESS_KEY_ID=your-access-key
|
||||
S3_SECRET_ACCESS_KEY=your-secret-key
|
||||
S3_REGION=us-east-1
|
||||
S3_PUBLIC_DOMAIN=https://s3.example.com/your-bucket
|
||||
S3_SET_ACL=0
|
||||
S3_FILE_PATH=materials/
|
||||
S3_ENV_PREFIX=
|
||||
MINIO_ROOT_USER=replace-with-minio-admin-user
|
||||
MINIO_ROOT_PASSWORD=replace-with-strong-minio-admin-password
|
||||
|
||||
# Vector database (optional)
|
||||
VECTOR_DB=pgvector
|
||||
|
||||
@ -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
|
||||
@ -158,7 +158,7 @@ RUN if [ ! -d "node_modules/.prisma" ]; then \
|
||||
fi
|
||||
|
||||
# 构建 Next.js 应用(standalone 模式)
|
||||
# 允许构建时出现预渲染错误(这些页面会在运行时动态渲染)
|
||||
# 商业交付镜像必须在构建阶段暴露失败,不能吞掉构建错误继续出包。
|
||||
RUN set -ex && \
|
||||
if [ -f "${PRISMA_ENGINE_CACHE_PATH}" ]; then \
|
||||
export PRISMA_QUERY_ENGINE_LIBRARY="${PRISMA_ENGINE_CACHE_PATH}" && \
|
||||
@ -172,7 +172,7 @@ RUN if [ ! -d "node_modules/.prisma" ]; then \
|
||||
else \
|
||||
unset PRISMA_SCHEMA_ENGINE_BINARY; \
|
||||
fi && \
|
||||
npm run build || (echo "构建完成,但部分页面预渲染失败(这是预期的,这些页面将在运行时动态渲染)" && true)
|
||||
npm run build
|
||||
|
||||
# 验证构建产物(允许 ESLint 警告,但不允许致命错误)
|
||||
RUN if [ ! -f ".next/standalone/server.js" ]; then \
|
||||
|
||||
22
__tests__/deployment-compose.test.ts
Normal file
22
__tests__/deployment-compose.test.ts
Normal 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}')
|
||||
})
|
||||
})
|
||||
@ -61,7 +61,7 @@ NEXT_PUBLIC_DINGTALK_CALLBACK_URL=
|
||||
# 图片远程域名白名单(Next/Image)
|
||||
# -----------------------------
|
||||
# 用于 next.config.js 中的 remotePatterns。逗号分隔主机名;
|
||||
# 默认已包含 s3.1080p.hk,如使用自有域名请在此追加。
|
||||
# 如使用自有对象存储或 CDN 域名,请在此追加。
|
||||
# 例如:cdn.company.com,images.company.com
|
||||
IMAGE_REMOTE_HOSTS=
|
||||
|
||||
|
||||
@ -1,23 +1,23 @@
|
||||
# ================================================================
|
||||
# FLNXI素材库 - 部署脚本 v1.2
|
||||
# 功能:本地构建 -> 推送到Registry -> 远程部署
|
||||
# 特性:自动加载基础镜像、智能重试、镜像清理、趣味提示
|
||||
# FLNXI素材库 - 历史内部部署脚本 v1.2
|
||||
# 状态:已废弃,不作为客户安装、升级或正式发布入口
|
||||
# 当前正式发布入口请使用 deploy-registry.ps1
|
||||
# ================================================================
|
||||
|
||||
param(
|
||||
[string]$Server = "45.249.208.118",
|
||||
[string]$Server = "",
|
||||
[string]$Username = "root",
|
||||
[SecureString]$Password = $null,
|
||||
[string]$KeyPath = "",
|
||||
[string]$ImageName = "flnxi-material",
|
||||
[string]$Tag = "pro-latest",
|
||||
[string]$Tag = "",
|
||||
[string]$RemotePath = "/root/deploy",
|
||||
[string]$EnvFile = "",
|
||||
[string]$Registry = "harbor", # 默认使用 Harbor
|
||||
[string]$HarborUrl = "harbor.flnxi.com",
|
||||
[string]$HarborUrl = "",
|
||||
[string]$HarborProjectName = "flnxi", # Harbor 项目名称,例如: flnxi, sucai, demo, library
|
||||
[string]$HarborUsername = "admin",
|
||||
[string]$HarborPassword = "Harbor12345",
|
||||
[string]$HarborPassword = "",
|
||||
[switch]$SkipCheck,
|
||||
[switch]$SkipBuild,
|
||||
[switch]$SkipUpload,
|
||||
@ -26,7 +26,8 @@ param(
|
||||
[switch]$CleanImages,
|
||||
[int]$KeepImageCount = 5,
|
||||
[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 = @{
|
||||
Total = 0
|
||||
Current = 0
|
||||
@ -173,21 +181,24 @@ if ($Help) {
|
||||
Write-Host $Message -ForegroundColor $ForegroundColor
|
||||
}
|
||||
|
||||
Write-ColorOutput "新建简化部署脚本" "Green"
|
||||
Write-ColorOutput "历史内部部署脚本(已废弃)" "Yellow"
|
||||
Write-ColorOutput "========================================" "Green"
|
||||
Write-ColorOutput "此脚本不作为客户安装、升级或正式发布入口;当前请使用 deploy-registry.ps1。" "Yellow"
|
||||
Write-ColorOutput "默认禁止执行,确需排查历史链路时必须追加 -AcknowledgeLegacyInternalDeploy。" "Yellow"
|
||||
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 " -Server [地址] 服务器地址 (默认: 45.249.208.118)" "Gray"
|
||||
Write-ColorOutput " -Server [地址] 服务器地址,必须显式传入" "Gray"
|
||||
Write-ColorOutput " -Username [用户] SSH用户名 (默认: root)" "Gray"
|
||||
Write-ColorOutput " -KeyPath [路径] SSH私钥路径 (推荐)" "Gray"
|
||||
Write-ColorOutput " -EnvFile [路径] 指定构建期使用的环境变量文件" "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 " -HarborUsername [用户] Harbor用户名 (默认: admin)" "Gray"
|
||||
Write-ColorOutput " -HarborPassword [密码] Harbor密码 (默认: Harbor12345)" "Gray"
|
||||
Write-ColorOutput " -HarborPassword [密码] Harbor密码,必须显式传入" "Gray"
|
||||
Write-ColorOutput " -SkipCheck 跳过代码检查" "Gray"
|
||||
Write-ColorOutput " -SkipBuild 跳过Docker构建" "Gray"
|
||||
Write-ColorOutput " -SkipUpload 跳过镜像推送和远程部署" "Gray"
|
||||
@ -199,11 +210,7 @@ if ($Help) {
|
||||
Write-ColorOutput " -Help 显示此帮助信息" "Gray"
|
||||
Write-ColorOutput "" "White"
|
||||
Write-ColorOutput "示例:" "White"
|
||||
Write-ColorOutput " .\deploy-new-fixed.ps1" "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"
|
||||
Write-ColorOutput " .\deploy-new-fixed.ps1 -AcknowledgeLegacyInternalDeploy -Server 192.168.1.20 -Tag 1.0.1 -HarborUrl registry.example.com" "Yellow"
|
||||
exit 0
|
||||
}
|
||||
|
||||
@ -322,14 +329,14 @@ APP_PUBLIC_URL=http://$Server:4320
|
||||
NEXTAUTH_SECRET=$(New-Guid)
|
||||
|
||||
# 存储配置
|
||||
S3_ENDPOINT=https://s3.1080p.hk
|
||||
S3_BUCKET=jsxiangche
|
||||
S3_ACCESS_KEY_ID=AEvT1p0wSNjow3Asu9NV
|
||||
S3_SECRET_ACCESS_KEY=u8nzFl1S6dNoT5PQaQhkHz8Du6iX0hVnPswtvHhs
|
||||
S3_ENDPOINT=https://s3.example.com
|
||||
S3_BUCKET=placeholder-bucket
|
||||
S3_ACCESS_KEY_ID=placeholder-access-key
|
||||
S3_SECRET_ACCESS_KEY=placeholder-secret-key
|
||||
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_FILE_PATH=由壹点AI/
|
||||
S3_FILE_PATH=materials/
|
||||
|
||||
# 本地上传目录
|
||||
UPLOAD_DIR=./uploads
|
||||
@ -414,6 +421,91 @@ function Get-TempFileName {
|
||||
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连接(带重试机制)
|
||||
function Test-SSHConnection {
|
||||
param(
|
||||
@ -441,28 +533,29 @@ function Test-SSHConnection {
|
||||
$stderrFile = Get-TempFileName -Prefix "ssh_stderr"
|
||||
|
||||
try {
|
||||
# 使用 OpenSSH(增加超时时间到20秒)
|
||||
$sshArgs = @("-o", "BatchMode=yes", "-o", "ConnectTimeout=20", "-o", "ServerAliveInterval=5", "-o", "ServerAliveCountMax=3")
|
||||
|
||||
if ($KeyPath -and (Test-Path $KeyPath)) {
|
||||
$sshArgs += @("-i", $KeyPath)
|
||||
if ($attempt -eq 1) {
|
||||
Write-Info "使用SSH密钥: $KeyPath"
|
||||
}
|
||||
} elseif ($Password) {
|
||||
# 注意:SecureString 密码认证需要额外处理,当前主要使用 SSH 密钥
|
||||
$sshArgs += @("-o", "PasswordAuthentication=yes")
|
||||
if ($attempt -eq 1) {
|
||||
Write-Warning "密码认证支持有限,建议使用 SSH 密钥"
|
||||
Write-Info "使用 PuTTY plink 进行密码认证"
|
||||
}
|
||||
} else {
|
||||
if ($attempt -eq 1) {
|
||||
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) {
|
||||
Write-Success "SSH连接测试成功"
|
||||
@ -543,6 +636,7 @@ function Clean-RemoteDockerImages {
|
||||
param(
|
||||
[string]$Server,
|
||||
[string]$Username,
|
||||
[SecureString]$Password = $null,
|
||||
[string]$KeyPath,
|
||||
[string]$ImageName,
|
||||
[int]$KeepCount = 3
|
||||
@ -562,14 +656,14 @@ echo '--- 清理后的镜像列表 ---'
|
||||
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 {
|
||||
$sshArgs += @("${Username}@${Server}", $cleanCmd)
|
||||
$process = Start-Process -FilePath "ssh" -ArgumentList $sshArgs -NoNewWindow -PassThru -Wait
|
||||
$process = Start-RemoteProcess `
|
||||
-Server $Server `
|
||||
-Username $Username `
|
||||
-Password $Password `
|
||||
-KeyPath $KeyPath `
|
||||
-Command $cleanCmd `
|
||||
-ConnectTimeoutSeconds 30
|
||||
if ($process.ExitCode -eq 0) {
|
||||
Write-Success "服务器镜像清理完成"
|
||||
return $true
|
||||
@ -1017,27 +1111,31 @@ function Main {
|
||||
# 因为镜像已经在本地构建完成,所有层(包括基础镜像层)都已包含在镜像中
|
||||
# 在服务器上拉取镜像并部署(带重试机制)
|
||||
Write-Info "在服务器上拉取镜像并部署..."
|
||||
$composeCmd = "docker compose -f docker-compose.prod.yml"
|
||||
# 如果使用 Harbor,需要在服务器上先登录
|
||||
if ($Registry -eq "harbor") {
|
||||
$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 {
|
||||
# 先停止并删除旧容器,然后拉取新镜像,重新启动(使用 --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)) {
|
||||
$sshArgs += @("-i", $KeyPath)
|
||||
Write-Info "使用 OpenSSH 执行远程部署命令"
|
||||
$remoteCommandAvailable = $true
|
||||
} elseif ($Password) {
|
||||
Write-Warning "未提供 SSH 密钥,无法在非交互模式下使用密码。请手动执行部署命令。"
|
||||
$sshArgs = $null
|
||||
if (Get-PlinkPath) {
|
||||
Write-Info "使用 PuTTY plink 执行远程部署命令"
|
||||
$remoteCommandAvailable = $true
|
||||
} else {
|
||||
Write-Warning "本机未找到 plink.exe,无法使用密码自动部署。请手动执行部署命令。"
|
||||
}
|
||||
} else {
|
||||
Write-Warning "缺少 SSH 凭据,请手动执行部署命令。"
|
||||
$sshArgs = $null
|
||||
}
|
||||
|
||||
if ($sshArgs) {
|
||||
if ($remoteCommandAvailable) {
|
||||
$deployMaxRetries = 2
|
||||
$deployRetryDelay = 5
|
||||
$deployAttempt = 0
|
||||
@ -1050,9 +1148,14 @@ function Main {
|
||||
Start-Sleep -Seconds $deployRetryDelay
|
||||
}
|
||||
|
||||
$currentSshArgs = $sshArgs + @("${Username}@${Server}", $deployCmd)
|
||||
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) {
|
||||
Write-Success "部署成功"
|
||||
$deploySuccess = $true
|
||||
@ -1076,7 +1179,7 @@ function Main {
|
||||
}
|
||||
}
|
||||
|
||||
if (-not $sshTest -or -not $sshArgs) {
|
||||
if (-not $sshTest -or -not $remoteCommandAvailable) {
|
||||
Write-Warning "远程部署阶段未完成,已提供手动命令指引"
|
||||
} elseif ($sshTest) {
|
||||
Write-Info "开始对线上应用做一次健康检查..."
|
||||
@ -1095,8 +1198,8 @@ function Main {
|
||||
Clean-LocalDockerImages -ImageName $ImageName -KeepTag $Tag -KeepCount $KeepImageCount
|
||||
|
||||
# 清理服务器镜像
|
||||
if ($sshTest -and $sshArgs) {
|
||||
Clean-RemoteDockerImages -Server $Server -Username $Username -KeyPath $KeyPath -ImageName $ImageName -KeepCount $KeepImageCount
|
||||
if ($sshTest -and $remoteCommandAvailable) {
|
||||
Clean-RemoteDockerImages -Server $Server -Username $Username -Password $Password -KeyPath $KeyPath -ImageName $ImageName -KeepCount $KeepImageCount
|
||||
}
|
||||
|
||||
# 清理 Registry 镜像(仅适用于旧 Registry)
|
||||
@ -1136,18 +1239,18 @@ function Main {
|
||||
Write-Info "如果自动部署失败,请在服务器上手动执行:"
|
||||
Write-Info " cd $RemotePath"
|
||||
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 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 {
|
||||
Write-Info "镜像已推送到 Registry: $registryImage"
|
||||
Write-Info "如果自动部署失败,请在服务器上手动执行:"
|
||||
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 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"
|
||||
}
|
||||
@ -1167,4 +1270,3 @@ function Main {
|
||||
|
||||
# 执行主函数
|
||||
Main
|
||||
|
||||
|
||||
@ -3,21 +3,22 @@
|
||||
# 支持内网穿透,资源存储在MinIO存储桶
|
||||
# 从私有镜像仓库拉取镜像,无需源码
|
||||
# ============================================
|
||||
# Harbor 项目结构:
|
||||
# - flnxi-public (公开): Agent、runtime 依赖镜像
|
||||
# - flnxi (私有): 素材系统镜像,需 token 拉取
|
||||
# Registry 项目结构:
|
||||
# - flnxi/runtime:PostgreSQL、MinIO 等运行依赖镜像
|
||||
# - flnxi/material 或 flnxi/material-dev:素材系统正式版 / 测试版镜像
|
||||
# 客户交付必须在 .env 中显式填写 APP_IMAGE,避免误拉旧仓库或 latest。
|
||||
# ============================================
|
||||
|
||||
services:
|
||||
# PostgreSQL 数据库
|
||||
postgres:
|
||||
# 从 Harbor 的 flnxi-public 项目拉取(公开,无需登录)
|
||||
image: ${POSTGRES_IMAGE:-harbor.flnxi.com/flnxi-public/runtime/postgres:15-alpine}
|
||||
container_name: flnxi-postgres
|
||||
# 可由 .env 覆盖为私有 Registry runtime 镜像;默认走 Docker Hub 官方镜像。
|
||||
image: ${POSTGRES_IMAGE:-postgres:15-alpine}
|
||||
container_name: ${POSTGRES_CONTAINER_NAME:-flnxi-postgres}
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
POSTGRES_USER: ${POSTGRES_USER:-postgres}
|
||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-youyidian123456}
|
||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?POSTGRES_PASSWORD is required}
|
||||
POSTGRES_DB: ${POSTGRES_DB:-flnxi}
|
||||
POSTGRES_INITDB_ARGS: "--encoding=UTF-8"
|
||||
volumes:
|
||||
@ -33,14 +34,14 @@ services:
|
||||
# MinIO 对象存储
|
||||
# 使用特定版本号确保稳定性(符合项目宗旨:稳定性优先)
|
||||
minio:
|
||||
# 从 Harbor 的 flnxi-public 项目拉取(公开,无需登录)
|
||||
image: ${MINIO_IMAGE:-harbor.flnxi.com/flnxi-public/runtime/minio:RELEASE.2025-04-22T22-12-26Z}
|
||||
container_name: flnxi-minio
|
||||
# 可由 .env 覆盖为私有 Registry runtime 镜像;默认走官方镜像。
|
||||
image: ${MINIO_IMAGE:-minio/minio:RELEASE.2025-04-22T22-12-26Z}
|
||||
container_name: ${MINIO_CONTAINER_NAME:-flnxi-minio}
|
||||
restart: unless-stopped
|
||||
command: server /data --console-address ":9001"
|
||||
environment:
|
||||
MINIO_ROOT_USER: ${MINIO_ROOT_USER:-minioadmin}
|
||||
MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD:-minioadmin123456}
|
||||
MINIO_ROOT_USER: ${MINIO_ROOT_USER:?MINIO_ROOT_USER is required}
|
||||
MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD:?MINIO_ROOT_PASSWORD is required}
|
||||
volumes:
|
||||
- minio_data:/data
|
||||
ports:
|
||||
@ -55,32 +56,47 @@ services:
|
||||
retries: 3
|
||||
|
||||
# MinIO 初始化服务(创建bucket)
|
||||
# 使用 latest 标签确保可用性(mc 客户端版本兼容性好)
|
||||
minio-init:
|
||||
# 从 Harbor 的 flnxi-public 项目拉取(公开,无需登录)
|
||||
image: ${MINIO_MC_IMAGE:-harbor.flnxi.com/flnxi-public/runtime/minio-mc:latest}
|
||||
container_name: flnxi-minio-init
|
||||
# mc 客户端用于初始化桶;离线或客户现场可在 .env 中覆盖为私有 Registry 镜像。
|
||||
image: ${MINIO_MC_IMAGE:-minio/mc:RELEASE.2025-04-16T18-13-26Z}
|
||||
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:
|
||||
minio:
|
||||
condition: service_healthy
|
||||
entrypoint: >
|
||||
/bin/sh -c "
|
||||
entrypoint: |
|
||||
/bin/sh -c '
|
||||
set -e;
|
||||
sleep 5;
|
||||
mc alias set myminio http://minio:9000 ${MINIO_ROOT_USER:-minioadmin} ${MINIO_ROOT_PASSWORD:-minioadmin123456};
|
||||
mc mb myminio/${S3_BUCKET:-flnxi-materials} --ignore-existing;
|
||||
mc anonymous set download myminio/${S3_BUCKET:-flnxi-materials};
|
||||
echo 'MinIO bucket created and configured successfully';
|
||||
exit 0;
|
||||
"
|
||||
mc alias set myminio http://minio:9000 "$${MINIO_ROOT_USER}" "$${MINIO_ROOT_PASSWORD}";
|
||||
mc mb "myminio/$${S3_BUCKET}" --ignore-existing;
|
||||
PUBLIC_READ="$$(printf "%s" "$${S3_SET_ACL:-0}:$${S3_PUBLIC_READ:-0}" | tr "[:upper:]" "[:lower:]")";
|
||||
case "$$PUBLIC_READ" in
|
||||
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:
|
||||
- app-network
|
||||
|
||||
# 主应用(Pro版 - 从私有镜像仓库拉取,需 token)
|
||||
app:
|
||||
# 从 Harbor 的 flnxi 项目拉取(私有,需许可证 token 登录)
|
||||
# Agent 会在验证许可证后自动获取 token 并拉取此镜像
|
||||
image: ${APP_IMAGE:-harbor.flnxi.com/flnxi/flnxi-material:pro-${IMAGE_TAG:-latest}}
|
||||
container_name: flnxi-material
|
||||
# 客户交付必须显式填写完整 APP_IMAGE,例如:
|
||||
# APP_IMAGE=registry.example.com/flnxi/material/flnxi-material:1.0.1
|
||||
image: ${APP_IMAGE:?APP_IMAGE is required, for example registry.example.com/flnxi/material/flnxi-material:1.0.1}
|
||||
container_name: ${APP_CONTAINER_NAME:-flnxi-material}
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "${APP_PORT:-4320}:3000"
|
||||
@ -93,33 +109,34 @@ services:
|
||||
# - SERVER_IP 必须在安装脚本或 .env 中配置为「本机在局域网中的访问 IP」,例如:192.168.41.144
|
||||
# - 对于一键安装用户,install.sh 会自动生成 .env.production 并写入 SERVER_IP,这里直接引用即可
|
||||
- NODE_ENV=production
|
||||
# DATABASE_URL 从 .env 文件自动读取(install.sh 会生成正确编码的 URL)
|
||||
# 如果 .env 文件中没有 DATABASE_URL,则使用默认值(向后兼容)
|
||||
- DATABASE_URL=${DATABASE_URL:-postgresql://${POSTGRES_USER:-postgres}:${POSTGRES_PASSWORD:-youyidian123456}@postgres:5432/${POSTGRES_DB:-flnxi}}
|
||||
# DATABASE_URL 从 .env 文件自动读取;未配置时按同一份 .env 中的数据库变量拼接
|
||||
- DATABASE_URL=${DATABASE_URL:-postgresql://${POSTGRES_USER:-postgres}:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB:-flnxi}}
|
||||
- DATABASE_DRIVER=node
|
||||
- STORAGE_PROVIDER=s3
|
||||
- S3_ENDPOINT=http://minio:9000
|
||||
- S3_BUCKET=${S3_BUCKET:-flnxi-materials}
|
||||
- S3_ACCESS_KEY_ID=${MINIO_ROOT_USER:-minioadmin}
|
||||
- S3_SECRET_ACCESS_KEY=${MINIO_ROOT_PASSWORD:-minioadmin123456}
|
||||
- S3_ACCESS_KEY_ID=${MINIO_ROOT_USER}
|
||||
- S3_SECRET_ACCESS_KEY=${MINIO_ROOT_PASSWORD}
|
||||
- S3_REGION=us-east-1
|
||||
# 对外访问 MinIO 的地址(供前端和分享链接使用),必须是局域网 / 公网可访问的 IP 或域名
|
||||
# 这里不再默认 127.0.0.1,避免在 NAS 等环境下写死 localhost
|
||||
- S3_PUBLIC_DOMAIN=http://${SERVER_IP}:${MINIO_API_PORT:-9000}/${S3_BUCKET:-flnxi-materials}
|
||||
- S3_ENABLE_PATH_STYLE=1
|
||||
- S3_SET_ACL=1
|
||||
- S3_SET_ACL=${S3_SET_ACL:-0}
|
||||
- S3_PREVIEW_URL_EXPIRE_IN=7200
|
||||
- S3_FILE_PATH=materials/
|
||||
- NEXT_PUBLIC_S3_FILE_PATH=materials/
|
||||
# NextAuth 的外部访问地址,从 .env(.production) 中注入(不在这里拼接,以免 SERVER_IP 为空时产生 http://:4320)
|
||||
# install.sh / 部署脚本会写入 NEXTAUTH_URL,例如:http://192.168.41.144:4320
|
||||
- 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 使用)
|
||||
- INIT_ADMIN_EMAIL=${INIT_ADMIN_EMAIL:-admin@flnxi.com}
|
||||
- INIT_ADMIN_PASSWORD=${INIT_ADMIN_PASSWORD:-88888888}
|
||||
- INIT_ADMIN_EMAIL=${INIT_ADMIN_EMAIL:-admin@local.flnxi}
|
||||
- INIT_ADMIN_PASSWORD=${INIT_ADMIN_PASSWORD:-}
|
||||
- SYSTEM_INIT_TOKEN=${SYSTEM_INIT_TOKEN:-}
|
||||
volumes:
|
||||
- ./uploads:/app/uploads
|
||||
- ./data:/app/data
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
@ -130,19 +147,22 @@ services:
|
||||
networks:
|
||||
- app-network
|
||||
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
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
|
||||
volumes:
|
||||
postgres_data:
|
||||
name: ${POSTGRES_VOLUME_NAME:-flnxi-postgres-data}
|
||||
driver: local
|
||||
minio_data:
|
||||
name: ${MINIO_VOLUME_NAME:-flnxi-minio-data}
|
||||
driver: local
|
||||
|
||||
networks:
|
||||
app-network:
|
||||
name: ${APP_NETWORK_NAME:-flnxi-app-network}
|
||||
driver: bridge
|
||||
|
||||
|
||||
|
||||
24
docker-compose.source-test.yml
Normal file
24
docker-compose.source-test.yml
Normal 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}
|
||||
@ -28,7 +28,7 @@ services:
|
||||
# DATABASE_DRIVER=${DATABASE_DRIVER}
|
||||
# 站点公共URL(可选,优先用于生成邀请链接等)
|
||||
# APP_PUBLIC_URL=${APP_PUBLIC_URL}
|
||||
- KEY_VAULTS_SECRET=/7DcksaICAECGjB38O3s8AE0wj2NoGzwIog0iUUMugY=
|
||||
- KEY_VAULTS_SECRET=${KEY_VAULTS_SECRET:-}
|
||||
- VECTOR_DB=pgvector
|
||||
- PGVECTOR_DB_URL=${DATABASE_URL}
|
||||
- STORAGE_PROVIDER=s3
|
||||
@ -46,11 +46,11 @@ services:
|
||||
# NextAuth配置从.env文件自动读取
|
||||
# NEXTAUTH_URL=${NEXTAUTH_URL}
|
||||
# NEXTAUTH_SECRET=${NEXTAUTH_SECRET}
|
||||
- GEMINI_API_KEY=sk-NqhrkeBudn6pUtStnOuOnGyDfasogvFSsex4YgjDXJU3SK3E
|
||||
- GEMINI_API_BASE_URL=https://api.huandutech.com/v1
|
||||
- GEMINI_MODEL=gemini-2.0-flash-exp
|
||||
- DINGTALK_CLIENT_ID=ding1bs1gxwrcfu16x0f
|
||||
- DINGTALK_CLIENT_SECRET=qUmj_gBDzpPEYR3RA3hRqKzwu11a986O-wKfP8V5lDHXn7XJrXxW79KIeP5G-mcY
|
||||
- GEMINI_API_KEY=${GEMINI_API_KEY:-}
|
||||
- GEMINI_API_BASE_URL=${GEMINI_API_BASE_URL:-}
|
||||
- GEMINI_MODEL=${GEMINI_MODEL:-}
|
||||
- DINGTALK_CLIENT_ID=${DINGTALK_CLIENT_ID:-}
|
||||
- DINGTALK_CLIENT_SECRET=${DINGTALK_CLIENT_SECRET:-}
|
||||
volumes:
|
||||
- ./uploads:/app/uploads
|
||||
healthcheck:
|
||||
|
||||
38
scripts/__tests__/deploy-registry-script.test.ts
Normal file
38
scripts/__tests__/deploy-registry-script.test.ts
Normal 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`"')
|
||||
})
|
||||
})
|
||||
69
scripts/__tests__/docker-entrypoint.test.ts
Normal file
69
scripts/__tests__/docker-entrypoint.test.ts
Normal 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)
|
||||
})
|
||||
})
|
||||
@ -1,5 +1,6 @@
|
||||
# 素材系统镜像构建脚本
|
||||
# 支持测试版和正式版分离
|
||||
# 素材系统镜像构建脚本(历史/内部备用)
|
||||
# 当前客户交付主线请使用根目录 deploy-registry.ps1。
|
||||
# 本脚本保留测试/正式分层的旧约定,但默认 Harbor/latest 口径不再作为客户升级依据。
|
||||
|
||||
param(
|
||||
[Parameter(Mandatory=$true)]
|
||||
@ -10,7 +11,8 @@ param(
|
||||
[string]$HarborHost = "harbor.flnxi.com",
|
||||
[string]$HarborProject = "flnxi",
|
||||
[switch]$Push = $false,
|
||||
[switch]$NoBuild = $false
|
||||
[switch]$NoBuild = $false,
|
||||
[switch]$AcknowledgeLegacyHarborPush = $false
|
||||
)
|
||||
|
||||
# Harbor 镜像路径规划:
|
||||
@ -30,6 +32,16 @@ param(
|
||||
|
||||
$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"
|
||||
switch ($Environment) {
|
||||
@ -78,6 +90,28 @@ try {
|
||||
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) {
|
||||
Write-Host "开始构建镜像..." -ForegroundColor Cyan
|
||||
@ -149,10 +183,11 @@ Write-Host " 最新标签: $LatestImageName" -ForegroundColor Gray
|
||||
Write-Host ""
|
||||
|
||||
if ($Environment -eq "release") {
|
||||
Write-Host "提示: 正式版镜像已发布,客户可通过以下地址拉取:" -ForegroundColor Yellow
|
||||
Write-Host " $HarborHost/$HarborProject/material/flnxi-material:latest" -ForegroundColor White
|
||||
Write-Host "NOTICE: release image built. Customer delivery must pin the full version image, not latest:" -ForegroundColor Yellow
|
||||
Write-Host " $FullImageName" -ForegroundColor White
|
||||
Write-Host "Current release/upgrade/rollback entry is root deploy-registry.ps1." -ForegroundColor Yellow
|
||||
} else {
|
||||
Write-Host "提示: 测试版镜像已发布到 material-dev 目录" -ForegroundColor Yellow
|
||||
Write-Host " 测试完成后,使用以下命令发布正式版:" -ForegroundColor Gray
|
||||
Write-Host " .\scripts\build-material-image.ps1 -Environment release -Version $Tag -Push" -ForegroundColor White
|
||||
Write-Host "NOTICE: test image built under material-dev." -ForegroundColor Yellow
|
||||
Write-Host " After testing, publish release with:" -ForegroundColor Gray
|
||||
Write-Host " .\deploy-registry.ps1 -Action PublishRelease -Version $Tag -RegistryHost <registry-host> -RegistryPassword `$registryPassword" -ForegroundColor White
|
||||
}
|
||||
|
||||
@ -39,7 +39,9 @@ echo "[2/3] 执行数据库迁移..."
|
||||
if npx prisma migrate deploy; then
|
||||
echo "✓ 数据库迁移完成"
|
||||
else
|
||||
echo "⚠ 数据库迁移失败,尝试继续启动..."
|
||||
echo "✗ 数据库迁移失败,已停止启动以保护数据一致性"
|
||||
echo "请先检查迁移日志、数据库连接和备份状态,修复后再重新启动容器"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 3. 启动应用(系统初始化会在首次请求时自动执行)
|
||||
|
||||
198
scripts/generate-shared-test-env.ps1
Normal file
198
scripts/generate-shared-test-env.ps1
Normal 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"
|
||||
384
发布管理.ps1
384
发布管理.ps1
@ -1,28 +1,42 @@
|
||||
# ================================================================
|
||||
# FLNXI 素材系统 - 发布管理工具 v2.0
|
||||
# 功能:统一管理测试版部署、正式版发布、Agent 更新等操作
|
||||
# 特性:进度条显示、健康检查、智能重试、趣味提示
|
||||
# FLNXI 素材系统 - 历史内部发布管理工具 v2.0
|
||||
# 状态:已废弃,不作为客户安装、升级或正式发布入口
|
||||
# 当前正式发布入口请使用 deploy-registry.ps1
|
||||
# ================================================================
|
||||
|
||||
param(
|
||||
[switch]$Help
|
||||
[switch]$Help,
|
||||
[switch]$AcknowledgeLegacyInternalPublish
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
||||
|
||||
if (-not $Help -and -not $AcknowledgeLegacyInternalPublish) {
|
||||
Write-Host "[ERROR] 发布管理.ps1 是历史内部发布控制台,默认禁止执行。" -ForegroundColor Red
|
||||
Write-Host "当前客户/测试发布请使用 deploy-registry.ps1。" -ForegroundColor Yellow
|
||||
Write-Host "该脚本包含旧服务器、旧 Harbor、许可证服务和 Docker 清理逻辑,不应作为客户交付入口。" -ForegroundColor Yellow
|
||||
Write-Host "如确需排查历史链路,请追加 -AcknowledgeLegacyInternalPublish。" -ForegroundColor Yellow
|
||||
exit 1
|
||||
}
|
||||
|
||||
# 配置
|
||||
$Config = @{
|
||||
Server = "45.249.208.118"
|
||||
SshKey = "$env:USERPROFILE\.ssh\youyidian_prod_ed25519"
|
||||
HarborHost = "harbor.flnxi.com"
|
||||
HarborProject = "flnxi" # 私有项目(素材系统)
|
||||
Server = ""
|
||||
SshKey = ""
|
||||
HarborHost = ""
|
||||
HarborProject = "flnxi" # 公开项目(素材系统)
|
||||
HarborPublicProject = "flnxi-public" # 公开项目(Agent、runtime)
|
||||
DockerHubUser = "flnxi"
|
||||
HealthCheckPort = 4320
|
||||
HealthCheckTimeout = 45
|
||||
SshRetryCount = 3
|
||||
SshRetryDelay = 2
|
||||
# 新许可证服务器配置
|
||||
LicenseServer = ""
|
||||
LicenseServerUser = "root"
|
||||
LicenseServerSshKey = ""
|
||||
LicenseServerPort = 9527
|
||||
}
|
||||
|
||||
# 趣味提示
|
||||
@ -98,7 +112,7 @@ function Complete-Stage {
|
||||
# 确认操作
|
||||
function Confirm-Action {
|
||||
param([string]$Message, [string]$Warning = "")
|
||||
|
||||
|
||||
Write-Host ""
|
||||
if ($Warning) {
|
||||
Write-Color "⚠️ 警告: $Warning" "Yellow"
|
||||
@ -118,6 +132,11 @@ function Get-CurrentVersion {
|
||||
}
|
||||
|
||||
function Get-AgentVersion {
|
||||
# 优先读取 Go 版本
|
||||
if (Test-Path "flnxi-agent-go/VERSION.txt") {
|
||||
return (Get-Content "flnxi-agent-go/VERSION.txt" -Raw).Trim()
|
||||
}
|
||||
# 兼容旧版 Node.js 版本
|
||||
if (Test-Path "flnxi-agent/VERSION.txt") {
|
||||
return (Get-Content "flnxi-agent/VERSION.txt" -Raw).Trim()
|
||||
}
|
||||
@ -128,21 +147,21 @@ function Get-AgentVersion {
|
||||
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
|
||||
}
|
||||
@ -150,13 +169,13 @@ function Test-SSHConnection {
|
||||
# 健康检查
|
||||
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
|
||||
@ -172,7 +191,7 @@ function Test-RemoteHealth {
|
||||
Start-Sleep -Seconds $interval
|
||||
$elapsed += $interval
|
||||
}
|
||||
|
||||
|
||||
Write-Warning "健康检查超时 (${MaxWait}s),请手动检查"
|
||||
return $false
|
||||
}
|
||||
@ -180,22 +199,22 @@ function Test-RemoteHealth {
|
||||
# 清理本地 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"
|
||||
@ -205,7 +224,7 @@ function Clean-LocalImages {
|
||||
docker rmi -f $imgId 2>&1 | Out-Null
|
||||
$removedCount++
|
||||
}
|
||||
|
||||
|
||||
if ($removedCount -gt 0) {
|
||||
Write-Success " 已清理 $removedCount 个旧镜像"
|
||||
} else {
|
||||
@ -217,17 +236,17 @@ function Clean-LocalImages {
|
||||
# 清理远程服务器镜像
|
||||
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 {
|
||||
@ -246,7 +265,7 @@ function Show-Menu {
|
||||
Write-Color "║ [1] 构建并部署测试版 ║" "White"
|
||||
Write-Color "║ ║" "Cyan"
|
||||
Write-Color "║ === 正式发布 (需要确认) === ║" "Yellow"
|
||||
Write-Color "║ [2] 发布素材系统正式版 (flnxi 私有) ⚠️ ║" "Yellow"
|
||||
Write-Color "║ [2] 发布素材系统正式版 (flnxi 公开) ⚠️ ║" "Yellow"
|
||||
Write-Color "║ [3] 发布 Agent (flnxi-public 公开) ⚠️ ║" "Yellow"
|
||||
Write-Color "║ ║" "Cyan"
|
||||
Write-Color "║ === 基础设施 === ║" "White"
|
||||
@ -273,18 +292,39 @@ function Show-Menu {
|
||||
# 1. 构建并部署测试版
|
||||
function Deploy-TestVersion {
|
||||
Write-Title "构建并部署测试版"
|
||||
|
||||
|
||||
$tag = "test-$(Get-Date -Format 'yyyyMMdd')"
|
||||
Write-Info "测试版标签: $tag"
|
||||
Write-Info "目标: harbor.flnxi.com/flnxi/material-dev/flnxi-material:$tag"
|
||||
Write-Host ""
|
||||
|
||||
|
||||
# 启用 BuildKit 以使用缓存加速
|
||||
$env:DOCKER_BUILDKIT = "1"
|
||||
|
||||
|
||||
# 优先加载本地基础镜像(避免网络问题)
|
||||
$LocalImagePath = "docker-images/node/node-18-alpine.tar"
|
||||
if (Test-Path $LocalImagePath) {
|
||||
Write-Info "检查本地基础镜像..."
|
||||
$existingImage = docker images node:18-alpine --format "{{.ID}}" 2>$null
|
||||
if (-not $existingImage) {
|
||||
Write-Info "加载本地基础镜像: $LocalImagePath"
|
||||
docker load -i $LocalImagePath
|
||||
if ($LASTEXITCODE -eq 0) {
|
||||
Write-Success "本地基础镜像加载成功"
|
||||
} else {
|
||||
Write-Warning "本地基础镜像加载失败,将尝试在线拉取"
|
||||
}
|
||||
} else {
|
||||
Write-Success "本地基础镜像已存在"
|
||||
}
|
||||
} else {
|
||||
Write-Warning "未找到本地基础镜像 ($LocalImagePath),将使用在线拉取"
|
||||
Write-Info "建议: 运行 'docker save node:18-alpine -o $LocalImagePath' 保存基础镜像"
|
||||
}
|
||||
|
||||
Write-Info "开始构建 (BuildKit 缓存已启用)..."
|
||||
Write-Info "提示: 首次构建较慢,后续构建会利用缓存加速"
|
||||
|
||||
|
||||
# 构建镜像
|
||||
$buildArgs = @(
|
||||
"build",
|
||||
@ -294,25 +334,25 @@ function Deploy-TestVersion {
|
||||
"-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 = @"
|
||||
@ -321,17 +361,17 @@ docker pull $($Config.HarborHost)/$($Config.HarborProject)/material-dev/flnxi-ma
|
||||
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 "测试版部署"
|
||||
}
|
||||
@ -344,16 +384,16 @@ docker compose up -d
|
||||
# 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" `
|
||||
@ -361,14 +401,35 @@ function Publish-ReleaseVersion {
|
||||
Write-Warning "取消发布"
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
# 更新版本文件
|
||||
$newVersion | Out-File -FilePath "VERSION.txt" -Encoding UTF8 -NoNewline
|
||||
Write-Success "版本文件已更新: $newVersion"
|
||||
|
||||
|
||||
# 启用 BuildKit
|
||||
$env:DOCKER_BUILDKIT = "1"
|
||||
|
||||
|
||||
# 优先加载本地基础镜像(避免网络问题)
|
||||
$LocalImagePath = "docker-images/node/node-18-alpine.tar"
|
||||
if (Test-Path $LocalImagePath) {
|
||||
Write-Info "检查本地基础镜像..."
|
||||
$existingImage = docker images node:18-alpine --format "{{.ID}}" 2>$null
|
||||
if (-not $existingImage) {
|
||||
Write-Info "加载本地基础镜像: $LocalImagePath"
|
||||
docker load -i $LocalImagePath
|
||||
if ($LASTEXITCODE -eq 0) {
|
||||
Write-Success "本地基础镜像加载成功"
|
||||
} else {
|
||||
Write-Warning "本地基础镜像加载失败,将尝试在线拉取"
|
||||
}
|
||||
} else {
|
||||
Write-Success "本地基础镜像已存在"
|
||||
}
|
||||
} else {
|
||||
Write-Warning "未找到本地基础镜像 ($LocalImagePath),将使用在线拉取"
|
||||
Write-Info "建议: 运行 'docker save node:18-alpine -o $LocalImagePath' 保存基础镜像"
|
||||
}
|
||||
|
||||
# 构建正式版镜像
|
||||
Write-Info "构建正式版镜像 (BuildKit 缓存已启用)..."
|
||||
$buildArgs = @(
|
||||
@ -380,19 +441,19 @@ function Publish-ReleaseVersion {
|
||||
"--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 "客户可通过以下地址拉取:"
|
||||
@ -402,29 +463,30 @@ function Publish-ReleaseVersion {
|
||||
}
|
||||
}
|
||||
|
||||
# 3. 发布 Agent 安装程序
|
||||
# 3. 发布 Agent 安装程序 (Go 版本)
|
||||
function Publish-Agent {
|
||||
Write-Title "发布 Agent 安装程序"
|
||||
|
||||
Write-Title "发布 Agent 安装程序 (Go 版本)"
|
||||
|
||||
$currentVersion = Get-AgentVersion
|
||||
Write-Info "当前 Agent 版本: $currentVersion"
|
||||
|
||||
Write-Info "技术栈: Go (镜像约 70MB)"
|
||||
|
||||
# Harbor 公开项目镜像配置
|
||||
# 格式: harbor.flnxi.com/flnxi-public/agent:版本号
|
||||
$harborRepo = "$($Config.HarborHost)/$($Config.HarborPublicProject)/agent"
|
||||
|
||||
|
||||
$newVersion = Read-Host "请输入新版本号 (当前: $currentVersion)"
|
||||
if ([string]::IsNullOrWhiteSpace($newVersion)) {
|
||||
Write-Warning "取消发布"
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
Write-Host ""
|
||||
Write-Info "镜像将推送到 Harbor flnxi-public 项目 (公开,无需登录即可拉取):"
|
||||
Write-Info " ${harborRepo}:${newVersion}"
|
||||
Write-Info " ${harborRepo}:latest"
|
||||
Write-Host ""
|
||||
|
||||
|
||||
# 确认
|
||||
if (-not (Confirm-Action `
|
||||
"将发布 Agent $newVersion 到 Harbor (flnxi-public)" `
|
||||
@ -432,7 +494,7 @@ function Publish-Agent {
|
||||
Write-Warning "取消发布"
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
# 检查 Harbor 登录
|
||||
$loginCheck = docker login $Config.HarborHost 2>&1
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
@ -440,47 +502,54 @@ function Publish-Agent {
|
||||
return
|
||||
}
|
||||
Write-Success "Harbor 登录状态: OK"
|
||||
|
||||
# 更新版本文件
|
||||
$newVersion | Out-File -FilePath "flnxi-agent/VERSION.txt" -Encoding UTF8 -NoNewline
|
||||
|
||||
# 更新版本文件 (Go 版本目录)
|
||||
$newVersion | Out-File -FilePath "flnxi-agent-go/VERSION.txt" -Encoding UTF8 -NoNewline
|
||||
Write-Success "Agent 版本文件已更新: $newVersion"
|
||||
|
||||
|
||||
# 启用 BuildKit
|
||||
$env:DOCKER_BUILDKIT = "1"
|
||||
|
||||
# 构建 Agent 镜像
|
||||
Write-Info "构建 Agent 镜像 (BuildKit 缓存已启用)..."
|
||||
Push-Location "flnxi-agent"
|
||||
|
||||
|
||||
# 构建 Agent 镜像 (Go 版本)
|
||||
Write-Info "构建 Agent 镜像 (Go 版本, BuildKit 缓存已启用)..."
|
||||
Push-Location "flnxi-agent-go"
|
||||
|
||||
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 "构建完成"
|
||||
|
||||
|
||||
# 显示镜像大小
|
||||
$imageSize = docker images "${harborRepo}:${newVersion}" --format "{{.Size}}"
|
||||
Write-Info "镜像大小: $imageSize"
|
||||
|
||||
# 推送到 Harbor flnxi-public 项目
|
||||
Write-Info "推送到 Harbor (flnxi-public)..."
|
||||
docker push "${harborRepo}:${newVersion}"
|
||||
docker push "${harborRepo}:latest"
|
||||
|
||||
|
||||
if ($LASTEXITCODE -eq 0) {
|
||||
Write-Success "Agent $newVersion 发布完成!"
|
||||
Write-Host ""
|
||||
Write-Color "═══════════════════════════════════════════════════════════" "Cyan"
|
||||
Write-Info "镜像地址: ${harborRepo}:latest"
|
||||
Write-Info "镜像大小: $imageSize (Go 版本,比 Node.js 版本减少 ~70%)"
|
||||
Write-Info "客户安装命令:"
|
||||
Write-Color " docker run -d -p 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"
|
||||
} else {
|
||||
Write-Error "推送失败"
|
||||
@ -494,7 +563,7 @@ function Publish-Agent {
|
||||
# 4. 同步 Runtime 镜像
|
||||
function Sync-RuntimeImages {
|
||||
Write-Title "同步 Runtime 镜像到 Harbor (flnxi-public)"
|
||||
|
||||
|
||||
# Runtime 镜像列表(含国内镜像源)
|
||||
# 推送到 flnxi-public 公开项目,无需登录即可拉取
|
||||
$runtimeImages = @(
|
||||
@ -507,13 +576,13 @@ function Sync-RuntimeImages {
|
||||
@{ 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) {
|
||||
@ -522,20 +591,20 @@ function Sync-RuntimeImages {
|
||||
}
|
||||
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) {
|
||||
@ -549,7 +618,7 @@ function Sync-RuntimeImages {
|
||||
Write-Warning " 国内镜像源失败,尝试官方源..."
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
# 回退到官方源
|
||||
if (-not $pullSuccess) {
|
||||
Write-Info " 从官方源拉取: $source"
|
||||
@ -559,18 +628,18 @@ function Sync-RuntimeImages {
|
||||
$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++
|
||||
@ -580,7 +649,7 @@ function Sync-RuntimeImages {
|
||||
}
|
||||
Write-Host ""
|
||||
}
|
||||
|
||||
|
||||
Write-Color "═══════════════════════════════════════════════════════════" "Cyan"
|
||||
Write-Color "同步完成 - 成功: $successCount, 失败: $failCount" $(if ($failCount -eq 0) { "Green" } else { "Yellow" })
|
||||
}
|
||||
@ -588,18 +657,18 @@ function Sync-RuntimeImages {
|
||||
# 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 ""
|
||||
@ -609,7 +678,7 @@ function Deploy-LicenseServer {
|
||||
Write-Color " [0] 取消" "Gray"
|
||||
Write-Host ""
|
||||
$deployChoice = Read-Host "选择"
|
||||
|
||||
|
||||
switch ($deployChoice) {
|
||||
"1" { Deploy-LicenseServer-Update }
|
||||
"2" { Deploy-LicenseServer-Fresh }
|
||||
@ -630,17 +699,17 @@ function Deploy-LicenseServer {
|
||||
# 更新许可证服务器(保留配置)
|
||||
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",
|
||||
@ -652,14 +721,14 @@ function Deploy-LicenseServer-Update {
|
||||
"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) {
|
||||
@ -668,7 +737,7 @@ function Deploy-LicenseServer-Update {
|
||||
Copy-Item -Path $src -Destination $tempDir -Recurse -Force
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
# 创建 public 目录(如果不存在)
|
||||
$publicDir = "license-server/public"
|
||||
if (Test-Path $publicDir) {
|
||||
@ -676,20 +745,20 @@ function Deploy-LicenseServer-Update {
|
||||
} 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 = @"
|
||||
@ -701,9 +770,9 @@ 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"
|
||||
@ -716,19 +785,19 @@ docker compose exec -T license-server npx prisma db push
|
||||
# 全新部署许可证服务器
|
||||
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"
|
||||
@ -737,12 +806,12 @@ function Deploy-LicenseServer-Fresh {
|
||||
} 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 {
|
||||
@ -754,20 +823,20 @@ function Deploy-LicenseServer-Fresh {
|
||||
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')
|
||||
@ -787,25 +856,25 @@ 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 = @"
|
||||
@ -834,9 +903,9 @@ async function init() {
|
||||
init().catch(console.error).finally(() => prisma.\$disconnect());
|
||||
"
|
||||
"@
|
||||
|
||||
|
||||
ssh -i $Config.SshKey "root@$($Config.Server)" $deployCmd
|
||||
|
||||
|
||||
if ($LASTEXITCODE -eq 0) {
|
||||
Write-Success "许可证服务器部署完成!"
|
||||
Write-Host ""
|
||||
@ -864,46 +933,46 @@ init().catch(console.error).finally(() => prisma.\$disconnect());
|
||||
# 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-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"
|
||||
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
|
||||
}
|
||||
@ -912,26 +981,19 @@ function Show-Status {
|
||||
|
||||
if ($Help) {
|
||||
Write-Color "╔═══════════════════════════════════════════════════════════╗" "Cyan"
|
||||
Write-Color "║ FLNXI 素材系统 - 发布管理工具 v2.0 ║" "Cyan"
|
||||
Write-Color "║ FLNXI 素材系统 - 历史内部发布工具 v2.0 ║" "Cyan"
|
||||
Write-Color "╠═══════════════════════════════════════════════════════════╣" "Cyan"
|
||||
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 "╠═══════════════════════════════════════════════════════════╣" "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 "║ 说明: ║" "White"
|
||||
Write-Color "║ 该脚本包含旧服务器、旧 Harbor、latest 和内部清理逻辑, ║" "Gray"
|
||||
Write-Color "║ 不作为客户安装、升级或正式发布入口。 ║" "Gray"
|
||||
Write-Color "╠═══════════════════════════════════════════════════════════╣" "Cyan"
|
||||
Write-Color "║ 特性: ║" "White"
|
||||
Write-Color "║ ✓ 进度条显示 ✓ 健康检查 ✓ SSH智能重试 ║" "Green"
|
||||
Write-Color "║ ✓ 国内镜像源加速 ✓ 趣味提示 ✓ 镜像清理 ║" "Green"
|
||||
Write-Color "║ 当前入口: deploy-registry.ps1 + docker-compose.prod.yml ║" "Green"
|
||||
Write-Color "╚═══════════════════════════════════════════════════════════╝" "Cyan"
|
||||
exit 0
|
||||
}
|
||||
@ -939,10 +1001,10 @@ if ($Help) {
|
||||
# 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 "本地镜像清理"
|
||||
}
|
||||
@ -950,15 +1012,15 @@ function Menu-CleanLocal {
|
||||
# 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 "服务器镜像清理"
|
||||
}
|
||||
@ -966,36 +1028,36 @@ function Menu-CleanRemote {
|
||||
# 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 设置中"
|
||||
@ -1007,7 +1069,7 @@ function Menu-DeepClean {
|
||||
while ($true) {
|
||||
Show-Menu
|
||||
$choice = Read-Host "请选择操作"
|
||||
|
||||
|
||||
switch ($choice) {
|
||||
"1" { Deploy-TestVersion }
|
||||
"2" { Publish-ReleaseVersion }
|
||||
@ -1018,13 +1080,13 @@ while ($true) {
|
||||
"7" { Menu-CleanLocal }
|
||||
"8" { Menu-CleanRemote }
|
||||
"9" { Menu-DeepClean }
|
||||
"0" {
|
||||
"0" {
|
||||
Write-Color "再见!$(Get-FunTip)" "Cyan"
|
||||
exit 0
|
||||
exit 0
|
||||
}
|
||||
default { Write-Warning "无效选项,请重新选择" }
|
||||
}
|
||||
|
||||
|
||||
Write-Host ""
|
||||
Read-Host "按 Enter 键继续..."
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user