diff --git a/.github/workflows/test-start-ps1.yml b/.github/workflows/test-start-ps1.yml new file mode 100644 index 0000000..a81af06 --- /dev/null +++ b/.github/workflows/test-start-ps1.yml @@ -0,0 +1,38 @@ +# 在 Windows 上测试 start.ps1(DRY_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 配置正确" diff --git a/docs/Windows一键部署方案.md b/docs/Windows一键部署方案.md new file mode 100644 index 0000000..2e7e0cf --- /dev/null +++ b/docs/Windows一键部署方案.md @@ -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 安装与检查 | 检测并安装 Docker(Ubuntu/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://:3001 +NUXT_PUBLIC_BASE_SOCKETURL=ws://:3002 +NUXT_PUBLIC_SG_APIURL=http://:3003 +``` + +**域名模式**: +``` +NUXT_PUBLIC_BASE_APIURL=/api +NUXT_PUBLIC_BASE_SOCKETURL=wss:///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://: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 +``` diff --git a/scripts/test-start-ps1-env.py b/scripts/test-start-ps1-env.py new file mode 100644 index 0000000..5c03936 --- /dev/null +++ b/scripts/test-start-ps1-env.py @@ -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") diff --git a/start.ps1 b/start.ps1 new file mode 100644 index 0000000..2783d71 --- /dev/null +++ b/start.ps1 @@ -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