优化部署脚本非交互变量支持

This commit is contained in:
2026-06-30 22:12:26 +08:00
parent d54cf2f6ce
commit 2a40ad91c8
7 changed files with 483 additions and 73 deletions
+77
View File
@@ -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
```
---
+22 -9
View File
@@ -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"
```
+122 -18
View File
@@ -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 \
+45 -4
View File
@@ -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"
}
}
}
+82 -14
View File
@@ -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
+65 -16
View File
@@ -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 ""
+70 -12
View File
@@ -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