diff --git a/README.md b/README.md index a7d0cb9..cc20488 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,41 @@ git clone --depth 1 https://git.51easyai.com/wangbo/easyai.git && cd easyai && chmod +x start.sh && ./start.sh ``` +也可以通过环境变量直接传入部署方式,脚本会跳过对应问答: + +```bash +# IP 访问 +DEPLOY_ACCESS=ip DEPLOY_IP=服务器IP ./start.sh + +# 域名访问,不申请 HTTPS 证书 +DEPLOY_ACCESS=domain DEPLOY_DOMAIN=demo.example.com DEPLOY_HTTPS=false ./start.sh + +# 域名访问,并自动申请 HTTPS 证书 +DEPLOY_ACCESS=domain DEPLOY_DOMAIN=demo.example.com DEPLOY_HTTPS=true CERTBOT_EMAIL=ops@example.com ./start.sh +``` + +常用变量: + +| 变量 | 说明 | +|------|------| +| `DEPLOY_ACCESS` | `ip` 或 `domain`;未传时进入问答选择 | +| `DEPLOY_IP` | IP 模式访问地址,如 `10.0.0.8` | +| `DEPLOY_DOMAIN` | 域名模式访问地址,可带 `http://` 或 `https://`,脚本会自动清理 | +| `DEPLOY_HTTPS` | `true/false`,域名模式下是否申请 HTTPS 证书 | +| `CERTBOT_EMAIL` | 申请 HTTPS 证书使用的邮箱,默认 `wangbo@51easyai.com` | +| `DEPLOY_FORCE_RECONFIG` | 非空时即使已有 `.env` 也重新生成部署配置 | +| `DEPLOY_DRY_RUN` | `1` 时只生成配置文件,不安装 Docker、不启动服务 | +| `DEPLOY_NON_INTERACTIVE` | `1` 时缺少必要变量会直接报错,不再进入问答 | +| `DEPLOY_MAC_DOCKER_INSTALL` | macOS 未安装 Docker 时使用,`auto` 自动安装或 `manual` 提示手动安装 | + +`https.sh` 单独执行时也可以跳过域名选择: + +```bash +DEPLOY_DOMAIN=demo.example.com EASYAI_PROXY_CONF=demo.example.com.conf CERTBOT_EMAIL=ops@example.com ./https.sh +``` + +如需对已有 Nginx 配置追加或替换域名,可设置 `EASYAI_PROXY_DOMAIN_ACTION=add|replace`;未设置时默认追加。 + ### Windows 在 **PowerShell** 中执行: @@ -14,6 +49,24 @@ git clone --depth 1 https://git.51easyai.com/wangbo/easyai.git && cd easyai && c git clone --depth 1 "https://git.51easyai.com/wangbo/easyai.git" "easyai"; Set-Location "easyai"; powershell.exe -NoProfile -ExecutionPolicy Bypass -File ".\start.ps1" ``` +Windows 也支持变量方式跳过选择: + +```powershell +# 本机访问 +$env:DEPLOY_ACCESS = "local" +powershell.exe -NoProfile -ExecutionPolicy Bypass -File ".\start.ps1" + +# 局域网访问 +$env:DEPLOY_IP = "192.168.1.10" +powershell.exe -NoProfile -ExecutionPolicy Bypass -File ".\start.ps1" + +# 仅生成配置,不启动 Docker +$env:DEPLOY_IP = "127.0.0.1" +$env:DEPLOY_DRY_RUN = "1" +$env:DEPLOY_NO_WAIT = "1" +powershell.exe -NoProfile -ExecutionPolicy Bypass -File ".\start.ps1" +``` + > **Windows 脚本权限说明**:PowerShell 默认禁止运行脚本,直接双击 `start.ps1` 会闪退。 > > - **推荐**:使用 `powershell.exe -NoProfile -ExecutionPolicy Bypass -File ".\start.ps1"` 执行,无需修改系统策略 @@ -48,6 +101,12 @@ mv easyai easyai2 ``` 3. 重新执行一键部署命令,按脚本提示完成初始化输入。 + 如果希望跳过问答,可直接传入变量: + ```bash + DEPLOY_ACCESS=ip DEPLOY_IP=服务器IP ./start.sh + # 或 + DEPLOY_ACCESS=domain DEPLOY_DOMAIN=demo.example.com DEPLOY_HTTPS=false ./start.sh + ``` 4. 当脚本询问是否启用 HTTPS 时: - 如果你之前已经有可用证书,填写 `N`(不生成新证书)。 @@ -80,6 +139,18 @@ chmod +x update.sh - `[2]` 仅更新镜像并重启(跳过 git pull,适用于有本地修改不想被覆盖的场景) - 如果本次**不涉及配置文件更新**(如 `.env*`、`docker-compose.yml`、`easyai-proxy.conf*` 无变更),可直接选择 `[2]`,仅更新后台服务镜像并重启即可。 +- 自动化更新可通过变量跳过选择: +```bash +# 更新仓库 + 拉取镜像 + 重启(等同默认选 1) +UPDATE_MODE=full ./update.sh + +# 仅拉取镜像 + 重启(等同选 2) +UPDATE_MODE=image ./update.sh + +# 或使用布尔变量 +UPDATE_SKIP_REPO_UPDATE=true ./update.sh +``` + - **查看帮助**:`./update.sh -h` 或 `./update.sh --help` ### 更新说明 @@ -94,6 +165,12 @@ Windows 下使用 `update.ps1`,功能与 Linux 版一致: ``` - 执行后会**命令行内选择**:`[1]` 更新并拉取仓库 + 更新镜像(默认);`[2]` 仅更新镜像 - 需在 Git 克隆的 easyai 目录下运行 +- 也可通过变量跳过选择: +```powershell +$env:UPDATE_MODE = "image" +$env:UPDATE_NO_WAIT = "1" +.\update.ps1 +``` --- diff --git a/docs/Windows一键部署方案.md b/docs/Windows一键部署方案.md index 2e7e0cf..cb8406f 100644 --- a/docs/Windows一键部署方案.md +++ b/docs/Windows一键部署方案.md @@ -7,6 +7,7 @@ - **仅 IP 访问**:不含域名模式与 HTTPS - **本地访问无需放行端口**:选择本地 (127.0.0.1) 时,不涉及防火墙配置 - **Docker 未安装时**:用户可选择手动或自动安装 **Docker Desktop for Windows**(winget/Chocolatey) +- **支持变量非交互部署**:通过 `DEPLOY_IP` 或 `DEPLOY_ACCESS=local` 可跳过访问方式选择 ## 2. Linux start.sh 流程梳理 @@ -103,9 +104,13 @@ Windows 版**仅支持 IP 访问**,不包含域名模式与 HTTPS 配置。用 | 变量 | 说明 | |------|------| +| `DEPLOY_ACCESS` | `local` 时直接使用 `127.0.0.1`;`lan`/`ip` 需要同时传 `DEPLOY_IP` | | `DEPLOY_IP` | 访问 IP(本地填 `127.0.0.1`,局域网填实际 IP) | | `DEPLOY_DRY_RUN` | 1 时只生成配置,不安装/启动 Docker | | `DEPLOY_FORCE_RECONFIG` | 非空时强制重新配置 | +| `DEPLOY_NON_INTERACTIVE` | 1 时缺少必要变量会直接报错,不进入问答 | +| `DEPLOY_DOCKER_INSTALL` | Docker Desktop 未安装时使用,`manual` 打开下载地址,`auto` 使用 winget/choco 自动安装 | +| `DEPLOY_NO_WAIT` | 1 时脚本结束或失败不等待按 Enter | ### 5.4 配置文件修改实现(仅 IP 模式) @@ -126,6 +131,7 @@ Set-Content .env -Value $content -Encoding UTF8 -NoNewline - **未安装时**: prompt 让用户选择 - `[1] 手动安装`:输出安装说明及 Docker Desktop for Windows 下载链接,退出脚本 - `[2] 自动安装`:通过 winget 或 Chocolatey 安装 Docker Desktop for Windows,安装后需用户重启终端/机器再继续 +- 若设置 `DEPLOY_DOCKER_INSTALL=manual|auto`,脚本会直接执行对应分支,不再提示选择 ### 5.6 执行策略 @@ -154,15 +160,16 @@ powershell -ExecutionPolicy Bypass -File .\start.ps1 ## 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] 创建 `start.ps1` 主脚本 +- [x] 实现 `Init-ProjectDir`:项目目录校验 +- [x] 实现 `Run-DeployQuestions`:本地 / 局域网 IP 选择(无域名/HTTPS) +- [x] 实现 `Setup-EnvFiles`:配置文件生成与替换 +- [x] 实现 `Test-Docker`:Docker 检测,未安装时 prompt「手动安装」或「自动安装」 +- [x] 实现 `Install-Docker`:自动安装 **Docker Desktop for Windows**(winget / Chocolatey) +- [x] 实现 `Start-Services`:`docker compose` 启动 +- [x] 实现 `Main`:主流程串联 +- [x] 支持 `DEPLOY_DRY_RUN`、`DEPLOY_IP` 环境变量非交互模式 +- [x] 支持 `DEPLOY_ACCESS=local`、`DEPLOY_NON_INTERACTIVE`、`DEPLOY_DOCKER_INSTALL` 非交互控制 - [x] 在 README 或文档中补充 Windows 部署说明与执行策略 - [x] 创建 `scripts/test-start-ps1-env.py` 验证 .env 替换逻辑 @@ -178,6 +185,12 @@ $env:DEPLOY_DRY_RUN = "1" $env:DEPLOY_IP = "127.0.0.1" .\start.ps1 +# 或使用本机访问模式变量 +$env:DEPLOY_DRY_RUN = "1" +$env:DEPLOY_ACCESS = "local" +$env:DEPLOY_NO_WAIT = "1" +.\start.ps1 + # 检查 .env 是否已正确写入 Select-String -Path .env -Pattern "NUXT_PUBLIC_BASE" ``` diff --git a/https.sh b/https.sh index 76f0d0c..06d4323 100755 --- a/https.sh +++ b/https.sh @@ -1,3 +1,5 @@ +#!/bin/bash + if command -v nginx &> /dev/null; then echo "✅ Nginx 已安装,跳过安装步骤" else @@ -104,12 +106,55 @@ else fi # ===== 域名配置交互与文件生成 ===== +sanitize_domain() { + local domain=$1 + domain="${domain#http://}" + domain="${domain#https://}" + domain="${domain%%/*}" + domain="${domain%;}" + echo "$domain" | xargs +} + +validate_domain_value() { + local domain=$1 + [[ "$domain" =~ ^[A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?(\.[A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?)+$ ]] +} + +get_env_domain() { + local domain="${HTTPS_DOMAIN:-${EASYAI_PROXY_DOMAIN:-${DEPLOY_DOMAIN:-}}}" + if [ -z "$domain" ]; then + return 0 + fi + domain="$(sanitize_domain "$domain")" + if ! validate_domain_value "$domain"; then + echo "❌ 域名变量格式不正确: $domain" >&2 + exit 1 + fi + echo "$domain" +} + +is_non_interactive() { + [ "${HTTPS_NON_INTERACTIVE:-${DEPLOY_NON_INTERACTIVE:-0}}" = "1" ] || [ "${CI:-}" = "true" ] || [ ! -t 0 ] +} + prompt_domain() { local domain + domain="$(get_env_domain)" + if [ -n "$domain" ]; then + echo "$domain" + return 0 + fi + + if is_non_interactive; then + echo "❌ 当前为非交互环境,HTTPS 配置需要传入域名变量" >&2 + echo " 请设置 DEPLOY_DOMAIN=<域名> 或 EASYAI_PROXY_DOMAIN=<域名>" >&2 + exit 1 + fi + while true; do read -r -p "🌐 请输入域名(例如 demo.example.com): " domain - domain=$(echo "$domain" | xargs) - if [ -n "$domain" ] && [[ "$domain" =~ ^[A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?(\.[A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?)+$ ]]; then + domain="$(sanitize_domain "$domain")" + if [ -n "$domain" ] && validate_domain_value "$domain"; then echo "$domain" return 0 fi @@ -211,24 +256,59 @@ server { EOF } +conf_contains_domain() { + local conf_file=$1 + local domain=$2 + grep -Eq "server_name[^;]*(^|[[:space:]])(www\\.)?${domain//./\\.}([[:space:];]|$)" "./$conf_file" +} + +normalize_domain_action() { + local action=$1 + action="$(printf '%s' "$action" | tr '[:upper:]' '[:lower:]')" + case "$action" in + 1|replace) echo "1" ;; + 2|add|"") echo "2" ;; + *) + echo "❌ EASYAI_PROXY_DOMAIN_ACTION 只能是 replace 或 add,当前为: $action" >&2 + exit 1 + ;; + esac +} + upsert_domain_for_conf() { local conf_file=$1 + local env_domain="${2:-}" + local env_action="${3:-}" local mode local new_domain echo "ℹ️ 检测到当前目录已有配置文件: $conf_file" - while true; do - echo "请选择域名处理方式:" - echo " 1) 替换当前域名" - echo " 2) 新增一个域名" - read -r -p "请输入选项 (1/2): " mode - case "$mode" in - 1|2) break ;; - *) echo "❌ 无效选项,请输入 1 或 2" ;; - esac - done + if [ -n "$env_domain" ]; then + new_domain="$(sanitize_domain "$env_domain")" + if ! validate_domain_value "$new_domain"; then + echo "❌ 域名变量格式不正确: $new_domain" + exit 1 + fi + if conf_contains_domain "$conf_file" "$new_domain"; then + echo "ℹ️ 配置文件已包含域名 $new_domain,无需修改。" + return 0 + fi + mode="$(normalize_domain_action "$env_action")" + echo "使用变量配置域名: $new_domain, action=$([ "$mode" = "1" ] && echo replace || echo add)" + else + while true; do + echo "请选择域名处理方式:" + echo " 1) 替换当前域名" + echo " 2) 新增一个域名" + read -r -p "请输入选项 (1/2): " mode + case "$mode" in + 1|2) break ;; + *) echo "❌ 无效选项,请输入 1 或 2" ;; + esac + done - new_domain=$(prompt_domain) + new_domain=$(prompt_domain) + fi if [ "$mode" = "1" ]; then local old_domain @@ -268,7 +348,9 @@ upsert_domain_for_conf() { detect_existing_proxy_conf() { local preferred_file=$1 + local domain_filter="${2:-}" local -a candidates=() + local -a matched_candidates=() local conf if [ -n "$preferred_file" ] && [ -f "./$preferred_file" ]; then @@ -286,15 +368,36 @@ detect_existing_proxy_conf() { return 1 fi + if [ -n "$domain_filter" ]; then + for conf in "${candidates[@]}"; do + if conf_contains_domain "$conf" "$domain_filter"; then + matched_candidates+=("$conf") + fi + done + if [ "${#matched_candidates[@]}" -eq 1 ]; then + echo "${matched_candidates[0]}" + return 0 + fi + if [ "${#matched_candidates[@]}" -gt 1 ] && is_non_interactive; then + echo "❌ 多个配置文件都包含 $domain_filter,请通过 EASYAI_PROXY_CONF 指定文件" >&2 + exit 1 + fi + fi + if [ "${#candidates[@]}" -eq 1 ]; then echo "${candidates[0]}" return 0 fi - echo "⚠️ 检测到多个可用配置文件,请选择一个:" + if is_non_interactive; then + echo "❌ 检测到多个可用配置文件,请通过 EASYAI_PROXY_CONF 指定文件" >&2 + exit 1 + fi + + echo "⚠️ 检测到多个可用配置文件,请选择一个:" >&2 local i for i in "${!candidates[@]}"; do - printf " %s) %s\n" "$((i + 1))" "${candidates[$i]}" + printf " %s) %s\n" "$((i + 1))" "${candidates[$i]}" >&2 done while true; do read -r -p "请输入序号: " i @@ -310,9 +413,10 @@ echo "🚀 复制当前目录的配置文件到nginx配置文件目录" # 支持 EASYAI_PROXY_CONF 指定配置文件(如 51easyai.com.conf) PREFERRED_CONF_FILE="${EASYAI_PROXY_CONF:-}" DEFAULT_NEW_CONF_FILE="${EASYAI_PROXY_CONF:-easyai-proxy.conf}" -CONF_FILE=$(detect_existing_proxy_conf "$PREFERRED_CONF_FILE") +HTTPS_DOMAIN_INPUT="$(get_env_domain)" +CONF_FILE=$(detect_existing_proxy_conf "$PREFERRED_CONF_FILE" "$HTTPS_DOMAIN_INPUT") if [ -n "$CONF_FILE" ]; then - upsert_domain_for_conf "$CONF_FILE" + upsert_domain_for_conf "$CONF_FILE" "$HTTPS_DOMAIN_INPUT" "${EASYAI_PROXY_DOMAIN_ACTION:-}" else CONF_FILE="$DEFAULT_NEW_CONF_FILE" echo "ℹ️ 当前目录未找到可用 nginx 配置文件(*.conf 且包含 server_name)" @@ -363,7 +467,7 @@ if [ -n "$DOMAINS" ]; then sudo certbot --nginx \ --non-interactive \ --agree-tos \ - --email wangbo@51easyai.com \ + --email "${CERTBOT_EMAIL:-wangbo@51easyai.com}" \ --rsa-key-size 2048 \ --preferred-challenges http \ --force-renewal \ diff --git a/start.ps1 b/start.ps1 index 3eb86f4..9189aa6 100644 --- a/start.ps1 +++ b/start.ps1 @@ -98,6 +98,27 @@ function Run-DeployQuestions { return } + if ($env:DEPLOY_ACCESS) { + $mode = $env:DEPLOY_ACCESS.Trim().ToLowerInvariant() + switch ($mode) { + { $_ -in @("local", "localhost", "127.0.0.1") } { + $script:DeployIP = "127.0.0.1" + Step "Using DEPLOY_ACCESS=$($env:DEPLOY_ACCESS), IP=$($script:DeployIP)" + return + } + { $_ -in @("lan", "ip") } { + Fail "DEPLOY_ACCESS=$($env:DEPLOY_ACCESS) requires DEPLOY_IP, for example: `$env:DEPLOY_IP='192.168.1.10'" + } + default { + Fail "DEPLOY_ACCESS only supports local or lan on Windows, current value: $($env:DEPLOY_ACCESS)" + } + } + } + + if ($env:DEPLOY_NON_INTERACTIVE -eq "1" -or $env:CI -eq "true") { + Fail "Non-interactive deployment requires DEPLOY_IP or DEPLOY_ACCESS=local." + } + Write-Host "Choose mode:" Write-Host " [1] Local only (127.0.0.1)" Write-Host " [2] LAN access" @@ -277,6 +298,19 @@ function Test-Docker { } Warn "Docker Desktop not detected." + $installMode = if ($env:DEPLOY_DOCKER_INSTALL) { $env:DEPLOY_DOCKER_INSTALL.Trim().ToLowerInvariant() } else { "" } + if (-not [string]::IsNullOrWhiteSpace($installMode)) { + switch ($installMode) { + { $_ -in @("manual", "1") } { Start-Process $script:DockerDesktopUrl; Wait-ForExit; exit 1 } + { $_ -in @("auto", "2") } { Install-DockerDesktop } + default { Fail "DEPLOY_DOCKER_INSTALL only supports manual or auto, current value: $($env:DEPLOY_DOCKER_INSTALL)" } + } + } + + if ($env:DEPLOY_NON_INTERACTIVE -eq "1" -or $env:CI -eq "true") { + Fail "Docker Desktop not detected. Set DEPLOY_DOCKER_INSTALL=manual or DEPLOY_DOCKER_INSTALL=auto, or install Docker Desktop first." + } + Write-Host "Choose:" Write-Host " [1] Manual install (open URL and exit)" Write-Host " [2] Auto install (winget/choco)" @@ -311,14 +345,21 @@ function Start-Services { function Main { Init-ProjectDir - 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) { + if ((Test-Path ".env") -and -not $env:DEPLOY_FORCE_RECONFIG -and -not $env:DEPLOY_IP -and -not $env:DEPLOY_ACCESS) { + if ($env:DEPLOY_NON_INTERACTIVE -eq "1" -or $env:CI -eq "true") { $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" + } else { + $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" + } } } diff --git a/start.sh b/start.sh index 139da83..1cd1c69 100755 --- a/start.sh +++ b/start.sh @@ -23,11 +23,57 @@ init_project_dir() { } # ==================== 配置变量(支持环境变量非交互模式) ==================== -DEPLOY_MODE="" # ip | domain -DEPLOY_IP="" -DEPLOY_DOMAIN="" +DEPLOY_ACCESS="${DEPLOY_ACCESS:-}" # ip | domain +DEPLOY_MODE="" # ip | domain | skip +DEPLOY_IP="${DEPLOY_IP:-}" +DEPLOY_DOMAIN="${DEPLOY_DOMAIN:-}" +DEPLOY_HTTPS_ENV="${DEPLOY_HTTPS_INPUT:-${DEPLOY_HTTPS:-}}" DEPLOY_HTTPS=false +is_non_interactive() { + [ "${DEPLOY_NON_INTERACTIVE:-0}" = "1" ] || [ "${CI:-}" = "true" ] || [ ! -t 0 ] +} + +require_interactive() { + local missing_hint=$1 + if is_non_interactive; then + echo "❌ 当前为非交互环境,无法继续问答配置" + echo " 请通过环境变量传入: ${missing_hint}" + exit 1 + fi +} + +normalize_bool() { + local value + value="$(printf '%s' "$1" | tr '[:upper:]' '[:lower:]')" + case "$value" in + 1|true|yes|y|on) echo "true" ;; + 0|false|no|n|off|"") echo "false" ;; + *) + echo "❌ 布尔变量值不合法: $1(支持 true/false/1/0/yes/no/y/n/on/off)" >&2 + return 1 + ;; + esac +} + +set_deploy_https_from_env_or_default() { + local normalized + if ! normalized="$(normalize_bool "${DEPLOY_HTTPS_ENV:-false}")"; then + exit 1 + fi + DEPLOY_HTTPS="$normalized" +} + +infer_deploy_access_from_env() { + if [ -z "$DEPLOY_ACCESS" ]; then + if [ -n "$DEPLOY_DOMAIN" ]; then + DEPLOY_ACCESS="domain" + elif [ -n "$DEPLOY_IP" ]; then + DEPLOY_ACCESS="ip" + fi + fi +} + sanitize_domain() { local domain="$1" domain="${domain#http://}" @@ -66,6 +112,8 @@ prompt_or_env() { return fi + require_interactive "$env_name" + if [ -n "$default" ]; then read -r -p "${prompt_text} [$default]: " input eval "$var_name=\"${input:-$default}\"" @@ -82,6 +130,8 @@ run_deploy_questions() { echo "================================" echo "" + infer_deploy_access_from_env + # 非交互模式:环境变量已完整设置则直接使用(CI/自动化部署) if [ -n "$DEPLOY_ACCESS" ]; then if [ "$DEPLOY_ACCESS" = "ip" ] && [ -n "$DEPLOY_IP" ]; then @@ -96,14 +146,19 @@ run_deploy_questions() { exit 1 fi DEPLOY_MODE="domain" - DEPLOY_HTTPS="${DEPLOY_HTTPS_INPUT:-false}" + set_deploy_https_from_env_or_default echo "使用环境变量: 域名模式, 域名=$DEPLOY_DOMAIN" return fi + if [ "$DEPLOY_ACCESS" != "ip" ] && [ "$DEPLOY_ACCESS" != "domain" ]; then + echo "❌ DEPLOY_ACCESS 只能是 ip 或 domain,当前为: $DEPLOY_ACCESS" + exit 1 + fi fi # 1. IP 或域名访问 if [ -z "$DEPLOY_ACCESS" ]; then + require_interactive "DEPLOY_ACCESS=ip DEPLOY_IP=<服务器IP> 或 DEPLOY_ACCESS=domain DEPLOY_DOMAIN=<域名>" echo "1. 通过 IP 地址还是域名访问?" echo " [1] IP 地址(需开放 3001、3002、3003 端口)" echo " [2] 域名" @@ -140,9 +195,10 @@ run_deploy_questions() { fi # 3.1 是否启用 HTTPS - if [ -n "$DEPLOY_HTTPS_INPUT" ]; then - DEPLOY_HTTPS=$DEPLOY_HTTPS_INPUT + if [ -n "$DEPLOY_HTTPS_ENV" ]; then + set_deploy_https_from_env_or_default else + require_interactive "DEPLOY_HTTPS=true 或 DEPLOY_HTTPS=false" read -r -p "3.1 是否签名 HTTPS 证书?原来已经有 HTTPS 证书这里选择 N,第一次部署或者原来没有证书选 Y [y/N]: " https_choice DEPLOY_HTTPS=false if [[ "$https_choice" =~ ^[yY] ]]; then @@ -308,17 +364,22 @@ install_docker() { sudo yum install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin elif [[ "$OS_FAMILY" == "macOS" ]]; then echo "⚠️ 检测到 macOS,未找到 Docker。" - echo "请选择安装方式:" - echo " [1] 自动安装 Docker Desktop(需要 Homebrew)" - echo " [2] 手动安装(默认)" - read -r -p "请选择 [1/2]: " mac_install_choice + mac_install_choice="${DEPLOY_MAC_DOCKER_INSTALL:-${DEPLOY_DOCKER_INSTALL:-}}" + if [ -z "$mac_install_choice" ]; then + require_interactive "DEPLOY_MAC_DOCKER_INSTALL=auto 或 DEPLOY_MAC_DOCKER_INSTALL=manual" + echo "请选择安装方式:" + echo " [1] 自动安装 Docker Desktop(需要 Homebrew)" + echo " [2] 手动安装(默认)" + read -r -p "请选择 [1/2]: " mac_install_choice + fi + mac_install_choice="$(printf '%s' "$mac_install_choice" | tr '[:upper:]' '[:lower:]')" case "${mac_install_choice:-2}" in - 1) + 1|auto) if ! install_docker_macos; then exit 1 fi ;; - 2) + 2|manual) echo " 请手动安装 Docker Desktop: https://www.docker.com/products/docker-desktop/" echo " 安装并启动 Docker Desktop 后,重新运行 ./start.sh" exit 1 @@ -379,14 +440,20 @@ run_https_setup() { # ==================== 主流程 ==================== main() { init_project_dir + infer_deploy_access_from_env # 检查是否已有 .env 且非强制重新配置 if [ -f .env ] && [ -z "$DEPLOY_FORCE_RECONFIG" ] && [ -z "$DEPLOY_ACCESS" ]; then echo "📁 检测到已有 .env 配置" - read -r -p "是否重新配置部署方式?[y/N]: " reconfigure - if [[ ! "$reconfigure" =~ ^[yY] ]]; then + if is_non_interactive; then echo "⏭️ 使用现有配置继续..." DEPLOY_MODE="skip" + else + read -r -p "是否重新配置部署方式?[y/N]: " reconfigure + if [[ ! "$reconfigure" =~ ^[yY] ]]; then + echo "⏭️ 使用现有配置继续..." + DEPLOY_MODE="skip" + fi fi fi @@ -415,6 +482,7 @@ main() { else install_docker start_services + export DEPLOY_DOMAIN run_https_setup fi diff --git a/update.ps1 b/update.ps1 index 866feaa..63626a6 100644 --- a/update.ps1 +++ b/update.ps1 @@ -33,6 +33,7 @@ trap { function Wait-ForExit { if ($env:CI -eq "true") { return } + if ($env:UPDATE_NO_WAIT -eq "1") { return } Write-Host "" Read-Host "按 Enter 键退出" } @@ -46,7 +47,17 @@ function Write-Warn { param($Msg) Write-Host "⚠️ $Msg" -ForegroundColor Yell if ($args -contains "-h" -or $args -contains "--help") { Write-Host "用法: .\update.ps1" Write-Host "" - Write-Host "脚本将提示选择更新方式,默认拉取仓库并更新镜像。" + Write-Host "默认拉取仓库并更新镜像;也可通过环境变量跳过交互选择。" + Write-Host "" + Write-Host "环境变量:" + Write-Host " UPDATE_MODE=full|image" + Write-Host " full 更新并拉取仓库(git pull)+ 更新镜像并重启" + Write-Host " image 仅更新镜像并重启(跳过 git pull)" + Write-Host " UPDATE_SKIP_REPO_UPDATE=true|false" + Write-Host " true 等同 UPDATE_MODE=image" + Write-Host " false 等同 UPDATE_MODE=full" + Write-Host " UPDATE_NO_WAIT=1" + Write-Host " 结束或失败时不等待按 Enter" exit 0 } @@ -58,24 +69,62 @@ if (-not (Test-Path $composePath)) { Write-Err "未找到 docker-compose.yml, Set-Location $scriptDir Write-Step "📁 项目目录: $scriptDir" -# ==================== 命令行内选择 ==================== -Write-Host "" -Write-Host "请选择更新方式:" -Write-Host " [1] 更新并拉取仓库(git pull)+ 更新镜像并重启(默认)" -Write-Host " [2] 仅更新镜像并重启(跳过 git pull)" -$choice = Read-Host "请选择 [1/2,回车默认 1]" -if ([string]::IsNullOrWhiteSpace($choice)) { $choice = "1" } - -$skipRepoUpdate = $false -switch ($choice) { - "2" { $skipRepoUpdate = $true } - "1" { } - default { - Write-Warn "无效选择,将使用默认:更新并拉取仓库" - $skipRepoUpdate = $false +function ConvertTo-BoolEnv { + param([string]$Value) + $v = if ($null -eq $Value) { "" } else { $Value.Trim().ToLowerInvariant() } + switch ($v) { + { $_ -in @("1", "true", "yes", "y", "on") } { return $true } + { $_ -in @("0", "false", "no", "n", "off", "") } { return $false } + default { Write-Err "布尔变量值不合法: $Value(支持 true/false/1/0/yes/no/y/n/on/off)" } } } +function Resolve-UpdateMode { + if (-not [string]::IsNullOrWhiteSpace($env:UPDATE_SKIP_REPO_UPDATE)) { + $skip = ConvertTo-BoolEnv $env:UPDATE_SKIP_REPO_UPDATE + Write-Step "使用环境变量: UPDATE_SKIP_REPO_UPDATE=$skip" + return $skip + } + + if (-not [string]::IsNullOrWhiteSpace($env:UPDATE_MODE)) { + $mode = $env:UPDATE_MODE.Trim().ToLowerInvariant() + switch ($mode) { + { $_ -in @("full", "repo", "pull", "git", "1") } { + Write-Step "使用环境变量: UPDATE_MODE=$($env:UPDATE_MODE)" + return $false + } + { $_ -in @("image", "images", "docker", "skip-git", "skip_repo", "skip-repo", "2") } { + Write-Step "使用环境变量: UPDATE_MODE=$($env:UPDATE_MODE)" + return $true + } + default { Write-Err "UPDATE_MODE 只能是 full 或 image,当前为: $($env:UPDATE_MODE)" } + } + } + + if ($env:UPDATE_NON_INTERACTIVE -eq "1" -or $env:CI -eq "true") { + Write-Step "使用默认更新方式: 更新并拉取仓库(git pull)+ 更新镜像并重启" + return $false + } + + Write-Host "" + Write-Host "请选择更新方式:" + Write-Host " [1] 更新并拉取仓库(git pull)+ 更新镜像并重启(默认)" + Write-Host " [2] 仅更新镜像并重启(跳过 git pull)" + $choice = Read-Host "请选择 [1/2,回车默认 1]" + if ([string]::IsNullOrWhiteSpace($choice)) { $choice = "1" } + + switch ($choice) { + "2" { return $true } + "1" { return $false } + default { + Write-Warn "无效选择,将使用默认:更新并拉取仓库" + return $false + } + } +} + +$skipRepoUpdate = Resolve-UpdateMode + # ==================== 拉取仓库 ==================== if (-not $skipRepoUpdate) { Write-Host "" diff --git a/update.sh b/update.sh index b6d402f..89ebe61 100755 --- a/update.sh +++ b/update.sh @@ -6,7 +6,15 @@ set -e # 发生错误时终止脚本执行 if [[ "${1:-}" =~ ^(-h|--help)$ ]]; then echo "用法: $0" echo "" - echo "脚本将提示选择更新方式,默认拉取仓库并更新镜像。" + echo "默认拉取仓库并更新镜像;也可通过环境变量跳过交互选择。" + echo "" + echo "环境变量:" + echo " UPDATE_MODE=full|image" + echo " full 更新并拉取仓库(git pull)+ 更新镜像并重启" + echo " image 仅更新镜像并重启(跳过 git pull)" + echo " UPDATE_SKIP_REPO_UPDATE=true|false" + echo " true 等同 UPDATE_MODE=image" + echo " false 等同 UPDATE_MODE=full" exit 0 fi @@ -15,18 +23,68 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" && pwd)" cd "$SCRIPT_DIR" # 命令行内选择更新方式(默认:更新并拉取仓库) -echo "" -echo "请选择更新方式:" -echo " [1] 更新并拉取仓库(git pull)+ 更新镜像并重启(默认)" -echo " [2] 仅更新镜像并重启(跳过 git pull)" -read -r -p "请选择 [1/2,回车默认 1]: " choice -choice="${choice:-1}" SKIP_REPO_UPDATE=false -case "$choice" in - 2) SKIP_REPO_UPDATE=true ;; - 1) ;; - *) echo "❌ 无效选择,将使用默认:更新并拉取仓库"; SKIP_REPO_UPDATE=false ;; -esac + +normalize_bool() { + local value + value="$(printf '%s' "$1" | tr '[:upper:]' '[:lower:]')" + case "$value" in + 1|true|yes|y|on) echo "true" ;; + 0|false|no|n|off|"") echo "false" ;; + *) + echo "❌ 布尔变量值不合法: $1(支持 true/false/1/0/yes/no/y/n/on/off)" >&2 + return 1 + ;; + esac +} + +resolve_update_mode() { + local mode + if [ -n "${UPDATE_SKIP_REPO_UPDATE:-}" ]; then + mode="$(normalize_bool "$UPDATE_SKIP_REPO_UPDATE")" || exit 1 + SKIP_REPO_UPDATE="$mode" + echo "使用环境变量: UPDATE_SKIP_REPO_UPDATE=$SKIP_REPO_UPDATE" + return + fi + + if [ -n "${UPDATE_MODE:-}" ]; then + mode="$(printf '%s' "$UPDATE_MODE" | tr '[:upper:]' '[:lower:]')" + case "$mode" in + full|repo|pull|git|1) + SKIP_REPO_UPDATE=false + ;; + image|images|docker|skip-git|skip_repo|skip-repo|2) + SKIP_REPO_UPDATE=true + ;; + *) + echo "❌ UPDATE_MODE 只能是 full 或 image,当前为: $UPDATE_MODE" + exit 1 + ;; + esac + echo "使用环境变量: UPDATE_MODE=$UPDATE_MODE" + return + fi + + if [ "${UPDATE_NON_INTERACTIVE:-0}" = "1" ] || [ "${CI:-}" = "true" ] || [ ! -t 0 ]; then + echo "使用默认更新方式: 更新并拉取仓库(git pull)+ 更新镜像并重启" + SKIP_REPO_UPDATE=false + return + fi + + echo "" + echo "请选择更新方式:" + echo " [1] 更新并拉取仓库(git pull)+ 更新镜像并重启(默认)" + echo " [2] 仅更新镜像并重启(跳过 git pull)" + read -r -p "请选择 [1/2,回车默认 1]: " choice + choice="${choice:-1}" + case "$choice" in + 2) SKIP_REPO_UPDATE=true ;; + 1) ;; + *) echo "❌ 无效选择,将使用默认:更新并拉取仓库"; SKIP_REPO_UPDATE=false ;; + esac +} + +resolve_update_mode # 拉取整个仓库更新 if [ "$SKIP_REPO_UPDATE" = false ]; then