Files
easyai/start.ps1
T

421 lines
17 KiB
PowerShell

#Requires -Version 5.1
#
# 从 Git 克隆后首次部署(推荐分行复制执行,避免参数被误传给 git;浅克隆加 --depth 1):
# git clone --depth 1 "https://git.51easyai.com/wangbo/easyai.git" "easyai"
# Set-Location ".\easyai"
# powershell.exe -NoProfile -ExecutionPolicy Bypass -File ".\start.ps1"
#
# 若必须一行,请用分号分隔整条链,且 -File 后必须是带引号的脚本路径(不要用 Istart.ps1,应为 .\start.ps1):
# git clone --depth 1 "https://git.51easyai.com/wangbo/easyai.git" "easyai"; Set-Location "easyai"; powershell.exe -NoProfile -ExecutionPolicy Bypass -File ".\start.ps1"
#
# 若出现 error: unknown switch 'E':说明 -ExecutionPolicy 被交给了 git(可复现:git clone URL -ExecutionPolicy)。
# 常见原因:整段命令未用分号分隔、或把 powershell 与 git 写在同一行且参数被 git 解析。
# "Istart.ps1" 多为把 ".\start.ps1" 复制错(点反斜杠被看成字母 I)。
#
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
$OutputEncoding = [System.Text.Encoding]::UTF8
$ErrorActionPreference = "Stop"
$script:Root = if ($PSScriptRoot) { $PSScriptRoot } else { (Get-Location).Path }
$script:LogFile = Join-Path $script:Root "start.ps1.log"
$script:DeployDryRun = ($env:DEPLOY_DRY_RUN -eq "1")
$script:DeployIP = ""
$script:SkipDeployQuestions = $false
$script:SecurityEnvMode = "upgrade"
$script:DockerDesktopUrl = "https://desktop.docker.com/win/main/amd64/Docker%20Desktop%20Installer.exe"
function Write-Log {
param([string]$Message)
try {
Add-Content -Path $script:LogFile -Encoding UTF8 -Value ("[{0}] {1}" -f (Get-Date -Format "yyyy-MM-dd HH:mm:ss"), $Message)
} catch { }
}
function Wait-ForExit {
if ($env:CI -eq "true") { return }
if ($env:DEPLOY_NO_WAIT -eq "1") { return }
Write-Host ""
Read-Host "Press Enter to exit"
}
trap {
$msg = $_.Exception.Message
Write-Log "FATAL: $msg"
Write-Host ""
Write-Host "========================================" -ForegroundColor Red
Write-Host "Script failed. Log file: $script:LogFile" -ForegroundColor Red
Write-Host "========================================" -ForegroundColor Red
Write-Host $msg -ForegroundColor Red
Wait-ForExit
exit 1
}
function Step { param([string]$Message) Write-Host $Message }
function Ok { param([string]$Message) Write-Host "[OK] $Message" -ForegroundColor Green }
function Warn { param([string]$Message) Write-Host "[WARN] $Message" -ForegroundColor Yellow }
function Fail { param([string]$Message) Write-Host "[ERR] $Message" -ForegroundColor Red; throw $Message }
function Init-ProjectDir {
$composePath = Join-Path $script:Root "docker-compose.yml"
if (-not (Test-Path $composePath)) {
Fail "docker-compose.yml not found. Please run this script in easyai directory."
}
Set-Location $script:Root
Step "Project directory: $script:Root"
}
function Ensure-FileFromSample {
param([string]$Target,[string]$Sample)
if (Test-Path $Target) { return }
if (-not (Test-Path $Sample)) { Fail "Missing sample file: $Sample" }
Copy-Item $Sample $Target
Ok "$Target created"
}
function Get-DetectedLanIp {
try {
$ip = Get-NetIPAddress -AddressFamily IPv4 -ErrorAction SilentlyContinue | Where-Object {
$_.IPAddress -notmatch "^127\." -and
$_.IPAddress -notmatch "^169\.254\." -and
$_.InterfaceAlias -notmatch "Loopback"
} | Sort-Object InterfaceIndex | Select-Object -First 1 -ExpandProperty IPAddress
return $ip
} catch {
return $null
}
}
function Run-DeployQuestions {
Write-Host ""
Write-Host "================================"
Write-Host " EasyAI Deployment Config"
Write-Host "================================"
Write-Host ""
if ($env:DEPLOY_IP) {
$script:DeployIP = $env:DEPLOY_IP.Trim()
Step "Using DEPLOY_IP=$($script:DeployIP)"
return
}
if ($env:DEPLOY_ACCESS) {
$mode = $env:DEPLOY_ACCESS.Trim().ToLowerInvariant()
switch ($mode) {
{ $_ -in @("local", "localhost", "127.0.0.1") } {
$script:DeployIP = "127.0.0.1"
Step "Using DEPLOY_ACCESS=$($env:DEPLOY_ACCESS), IP=$($script:DeployIP)"
return
}
{ $_ -in @("lan", "ip") } {
Fail "DEPLOY_ACCESS=$($env:DEPLOY_ACCESS) requires DEPLOY_IP, for example: `$env:DEPLOY_IP='192.168.1.10'"
}
default {
Fail "DEPLOY_ACCESS only supports local or lan on Windows, current value: $($env:DEPLOY_ACCESS)"
}
}
}
if ($env:DEPLOY_NON_INTERACTIVE -eq "1" -or $env:CI -eq "true") {
Fail "Non-interactive deployment requires DEPLOY_IP or DEPLOY_ACCESS=local."
}
Write-Host "Choose mode:"
Write-Host " [1] Local only (127.0.0.1)"
Write-Host " [2] LAN access"
$choice = Read-Host "Select [1/2]"
switch ($choice) {
"1" { $script:DeployIP = "127.0.0.1"; Step "Selected local mode" }
"2" {
$detected = Get-DetectedLanIp
if ($detected) {
Write-Host "Detected LAN IP: $detected"
$inputIp = (Read-Host "Input LAN IP (Enter to use detected)").Trim()
$script:DeployIP = if ([string]::IsNullOrWhiteSpace($inputIp)) { $detected } else { $inputIp }
} else {
$inputIp = (Read-Host "Input LAN IP").Trim()
if ([string]::IsNullOrWhiteSpace($inputIp)) { Fail "IP cannot be empty." }
$script:DeployIP = $inputIp
}
Warn "Allow firewall ports: 3001, 3002, 3003"
}
default { Fail "Invalid selection." }
}
}
function Upsert-Env {
param([string]$Content,[string]$Key,[string]$Value)
$line = "$Key=$Value"
$pattern = "(?m)^$Key=.*$"
if ($Content -match $pattern) { return ($Content -replace $pattern, $line) }
if ($Content -and -not $Content.EndsWith("`n")) { $Content += "`n" }
return ($Content + $line + "`n")
}
function Setup-EnvFiles {
Write-Host ""
Step "Configuring env files..."
Ensure-FileFromSample ".env.tools" ".env.tools.sample"
Ensure-FileFromSample ".env.ASG" ".env.ASG.sample"
Ensure-FileFromSample ".env.AMS" ".env.AMS.sample"
if (-not (Test-Path ".env")) { $script:SecurityEnvMode = "new" }
Ensure-FileFromSample ".env" ".env.sample"
$content = Get-Content ".env" -Raw -Encoding UTF8
if (-not $content) { $content = "" }
$content = Upsert-Env $content "NUXT_PUBLIC_BASE_APIURL" "http://$($script:DeployIP):3001"
$content = Upsert-Env $content "NUXT_PUBLIC_BASE_SOCKETURL" "ws://$($script:DeployIP):3002"
$content = Upsert-Env $content "NUXT_PUBLIC_SG_APIURL" "http://$($script:DeployIP):3003"
$webPortMatch = [regex]::Match($content, '(?m)^WEB_PORT=(\d+)$')
$webPort = if ($webPortMatch.Success) { $webPortMatch.Groups[1].Value } else { "3010" }
$content = Upsert-Env $content "CONFIG_SECURITY_ORIGIN" "http://$($script:DeployIP):$webPort"
[System.IO.File]::WriteAllText((Join-Path $script:Root ".env"), $content, [System.Text.UTF8Encoding]::new($false))
Ok ".env configured for IP=$($script:DeployIP)"
}
function Test-DockerInstalled {
$docker = Get-Command docker -ErrorAction SilentlyContinue
if (-not $docker) { return $false }
try { & docker --version *> $null; return $true } catch { return $false }
}
function Test-DockerRunning {
param([int]$TimeoutSeconds = 10)
try {
$psi = New-Object System.Diagnostics.ProcessStartInfo
$psi.FileName = "docker"
$psi.Arguments = "info"
$psi.RedirectStandardOutput = $true
$psi.RedirectStandardError = $true
$psi.UseShellExecute = $false
$psi.CreateNoWindow = $true
$p = [System.Diagnostics.Process]::Start($psi)
$ok = $p.WaitForExit($TimeoutSeconds * 1000)
if (-not $ok) { try { $p.Kill() } catch { }; return $false }
return ($p.ExitCode -eq 0)
} catch {
return $false
}
}
function Start-DockerDesktopAndWait {
param([int]$MaxWaitSeconds = 120, [int]$CheckIntervalSeconds = 5)
Step "Docker not running, trying to start Docker Desktop..."
$path = Join-Path $env:ProgramFiles "Docker\Docker\Docker Desktop.exe"
if (-not (Test-Path $path)) { $path = Join-Path ${env:ProgramFiles(x86)} "Docker\Docker\Docker Desktop.exe" }
if (-not (Test-Path $path)) { Warn "Docker Desktop executable not found."; return $false }
$startedByCli = $false
try {
& docker desktop start *> $null
if ($LASTEXITCODE -eq 0) { $startedByCli = $true; Step "Sent docker desktop start command" }
} catch { }
if (-not $startedByCli) { Start-Process -FilePath $path -WindowStyle Hidden }
$elapsed = 0
while ($elapsed -lt $MaxWaitSeconds) {
Start-Sleep -Seconds $CheckIntervalSeconds
$elapsed += $CheckIntervalSeconds
if (Test-DockerRunning) { Ok "Docker engine ready"; return $true }
}
Warn "Timeout waiting for Docker."
return $false
}
function Ensure-DockerRunning {
if (-not (Test-DockerInstalled)) { return $false }
if (Test-DockerRunning) { Ok "Docker engine running"; return $true }
return (Start-DockerDesktopAndWait)
}
function Install-DockerDesktop {
function Test-Winget {
return [bool](Get-Command winget -ErrorAction SilentlyContinue)
}
function Test-Choco {
return [bool](Get-Command choco -ErrorAction SilentlyContinue)
}
if (Test-Winget) {
Step "Installing Docker Desktop via winget..."
Write-Log "winget install Docker.DockerDesktop"
$wingetOut = & winget install --id Docker.DockerDesktop -e --accept-source-agreements --accept-package-agreements 2>&1
$wingetOut | ForEach-Object { Write-Host $_ }
$wingetCode = $LASTEXITCODE
Write-Log "winget exit code: $wingetCode"
if ($wingetCode -eq 0) {
Ok "Install started. Reopen terminal and rerun script."
Write-Log "winget reported success"
exit 0
}
if (Test-DockerInstalled) {
Ok "Docker CLI detected after winget. Reopen terminal if docker commands fail, then rerun script."
Write-Log "winget exit $wingetCode but docker is available"
exit 0
}
Warn "winget did not report success (exit code: $wingetCode). Trying Chocolatey if available..."
} else {
Warn "winget not found (install 'App Installer' from Microsoft Store or use Windows 11 / recent Windows 10). Trying Chocolatey if available..."
}
if (Test-Choco) {
Step "Installing Docker Desktop via choco..."
Write-Log "choco install docker-desktop"
$chocoOut = & choco install docker-desktop -y 2>&1
$chocoOut | ForEach-Object { Write-Host $_ }
$chocoCode = $LASTEXITCODE
Write-Log "choco exit code: $chocoCode"
if ($chocoCode -eq 0) {
Ok "Install started. Reopen terminal and rerun script."
exit 0
}
if (Test-DockerInstalled) {
Ok "Docker CLI detected after choco. Reopen terminal if docker commands fail, then rerun script."
exit 0
}
Warn "choco did not report success (exit code: $chocoCode)."
} else {
Warn "Chocolatey (choco) not found."
}
Warn "Automatic install was not completed. Please install Docker Desktop manually:"
Write-Host " $script:DockerDesktopUrl"
Write-Log "Docker auto-install failed (winget/choco unavailable or non-zero exit)"
Wait-ForExit
exit 1
}
function Test-Docker {
Write-Host ""
Write-Host "================================"
Write-Host " Docker Check"
Write-Host "================================"
Write-Host ""
if (Test-DockerInstalled) {
Ok "Docker installed"
if (-not (Ensure-DockerRunning)) { Warn "Docker failed to start automatically."; Wait-ForExit; exit 1 }
return
}
Warn "Docker Desktop not detected."
$installMode = if ($env:DEPLOY_DOCKER_INSTALL) { $env:DEPLOY_DOCKER_INSTALL.Trim().ToLowerInvariant() } else { "" }
if (-not [string]::IsNullOrWhiteSpace($installMode)) {
switch ($installMode) {
{ $_ -in @("manual", "1") } { Start-Process $script:DockerDesktopUrl; Wait-ForExit; exit 1 }
{ $_ -in @("auto", "2") } { Install-DockerDesktop }
default { Fail "DEPLOY_DOCKER_INSTALL only supports manual or auto, current value: $($env:DEPLOY_DOCKER_INSTALL)" }
}
}
if ($env:DEPLOY_NON_INTERACTIVE -eq "1" -or $env:CI -eq "true") {
Fail "Docker Desktop not detected. Set DEPLOY_DOCKER_INSTALL=manual or DEPLOY_DOCKER_INSTALL=auto, or install Docker Desktop first."
}
Write-Host "Choose:"
Write-Host " [1] Manual install (open URL and exit)"
Write-Host " [2] Auto install (winget/choco)"
$choice = (Read-Host "Select [1/2]").Trim()
switch ($choice) {
"1" { Start-Process $script:DockerDesktopUrl; Wait-ForExit; exit 1 }
"2" { Install-DockerDesktop }
default { Fail "Invalid selection." }
}
}
function Start-Services {
Write-Host ""
Step "Starting EasyAI services..."
$useComposeV2 = $false
try { & docker compose version *> $null; if ($LASTEXITCODE -eq 0) { $useComposeV2 = $true } } catch { }
if ($useComposeV2) {
docker compose pull
if ($LASTEXITCODE -ne 0) { Fail "docker compose pull failed." }
docker compose up -d
if ($LASTEXITCODE -ne 0) { Fail "docker compose up failed." }
} else {
docker-compose pull
if ($LASTEXITCODE -ne 0) { Fail "docker-compose pull failed." }
docker-compose up -d
if ($LASTEXITCODE -ne 0) { Fail "docker-compose up failed." }
}
Ok "EasyAI started"
}
function Main {
Init-ProjectDir
if ((Test-Path ".env") -and -not $env:DEPLOY_FORCE_RECONFIG -and -not $env:DEPLOY_IP -and -not $env:DEPLOY_ACCESS) {
if ($env:DEPLOY_NON_INTERACTIVE -eq "1" -or $env:CI -eq "true") {
$line = Get-Content ".env" -Encoding UTF8 | Where-Object { $_ -like "NUXT_PUBLIC_BASE_APIURL=*" } | Select-Object -First 1
if ($line -and $line -like "*:3001*") { $script:DeployIP = ($line -replace ".*http://", "" -replace ":3001.*", "").Trim() }
$script:SkipDeployQuestions = $true
Step "Using existing env config"
} else {
$answer = (Read-Host "Existing .env found. Reconfigure? [y/N]").Trim().ToLower()
$yes = ($answer.Length -gt 0) -and ($answer[0] -eq [char]121)
if (-not $yes) {
$line = Get-Content ".env" -Encoding UTF8 | Where-Object { $_ -like "NUXT_PUBLIC_BASE_APIURL=*" } | Select-Object -First 1
if ($line -and $line -like "*:3001*") { $script:DeployIP = ($line -replace ".*http://", "" -replace ":3001.*", "").Trim() }
$script:SkipDeployQuestions = $true
Step "Using existing env config"
}
}
}
if (-not $script:SkipDeployQuestions) {
Run-DeployQuestions
Setup-EnvFiles
} else {
Ensure-FileFromSample ".env.tools" ".env.tools.sample"
Ensure-FileFromSample ".env.ASG" ".env.ASG.sample"
Ensure-FileFromSample ".env.AMS" ".env.AMS.sample"
}
. (Join-Path $script:Root "scripts\Initialize-SecurityEnv.ps1")
Initialize-SecurityEnv -Path (Join-Path $script:Root ".env") -Mode $script:SecurityEnvMode
if ($script:DeployDryRun) {
Warn "dry-run mode: skip docker and services"
} else {
Test-Docker
Start-Services
}
$ip = if ($script:DeployIP) { $script:DeployIP } else { "127.0.0.1" }
Write-Host ""
if ($script:DeployDryRun) {
Write-Host "================================" -ForegroundColor Yellow
Write-Host " 配置已生成(未启动 Docker 服务)" -ForegroundColor Yellow
Write-Host "================================" -ForegroundColor Yellow
Write-Host ""
Write-Host " 预期访问: " -NoNewline; Write-Host "http://${ip}:3010" -ForegroundColor Cyan
Write-Host ""
Write-Host " 初始管理员: admin;随机密码保存在 .env 的 CONFIG_INITIAL_ADMIN_PASSWORD" -ForegroundColor DarkGray
} else {
Write-Host "================================" -ForegroundColor Green
Write-Host " 部署成功" -ForegroundColor Green
Write-Host "================================" -ForegroundColor Green
Write-Host ""
Write-Host " 访问地址: " -NoNewline; Write-Host "http://${ip}:3010" -ForegroundColor Cyan
Write-Host ""
Write-Host " -------- 初始管理员(首次登录后请修改密码)--------" -ForegroundColor Yellow
Write-Host " 账号: " -NoNewline; Write-Host "admin" -ForegroundColor White
Write-Host " 密码位置: " -NoNewline; Write-Host ".env -> CONFIG_INITIAL_ADMIN_PASSWORD" -ForegroundColor White
}
Write-Host ""
Wait-ForExit
}
Write-Log "=== script start ==="
Write-Host "EasyAI deploy script starting..." -ForegroundColor Cyan
Write-Host "Log file: $script:LogFile"
Write-Host ""
Main
Write-Log "=== script end ==="