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]$AppImage, [string]$LicenseServerUrl, [string]$LicenseProjectCode = "flnxi-sucai", [string]$LicenseRequestSigningSecret, [string]$InitAdminEmail = "admin@test.flnxi.local" ) Set-StrictMode -Version Latest $ErrorActionPreference = "Stop" if ([string]::IsNullOrWhiteSpace($LicenseServerUrl)) { throw "必须显式传入 -LicenseServerUrl,避免把过期或错误的许可证地址写进测试环境。" } if ([string]::IsNullOrWhiteSpace($AppImage)) { throw "必须显式传入 -AppImage,测试环境也要固定完整镜像版本,不能依赖 latest。" } 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 if ([string]::IsNullOrWhiteSpace($LicenseRequestSigningSecret)) { $LicenseRequestSigningSecret = New-RandomString -Length 48 } $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 # Versioned application image APP_IMAGE=$AppImage # 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_REQUEST_SIGNING_SECRET=$LicenseRequestSigningSecret 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"