feat: 新增 Windows 一键部署脚本 start.ps1
Some checks are pending
Test start.ps1 (Windows) / test-windows (push) Waiting to run

- start.ps1: PowerShell 部署脚本,支持本地/局域网 IP 访问
- docs/Windows一键部署方案.md: 实现方案与测试说明
- scripts/test-start-ps1-env.py: .env 替换逻辑验证
- .github/workflows/test-start-ps1.yml: Windows CI 测试

Made-with: Cursor
This commit is contained in:
wangbo 2026-03-10 21:59:12 +08:00
parent ea70cecef1
commit b7a11abe9f
4 changed files with 528 additions and 0 deletions

38
.github/workflows/test-start-ps1.yml vendored Normal file
View File

@ -0,0 +1,38 @@
# 在 Windows 上测试 start.ps1DRY_RUN 模式,不启动 Docker
name: Test start.ps1 (Windows)
on:
push:
paths:
- 'start.ps1'
- '.env.sample'
- '.github/workflows/test-start-ps1.yml'
pull_request:
paths:
- 'start.ps1'
- '.env.sample'
- '.github/workflows/test-start-ps1.yml'
workflow_dispatch:
jobs:
test-windows:
runs-on: windows-latest
steps:
- uses: actions/checkout@v4
- name: Run start.ps1 (DRY_RUN)
env:
DEPLOY_DRY_RUN: "1"
DEPLOY_IP: "192.168.1.100"
run: |
powershell -ExecutionPolicy Bypass -File .\start.ps1
- name: Verify .env
run: |
$api = Select-String -Path .env -Pattern '^NUXT_PUBLIC_BASE_APIURL=' | ForEach-Object { $_.Line }
$socket = Select-String -Path .env -Pattern '^NUXT_PUBLIC_BASE_SOCKETURL=' | ForEach-Object { $_.Line }
$sg = Select-String -Path .env -Pattern '^NUXT_PUBLIC_SG_APIURL=' | ForEach-Object { $_.Line }
if ($api -ne 'NUXT_PUBLIC_BASE_APIURL=http://192.168.1.100:3001') { exit 1 }
if ($socket -ne 'NUXT_PUBLIC_BASE_SOCKETURL=ws://192.168.1.100:3002') { exit 1 }
if ($sg -ne 'NUXT_PUBLIC_SG_APIURL=http://192.168.1.100:3003') { exit 1 }
Write-Host "OK: .env 配置正确"

View File

@ -0,0 +1,199 @@
# EasyAI Windows 一键部署方案
## 1. 背景与目标
`start.sh` 是 Linux 下的一键部署脚本,本方案为 Windows 平台提供等效的 `start.ps1` PowerShell 脚本。针对 Windows 典型场景做如下**精简**
- **仅 IP 访问**:不含域名模式与 HTTPS
- **本地访问无需放行端口**:选择本地 (127.0.0.1) 时,不涉及防火墙配置
- **Docker 未安装时**:用户可选择手动或自动安装 **Docker Desktop for Windows**winget/Chocolatey
## 2. Linux start.sh 流程梳理
### 2.1 整体流程
| 步骤 | 功能 | 说明 |
|------|------|------|
| 1 | 项目初始化 | 校验当前目录下存在 `docker-compose.yml` |
| 2 | 部署配置问答 | IP 或域名二选一,并采集对应参数 |
| 3 | 配置文件生成 | 生成/更新 `.env`、`.env.tools`、`.env.ASG` |
| 4 | Docker 安装与检查 | 检测并安装 DockerUbuntu/CentOS |
| 5 | 启动服务 | `docker compose pull && docker compose up -d` |
| 6 | HTTPS 配置(可选) | 域名模式下执行 `https.sh` |
### 2.2 配置问答逻辑
- **IP 模式**:输入服务器 IP 地址,需要放行 3001、3002、3003 端口
- **域名模式**:输入域名,可选是否启用 HTTPS需放行 80、443 端口
### 2.3 环境变量写入 .env
- `NUXT_PUBLIC_BASE_APIURL`API 地址
- `NUXT_PUBLIC_BASE_SOCKETURL`WebSocket 地址
- `NUXT_PUBLIC_SG_APIURL`Agent 服务治理 API 地址
**IP 模式**
```
NUXT_PUBLIC_BASE_APIURL=http://<IP>:3001
NUXT_PUBLIC_BASE_SOCKETURL=ws://<IP>:3002
NUXT_PUBLIC_SG_APIURL=http://<IP>:3003
```
**域名模式**
```
NUXT_PUBLIC_BASE_APIURL=/api
NUXT_PUBLIC_BASE_SOCKETURL=wss://<domain>/socket.io
NUXT_PUBLIC_SG_APIURL=/asg-api
```
---
## 3. Windows 与 Linux 差异
| 项目 | Linux | Windows |
|------|-------|---------|
| 脚本语言 | Bash | PowerShell |
| 文本替换 | sed | `(Get-Content) -replace``Set-Content` |
| 用户输入 | `read -p` | `Read-Host` |
| 访问方式 | IP + 域名 + HTTPS | **仅 IP**(本地 / 局域网) |
| 端口放行 | 本地无特殊说明 | **本地访问无需放行**,局域网需放行 3001/3002/3003 |
| Docker | apt/yum 安装 Linux Docker | **Docker Desktop for Windows**,未安装时可选择手动或自动安装 |
---
## 4. Windows IP 访问方式设计(核心差异)
Windows 版**仅支持 IP 访问**,不包含域名模式与 HTTPS 配置。用户访问方式分为两类:
| 选项 | 访问方式 | IP 值 | 端口说明 |
|------|----------|-------|----------|
| 1 | 本地访问 | `127.0.0.1` | 本机访问,**无需放行端口** |
| 2 | 局域网访问 | 用户输入本机局域网 IP | 同网段设备访问,需放行 3001、3002、3003 |
**交互流程:**
1. `[1] 本地访问` → 自动使用 `127.0.0.1`,无需配置防火墙
2. `[2] 局域网访问` → 提示用户输入局域网 IP可提示 `ipconfig` 查看),并提醒放行上述端口
---
## 5. 实现方案
### 5.1 脚本文件
- 文件路径:`easyai/start.ps1`
- 一行命令示例:
```powershell
git clone https://git.51easyai.com/wangbo/easyai; cd easyai; .\start.ps1
```
### 5.2 模块划分
| 模块 | 函数/区块 | 功能 |
|------|-----------|------|
| 项目初始化 | `Init-ProjectDir` | 检查 `docker-compose.yml`,切换到项目根目录 |
| 配置问答 | `Run-DeployQuestions` | 本地访问 / 局域网访问选择(含 IP 输入) |
| 环境变量 | `PromptOrEnv` | 支持环境变量覆盖,用于 CI/自动化 |
| 配置文件 | `Setup-EnvFiles` | 复制 sample 并修改 `.env`、`.env.tools`、`.env.ASG` |
| Docker 检查 | `Test-Docker` | 检测 `docker`,未安装则让用户选择**手动安装**或**自动安装** |
| 启动服务 | `Start-Services` | `docker compose pull``docker compose up -d` |
| 主流程 | `Main` | 串联上述步骤 |
### 5.3 环境变量支持(非交互模式)
| 变量 | 说明 |
|------|------|
| `DEPLOY_IP` | 访问 IP本地填 `127.0.0.1`,局域网填实际 IP |
| `DEPLOY_DRY_RUN` | 1 时只生成配置,不安装/启动 Docker |
| `DEPLOY_FORCE_RECONFIG` | 非空时强制重新配置 |
### 5.4 配置文件修改实现(仅 IP 模式)
使用 PowerShell 替换 `.env` 中相关行:
```powershell
$content = Get-Content .env -Raw -Encoding UTF8
$content = $content -replace 'NUXT_PUBLIC_BASE_APIURL=.*', "NUXT_PUBLIC_BASE_APIURL=http://${DEPLOY_IP}:3001"
$content = $content -replace 'NUXT_PUBLIC_BASE_SOCKETURL=.*', "NUXT_PUBLIC_BASE_SOCKETURL=ws://${DEPLOY_IP}:3002"
$content = $content -replace 'NUXT_PUBLIC_SG_APIURL=.*', "NUXT_PUBLIC_SG_APIURL=http://${DEPLOY_IP}:3003"
Set-Content .env -Value $content -Encoding UTF8 -NoNewline
```
### 5.5 Docker 处理策略
- **目标**:检测并安装 **Docker Desktop for Windows**Windows 版 Docker
- **检测**:执行 `docker --version``docker compose version`
- **未安装时** prompt 让用户选择
- `[1] 手动安装`:输出安装说明及 Docker Desktop for Windows 下载链接,退出脚本
- `[2] 自动安装`:通过 winget 或 Chocolatey 安装 Docker Desktop for Windows安装后需用户重启终端/机器再继续
### 5.6 执行策略
PowerShell 默认可能禁止执行脚本,建议在文档中说明:
```powershell
Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser
```
或在脚本开头提示用户使用:
```powershell
powershell -ExecutionPolicy Bypass -File .\start.ps1
```
---
## 6. 部署完成输出
成功部署后输出访问地址,例如:
- 本地访问:`http://127.0.0.1:3010`
- 局域网访问:`http://<LAN_IP>:3010`
---
## 7. 实现清单
- [ ] 创建 `start.ps1` 主脚本
- [ ] 实现 `Init-ProjectDir`:项目目录校验
- [ ] 实现 `Run-DeployQuestions`:本地 / 局域网 IP 选择(无域名/HTTPS
- [ ] 实现 `Setup-EnvFiles`:配置文件生成与替换
- [ ] 实现 `Test-Docker`Docker 检测,未安装时 prompt「手动安装」或「自动安装」
- [ ] 实现 `Install-Docker`:自动安装 **Docker Desktop for Windows**winget / Chocolatey
- [ ] 实现 `Start-Services``docker compose` 启动
- [ ] 实现 `Main`:主流程串联
- [ ] 支持 `DEPLOY_DRY_RUN`、`DEPLOY_IP` 环境变量非交互模式
- [x] 在 README 或文档中补充 Windows 部署说明与执行策略
- [x] 创建 `scripts/test-start-ps1-env.py` 验证 .env 替换逻辑
---
## 8. Windows 测试说明
### 本地测试(需 Windows 或 WSL + PowerShell
```powershell
# 非交互 + 仅生成配置(不启动 Docker
$env:DEPLOY_DRY_RUN = "1"
$env:DEPLOY_IP = "127.0.0.1"
.\start.ps1
# 检查 .env 是否已正确写入
Select-String -Path .env -Pattern "NUXT_PUBLIC_BASE"
```
### 完整部署测试(需 Windows + Docker Desktop
```powershell
.\start.ps1
# 按提示选择 [1] 本地访问 或 [2] 局域网访问
# 若未安装 Docker选择 [1] 手动 或 [2] 自动安装
```
### 执行策略(若提示无法运行脚本)
```powershell
Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser
# 或
powershell -ExecutionPolicy Bypass -File .\start.ps1
```

View File

@ -0,0 +1,27 @@
#!/usr/bin/env python3
# 验证 start.ps1 的 .env 替换逻辑(与 PowerShell 等效)
import re
DEPLOY_IP = "192.168.1.100"
content = """NUXT_PUBLIC_BASE_APIURL=http://127.0.0.1:3001
#NUXT_PUBLIC_BASE_APIURL=/api
NUXT_PUBLIC_BASE_SOCKETURL=ws://127.0.0.1:3002
NUXT_PUBLIC_SG_APIURL=http://127.0.0.1:3003
"""
content = re.sub(r'^NUXT_PUBLIC_BASE_APIURL=.*', f'NUXT_PUBLIC_BASE_APIURL=http://{DEPLOY_IP}:3001', content, flags=re.MULTILINE)
content = re.sub(r'^NUXT_PUBLIC_BASE_SOCKETURL=.*', f'NUXT_PUBLIC_BASE_SOCKETURL=ws://{DEPLOY_IP}:3002', content, flags=re.MULTILINE)
content = re.sub(r'^NUXT_PUBLIC_SG_APIURL=.*', f'NUXT_PUBLIC_SG_APIURL=http://{DEPLOY_IP}:3003', content, flags=re.MULTILINE)
expected = f"""NUXT_PUBLIC_BASE_APIURL=http://{DEPLOY_IP}:3001
#NUXT_PUBLIC_BASE_APIURL=/api
NUXT_PUBLIC_BASE_SOCKETURL=ws://{DEPLOY_IP}:3002
NUXT_PUBLIC_SG_APIURL=http://{DEPLOY_IP}:3003
"""
# 注释行不应被替换,检查第一行和第三行
lines = content.split('\n')
assert lines[0] == f'NUXT_PUBLIC_BASE_APIURL=http://{DEPLOY_IP}:3001', f"Got {lines[0]}"
assert lines[2] == f'NUXT_PUBLIC_BASE_SOCKETURL=ws://{DEPLOY_IP}:3002', f"Got {lines[2]}"
assert lines[3] == f'NUXT_PUBLIC_SG_APIURL=http://{DEPLOY_IP}:3003', f"Got {lines[3]}"
assert lines[1] == '#NUXT_PUBLIC_BASE_APIURL=/api', f"Comment should not change: {lines[1]}"
print("OK: .env replacement logic validated")

264
start.ps1 Normal file
View File

@ -0,0 +1,264 @@
#Requires -Version 5.1
# EasyAI Windows 一键部署脚本
# 仅支持 IP 访问(本地/局域网),不含域名与 HTTPS
# 一行命令: git clone https://git.51easyai.com/wangbo/easyai; cd easyai; .\start.ps1
$ErrorActionPreference = "Stop"
# 仅配置模式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; exit 1 }
function Write-Warn { param($Msg) Write-Host "⚠️ $Msg" -ForegroundColor Yellow }
# ==================== 项目初始化 ====================
function Init-ProjectDir {
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
$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" {
Write-Host " 提示: 可在本机运行 ipconfig 查看 IPv4 地址"
$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
}
}
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"
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"
exit 1
}
function Test-Docker {
Write-Host ""
Write-Host "================================"
Write-Host " Docker 检查"
Write-Host "================================"
Write-Host ""
if (Test-DockerInstalled) {
Write-Ok "Docker 已安装"
$v = docker --version 2>$null
if ($v) { Write-Host " $v" }
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
}
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]"
if ($reconfigure -notmatch '^[yY]') {
Write-Step "使用现有配置继续..."
# 从现有 .env 读取 IP用于最后输出
$line = Get-Content ".env" | Where-Object { $_ -match '^NUXT_PUBLIC_BASE_APIURL=http://([^:]+):3001' } | Select-Object -First 1
if ($line -match 'http://([^:]+):3001') { $script:DeployIP = $Matches[1] }
$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 ""
}
Main