easyai/start.ps1
wangbolhr 5c856749c1
Some checks are pending
Test start.ps1 (Windows) / test-windows (push) Waiting to run
fix(deploy): 浅克隆说明、README 精简与 Docker 自动安装可观测性
- README: Windows 一条 PowerShell 命令,保留脚本权限说明;合并重复段落
- start.sh/ps1: git clone --depth 1 示例
- start.ps1: 选 [2] 时输出 winget/choco 日志与退出码,失败原因可辨;Read-Host Trim;winget 后若 docker 可用则视为成功

Made-with: Cursor
2026-04-10 17:09:18 +08:00

372 lines
14 KiB
PowerShell
Raw Blame History

This file contains ambiguous Unicode characters

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

#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: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
}
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"
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"
[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."
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) {
$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"
}
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 / 123456启动服务后用于登录" -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 "123456" -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 ==="