easyai/start.ps1
wangbolhr e64a90332c
Some checks failed
Test start.ps1 (Windows) / test-windows (push) Has been cancelled
feat: 优化 Windows 部署与文档
- start.ps1: 局域网 IP 自动检测, 部署完成提示默认账户 admin/123456
- start.ps1: 修复 UTF-8 BOM 避免 PowerShell 5.1 解析闪退
- docker-compose: dozzle 容器增加 restart 与 logging 配置
- 新增 reset-docker-network.ps1 网络重置脚本
- README: 顶部增加 Linux/Windows 一键部署命令, Windows 权限说明

Made-with: Cursor
2026-03-11 14:41:53 +08:00

436 lines
16 KiB
PowerShell
Raw Permalink 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
# 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
}
# 结束时保持窗口不关闭,便于查看输出和 DebugCI 环境自动跳过)
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 startDocker 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 "=== 脚本正常结束 ==="