#Requires -Version 5.1 # EasyAI Windows 一键部署脚本 # 仅支持 IP 访问(本地/局域网),不含域名与 HTTPS # 一行命令: git clone https://git.51easyai.com/wangbo/easyai; cd easyai; .\start.ps1 # 设置控制台编码为 UTF-8,确保中文正确显示 [Console]::OutputEncoding = [System.Text.Encoding]::UTF8 $OutputEncoding = [System.Text.Encoding]::UTF8 $ErrorActionPreference = "Stop" $script:LogDir = $PSScriptRoot if (-not $script:LogDir) { $script:LogDir = $env:TEMP } $script:LogFile = Join-Path $script:LogDir "start.ps1.log" # 尽早写入启动标记,便于闪退时排查 try { "[$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')] === 脚本加载 ===" | Out-File -FilePath $script:LogFile -Append -Encoding utf8 } catch { } function Write-Log { param([string]$Msg) try { $line = "[$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')] $Msg" Add-Content -Path $script:LogFile -Value $line -Encoding UTF8 -ErrorAction SilentlyContinue } catch { } } trap { $errMsg = $_.Exception.Message $errStack = if ($_.ScriptStackTrace) { " | 堆栈: $($_.ScriptStackTrace)" } else { "" } try { $logPath = $script:LogFile if (-not $logPath) { $logPath = Join-Path $env:TEMP "start.ps1.log" } $logLine = "[$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')] 错误退出: $errMsg$errStack" Add-Content -Path $logPath -Value $logLine -Encoding UTF8 -ErrorAction SilentlyContinue } catch { } try { Write-Host "" Write-Host "========================================" -ForegroundColor Red Write-Host " 发生错误,详见日志: $script:LogFile" -ForegroundColor Red Write-Host "========================================" -ForegroundColor Red Write-Host $errMsg -ForegroundColor Red if ($errStack) { Write-Host $errStack } } catch { } Wait-ForExit exit 1 } # 结束时保持窗口不关闭,便于查看输出和 Debug(CI 环境自动跳过) function Wait-ForExit { if ($env:CI -eq "true") { return } Write-Host "" Read-Host "按 Enter 键退出" } # 仅配置模式:DEPLOY_DRY_RUN=1 只生成配置文件,不执行 Docker 安装和启动 $script:DeployDryRun = if ($env:DEPLOY_DRY_RUN -eq "1") { $true } else { $false } $script:DeployIP = "" $script:DeployModeSkip = $false # Docker Desktop for Windows 下载链接 $DockerDesktopUrl = "https://desktop.docker.com/win/main/amd64/Docker%20Desktop%20Installer.exe" function Write-Step { param($Msg) Write-Host $Msg } function Write-Ok { param($Msg) Write-Host " ✓ $Msg" -ForegroundColor Green } function Write-Err { param($Msg) Write-Host "❌ $Msg" -ForegroundColor Red; throw $Msg } function Write-Warn { param($Msg) Write-Host "⚠️ $Msg" -ForegroundColor Yellow } # ==================== 项目初始化 ==================== function Init-ProjectDir { $scriptDir = $PSScriptRoot if (-not $scriptDir) { throw "无法获取脚本所在目录。请在 easyai 目录下打开 PowerShell 执行: .\start.ps1" } $composePath = Join-Path $scriptDir "docker-compose.yml" if (Test-Path $composePath) { Set-Location $scriptDir Write-Step "📁 项目目录: $scriptDir" return } Write-Err "未找到 docker-compose.yml,请在 easyai 项目目录下运行 start.ps1" } # ==================== 配置问答 ==================== function Run-DeployQuestions { Write-Host "" Write-Host "================================" Write-Host " EasyAI 部署配置(Windows)" Write-Host "================================" Write-Host "" # 非交互模式:环境变量 DEPLOY_IP 已设置 if ($env:DEPLOY_IP) { $script:DeployIP = $env:DEPLOY_IP.Trim() Write-Step "使用环境变量: DEPLOY_IP=$($script:DeployIP)" return } Write-Host "请选择访问方式:" Write-Host " [1] 本地访问 (127.0.0.1) - 仅本机访问,无需放行端口" Write-Host " [2] 局域网访问 - 同网段设备访问,需放行 3001、3002、3003 端口" $choice = Read-Host "请选择 [1/2]" switch ($choice) { "1" { $script:DeployIP = "127.0.0.1" Write-Step "已选择本地访问" } "2" { # 自动获取局域网 IP(排除回环和 APIPA 地址) $detectedIp = $null try { $addrs = Get-NetIPAddress -AddressFamily IPv4 -ErrorAction SilentlyContinue | Where-Object { $_.InterfaceAlias -notlike "*Loopback*" -and $_.IPAddress -notmatch "^127\." -and $_.IPAddress -notmatch "^169\.254\." } $detectedIp = ($addrs | Sort-Object InterfaceIndex | Select-Object -First 1).IPAddress } catch { } if ($detectedIp) { $defaultHint = "回车使用 [$detectedIp]" Write-Host " 检测到局域网 IP: $detectedIp" $inputIp = Read-Host "请输入本机局域网 IP($defaultHint 或手动输入)" $inputIp = $inputIp.Trim() $script:DeployIP = if ([string]::IsNullOrWhiteSpace($inputIp)) { $detectedIp } else { $inputIp } } else { Write-Host " 提示: 未自动检测到局域网 IP,可在本机运行 ipconfig 查看" $lanIp = Read-Host "请输入本机局域网 IP 地址" $lanIp = $lanIp.Trim() if ([string]::IsNullOrWhiteSpace($lanIp)) { Write-Err "IP 不能为空" } $script:DeployIP = $lanIp } Write-Warn "请确保防火墙已放行 3001、3002、3003 端口" } default { Write-Err "无效选择" } } } # ==================== 生成配置文件 ==================== function Setup-EnvFiles { Write-Host "" Write-Step "📝 配置环境文件..." # 复制 .env.tools 和 .env.ASG if (-not (Test-Path ".env.tools")) { Copy-Item ".env.tools.sample" ".env.tools" Write-Ok ".env.tools" } if (-not (Test-Path ".env.ASG")) { Copy-Item ".env.ASG.sample" ".env.ASG" Write-Ok ".env.ASG" } # 配置 .env if (-not (Test-Path ".env")) { Copy-Item ".env.sample" ".env" } $content = Get-Content ".env" -Raw -Encoding UTF8 if (-not $content) { $content = "" } $content = $content -replace '(?m)^NUXT_PUBLIC_BASE_APIURL=.*', "NUXT_PUBLIC_BASE_APIURL=http://$($script:DeployIP):3001" $content = $content -replace '(?m)^NUXT_PUBLIC_BASE_SOCKETURL=.*', "NUXT_PUBLIC_BASE_SOCKETURL=ws://$($script:DeployIP):3002" $content = $content -replace '(?m)^NUXT_PUBLIC_SG_APIURL=.*', "NUXT_PUBLIC_SG_APIURL=http://$($script:DeployIP):3003" # 保持文件末尾换行 if ($content -and -not $content.EndsWith("`n")) { $content += "`n" } $envPath = Join-Path (Get-Location) ".env" [System.IO.File]::WriteAllText($envPath, $content, [System.Text.UTF8Encoding]::new($false)) Write-Ok ".env 已配置为 IP 模式 ($($script:DeployIP))" } # ==================== Docker 检测 ==================== function Test-DockerInstalled { $docker = Get-Command docker -ErrorAction SilentlyContinue if (-not $docker) { return $false } try { $null = & docker --version 2>&1 return $true } catch { return $false } } # 检查 Docker 引擎是否已启动并可响应(带超时,避免 docker info 卡住) 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) $exited = $p.WaitForExit($TimeoutSeconds * 1000) if (-not $exited) { try { $p.Kill() } catch { } return $false } return ($p.ExitCode -eq 0) } catch { return $false } } # 启动 Docker Desktop 并等待就绪 function Start-DockerDesktopAndWait { param( [int]$MaxWaitSeconds = 120, [int]$CheckIntervalSeconds = 5 ) Write-Step "Docker 未运行,正在启动 Docker Desktop..." $dockerDesktopPath = "${env:ProgramFiles}\Docker\Docker\Docker Desktop.exe" if (-not (Test-Path $dockerDesktopPath)) { $dockerDesktopPath = "${env:ProgramFiles(x86)}\Docker\Docker\Docker Desktop.exe" } if (-not (Test-Path $dockerDesktopPath)) { Write-Warn "未找到 Docker Desktop 可执行文件" Write-Host "请手动启动 Docker Desktop 后重新运行本脚本。" return $false } # 尝试使用 docker desktop start(Docker Desktop 4.38+ 支持) $useCli = $false try { $null = & docker desktop start 2>&1 $useCli = $true Write-Step "已通过 docker desktop start 发送启动命令" } catch { # 回退到直接启动进程 } if (-not $useCli) { Start-Process -FilePath $dockerDesktopPath -WindowStyle Hidden Write-Step "已启动 Docker Desktop 进程" } Write-Host "等待 Docker 引擎就绪(最长 $MaxWaitSeconds 秒)..." -ForegroundColor Cyan $elapsed = 0 while ($elapsed -lt $MaxWaitSeconds) { Start-Sleep -Seconds $CheckIntervalSeconds $elapsed += $CheckIntervalSeconds Write-Host " 已等待 $elapsed 秒..." -NoNewline if (Test-DockerRunning) { Write-Host " 完成" -ForegroundColor Green Write-Ok "Docker 已就绪" return $true } Write-Host "" } Write-Warn "等待超时,Docker 可能仍在启动中" Write-Host "请稍后手动执行 docker info 确认,或重新运行本脚本。" return $false } # 确保 Docker 已安装且运行,若未运行则尝试启动 function Ensure-DockerRunning { if (-not (Test-DockerInstalled)) { return $false } if (Test-DockerRunning) { Write-Ok "Docker 引擎已运行" $v = docker --version 2>$null if ($v) { Write-Host " $v" } return $true } return (Start-DockerDesktopAndWait) } function Install-DockerDesktop { $isWin = ($env:OS -eq "Windows_NT") -or ($PSVersionTable.PSVersion.Major -ge 6 -and $IsWindows) if (-not $isWin) { Write-Warn "自动安装仅支持 Windows。请手动安装 Docker Desktop。" Write-Host "下载地址: $DockerDesktopUrl" Wait-ForExit; exit 1 } # 优先 winget,其次 Chocolatey if (Get-Command winget -ErrorAction SilentlyContinue) { Write-Step "正在通过 winget 安装 Docker Desktop for Windows..." winget install --id Docker.DockerDesktop -e --accept-source-agreements --accept-package-agreements if ($LASTEXITCODE -eq 0) { Write-Ok "安装已启动。安装完成后请重启终端,然后重新运行本脚本。" exit 0 } } if (Get-Command choco -ErrorAction SilentlyContinue) { Write-Step "正在通过 Chocolatey 安装 Docker Desktop for Windows..." choco install docker-desktop -y if ($LASTEXITCODE -eq 0) { Write-Ok "安装已启动。安装完成后请重启终端,然后重新运行本脚本。" exit 0 } } Write-Warn "未找到 winget 或 Chocolatey,请手动安装 Docker Desktop。" Write-Host "下载地址: $DockerDesktopUrl" Wait-ForExit; exit 1 } function Test-Docker { Write-Host "" Write-Host "================================" Write-Host " Docker 检查" Write-Host "================================" Write-Host "" if (Test-DockerInstalled) { Write-Ok "Docker 已安装" # 检查并确保 Docker 引擎运行 if (-not (Ensure-DockerRunning)) { Write-Warn "Docker 未能自动启动" Write-Host "" Write-Host "请手动启动 Docker Desktop,确认其完全启动后再重新运行本脚本。" Wait-ForExit exit 1 } return } Write-Warn "未检测到 Docker Desktop for Windows" Write-Host "" Write-Host "请选择:" Write-Host " [1] 手动安装 - 打开下载页面,退出脚本" Write-Host " [2] 自动安装 - 通过 winget 或 Chocolatey 安装(需管理员权限)" $choice = Read-Host "请选择 [1/2]" switch ($choice) { "1" { Write-Host "" Write-Host "Docker Desktop for Windows 下载地址:" Write-Host " $DockerDesktopUrl" Write-Host "" Write-Host "安装完成后请重新运行本脚本。" if ($env:OS -eq "Windows_NT") { Start-Process $DockerDesktopUrl } Wait-ForExit; exit 1 } "2" { Install-DockerDesktop } default { Write-Err "无效选择" } } } # ==================== 启动服务 ==================== function Start-Services { Write-Host "" Write-Step "🚀 启动 EasyAI 服务..." $hasComposeV2 = $false try { $null = docker compose version 2>&1 $hasComposeV2 = $true } catch { } if ($hasComposeV2) { docker compose pull if ($LASTEXITCODE -ne 0) { Write-Err "docker compose pull 失败" } docker compose up -d if ($LASTEXITCODE -ne 0) { Write-Err "docker compose up 失败" } } else { docker-compose pull if ($LASTEXITCODE -ne 0) { Write-Err "docker-compose pull 失败" } docker-compose up -d if ($LASTEXITCODE -ne 0) { Write-Err "docker-compose up 失败" } } Write-Host "🎉 EasyAI 应用启动成功" } # ==================== 主流程 ==================== function Main { Init-ProjectDir # 检查是否已有 .env 且非强制重新配置 if ((Test-Path ".env") -and -not $env:DEPLOY_FORCE_RECONFIG -and -not $env:DEPLOY_IP) { Write-Step "📁 检测到已有 .env 配置" $reconfigure = Read-Host "是否重新配置访问方式?[y/N]" $trimmed = $reconfigure.Trim().ToLower() $startsWithY = ($trimmed.Length -gt 0) -and ($trimmed[0] -eq [char]121) if (-not $startsWithY) { Write-Step "使用现有配置继续..." # 从现有 .env 读取 IP(用于最后输出) $line = Get-Content ".env" -Encoding UTF8 | Where-Object { $_ -like "NUXT_PUBLIC_BASE_APIURL=*" } | Select-Object -First 1 if ($line -and $line -like "*:3001*") { $prefix = ".*http://" $suffix = ":3001.*" $script:DeployIP = ($line -replace $prefix, "" -replace $suffix, "").Trim() } $script:DeployModeSkip = $true } } if (-not $script:DeployModeSkip) { Run-DeployQuestions } if (-not $script:DeployModeSkip) { Setup-EnvFiles } else { if (-not (Test-Path ".env.tools")) { Copy-Item ".env.tools.sample" ".env.tools" } if (-not (Test-Path ".env.ASG")) { Copy-Item ".env.ASG.sample" ".env.ASG" } } if ($script:DeployDryRun) { Write-Host "" Write-Warn "dry-run 模式:跳过 Docker 安装和服务启动" Write-Host " 配置文件已生成,可直接运行 .\start.ps1 完成部署" } else { Test-Docker Start-Services } Write-Host "" Write-Host "================================" Write-Host " 部署完成" Write-Host "================================" $accessIp = if ($script:DeployIP) { $script:DeployIP } else { "127.0.0.1" } Write-Host "访问地址: http://${accessIp}:3010" Write-Host "默认登录账户: admin" Write-Host "默认登录密码: 123456" Write-Host "" Wait-ForExit } Write-Log "=== 脚本启动 ===" Write-Host "EasyAI Windows 部署脚本启动中..." -ForegroundColor Cyan Write-Host "日志文件: $script:LogFile" Write-Host "" Main Write-Log "=== 脚本正常结束 ==="