easyai/start.ps1
wangbolhr ce042333c4 docs(win): 分行展示一键部署命令,避免 git 误解析参数
- README: Windows 克隆与启动改为分行,补充单行安全写法与说明
- start.ps1: 无 Git 时 winget/choco 自动安装、PATH 刷新、UTF-8 BOM 与文件头使用说明

Made-with: Cursor
2026-04-10 16:31:44 +08:00

409 lines
15 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
# git clone "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 "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-GitInstalled {
$git = Get-Command git -ErrorAction SilentlyContinue
if (-not $git) { return $false }
try { & git --version *> $null; return $true } catch { return $false }
}
function Refresh-SessionPath {
try {
$machine = [Environment]::GetEnvironmentVariable("Path", "Machine")
$user = [Environment]::GetEnvironmentVariable("Path", "User")
$parts = @($machine, $user) | Where-Object { $_ }
if ($parts.Count -gt 0) { $env:Path = ($parts -join ";") }
} catch { }
}
function Install-Git {
if (Get-Command winget -ErrorAction SilentlyContinue) {
Step "Installing Git via winget..."
Write-Log "winget install Git.Git"
winget install --id Git.Git -e --accept-source-agreements --accept-package-agreements
if ($LASTEXITCODE -eq 0) {
Start-Sleep -Seconds 2
Refresh-SessionPath
if (Test-GitInstalled) {
Ok "Git installed"
Write-Log "Git available after winget (PATH refreshed)"
return
}
Ok "Git installed. Close this terminal, open a new one, then run this script again."
Write-Log "Git installed via winget, user must refresh shell"
exit 0
}
}
if (Get-Command choco -ErrorAction SilentlyContinue) {
Step "Installing Git via Chocolatey..."
Write-Log "choco install git"
choco install git -y
if ($LASTEXITCODE -eq 0) {
Start-Sleep -Seconds 2
Refresh-SessionPath
if (Test-GitInstalled) {
Ok "Git installed"
Write-Log "Git available after choco (PATH refreshed)"
return
}
Ok "Git installed. Close this terminal, open a new one, then run this script again."
Write-Log "Git installed via choco, user must refresh shell"
exit 0
}
}
Warn "Could not install Git automatically. Please install manually:"
Write-Host " https://git-scm.com/download/win"
Write-Log "Git auto-install failed, user must install manually"
Wait-ForExit
exit 1
}
function Ensure-Git {
if ($env:DEPLOY_SKIP_GIT -eq "1") { return }
if ($env:CI -eq "true") { return }
if ($script:DeployDryRun) { return }
if (Test-GitInstalled) {
Ok "Git installed"
return
}
Write-Host ""
Write-Host "================================"
Write-Host " Git Check"
Write-Host "================================"
Write-Host ""
Warn "Git not found. Attempting automatic install..."
Write-Log "Git missing, running Install-Git"
Install-Git
}
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 {
if (Get-Command winget -ErrorAction SilentlyContinue) {
Step "Installing Docker Desktop via winget..."
winget install --id Docker.DockerDesktop -e --accept-source-agreements --accept-package-agreements
if ($LASTEXITCODE -eq 0) { Ok "Install started. Reopen terminal and rerun script."; exit 0 }
}
if (Get-Command choco -ErrorAction SilentlyContinue) {
Step "Installing Docker Desktop via choco..."
choco install docker-desktop -y
if ($LASTEXITCODE -eq 0) { Ok "Install started. Reopen terminal and rerun script."; exit 0 }
}
Warn "Please install Docker Desktop manually:"
Write-Host " $script:DockerDesktopUrl"
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]"
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
Ensure-Git
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 ==="