feat(update): 完整重写热更新脚本,覆盖所有已知部署问题
Step 1 - CONSOLE_DOMAIN:检测裸 IP,交互提示输入公网域名 Step 2 - SDK URL:修复 xuqinmin.com 残留 / 内网 IP / 空值三种情形 Step 3 - SDK JSON:同步 xuqm-private-sdk.json Step 4 - 宿主机 nginx WebSocket 头: · nginx -T 获取完整配置 · Python3 精确定位代理到容器 nginx 的 location 块 · 缺少 proxy_http_version 1.1 / Upgrade / Connection 时自动注入 · 修改前备份 .xuqm.bak,nginx -t 失败自动回滚,成功后 nginx -s reload · 未发现代理配置时输出标准模板供参考 Step 5 - 拉取镜像(可选) Step 6 - 重启 tenant-service + nginx Step 7 - 等待 tenant-service 健康(最长 120s) Step 8 - 自动处理积压 PENDING 服务开通申请 Step 9 - 全量验证 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
这个提交包含在:
父节点
7d8f916ea5
当前提交
4ff09ae768
@ -1,18 +1,24 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# update.sh — XuqmGroup 私有化部署热更新脚本
|
# update.sh — XuqmGroup 私有化部署热更新脚本
|
||||||
#
|
#
|
||||||
# 用途:在已部署的环境上执行,无需重新配置基础信息:
|
# 执行内容(顺序):
|
||||||
# 1. 自动读取现有配置(.env / config/secrets.env)
|
# 1. 检测并修正 CONSOLE_DOMAIN(裸 IP → 提示输入公网域名)
|
||||||
# 2. 修复已知配置问题(SDK 地址、残留公网 URL 等)
|
# 2. 修复 SDK URL(公网平台残留地址 / 内网 IP)
|
||||||
# 3. 可选:拉取最新镜像
|
# 3. 同步 SDK 初始化配置文件
|
||||||
# 4. 重启受影响的容器
|
# 4. 检测并自动修复宿主机 nginx WebSocket 代理头
|
||||||
# 5. 重新运行全量验证
|
# 5. 可选拉取最新镜像
|
||||||
|
# 6. 重启受影响容器
|
||||||
|
# 7. 等待 tenant-service 健康
|
||||||
|
# 8. 自动处理积压的 PENDING 服务开通申请
|
||||||
|
# 9. 全量验证
|
||||||
#
|
#
|
||||||
# 前提:已执行过 install.sh 完成初始部署
|
# 前提:已执行过 install.sh 完成初始部署
|
||||||
|
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
# 自动定位安装目录:优先脚本所在目录的上级,否则搜索常见路径
|
# ---------------------------------------------------------------------------
|
||||||
|
# 定位安装目录
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
_script_parent="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
_script_parent="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
if [ -f "$_script_parent/docker-compose.yml" ]; then
|
if [ -f "$_script_parent/docker-compose.yml" ]; then
|
||||||
ROOT_DIR="$_script_parent"
|
ROOT_DIR="$_script_parent"
|
||||||
@ -43,9 +49,7 @@ info() { printf " → %s\n" "$*"; }
|
|||||||
# 修改 env 文件中某个 key 的值;若 key 不存在则追加
|
# 修改 env 文件中某个 key 的值;若 key 不存在则追加
|
||||||
_set_env() {
|
_set_env() {
|
||||||
local file="$1" key="$2" val="$3"
|
local file="$1" key="$2" val="$3"
|
||||||
local tmp; tmp="$(mktemp)"
|
|
||||||
if grep -q "^${key}=" "$file" 2>/dev/null; then
|
if grep -q "^${key}=" "$file" 2>/dev/null; then
|
||||||
# 用 python3 替换,避免 sed 在 URL 值中的斜杠转义问题
|
|
||||||
python3 - "$file" "$key" "$val" <<'PY'
|
python3 - "$file" "$key" "$val" <<'PY'
|
||||||
import sys, re
|
import sys, re
|
||||||
path, key, val = sys.argv[1], sys.argv[2], sys.argv[3]
|
path, key, val = sys.argv[1], sys.argv[2], sys.argv[3]
|
||||||
@ -56,38 +60,52 @@ PY
|
|||||||
else
|
else
|
||||||
printf '%s=%s\n' "$key" "$val" >> "$file"
|
printf '%s=%s\n' "$key" "$val" >> "$file"
|
||||||
fi
|
fi
|
||||||
rm -f "$tmp"
|
}
|
||||||
|
|
||||||
|
# 判断一个 http/https URL 是否需要修复(空 / xuqinmin.com 残留 / 裸 IP)
|
||||||
|
_url_needs_fix() {
|
||||||
|
local val="$1"
|
||||||
|
[ -z "$val" ] && return 0
|
||||||
|
printf '%s' "$val" | grep -qi 'xuqinmin\.com' && return 0
|
||||||
|
local host
|
||||||
|
host="$(printf '%s' "$val" | sed 's|https\?://||; s|/.*||; s|:.*||')"
|
||||||
|
printf '%s' "$host" | grep -qE \
|
||||||
|
'^([0-9]{1,3}\.){3}[0-9]{1,3}$' && return 0
|
||||||
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# 前置检查
|
# Banner
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
printf "\n${BOLD}══════════════════════════════════════════════════${RESET}\n"
|
printf "\n${BOLD}══════════════════════════════════════════════════${RESET}\n"
|
||||||
printf "${BOLD} XuqmGroup 私有化部署热更新${RESET}\n"
|
printf "${BOLD} XuqmGroup 私有化部署热更新${RESET}\n"
|
||||||
printf "${BOLD}══════════════════════════════════════════════════${RESET}\n"
|
printf "${BOLD}══════════════════════════════════════════════════${RESET}\n"
|
||||||
printf " 部署目录: %s\n\n" "$ROOT_DIR"
|
printf " 部署目录: %s\n\n" "$ROOT_DIR"
|
||||||
|
|
||||||
[ -f "$ROOT_DIR/.env" ] || fail "未找到 .env,请先执行 install.sh 完成初始部署"
|
# ---------------------------------------------------------------------------
|
||||||
[ -f "$ROOT_DIR/config/xuqm.env" ] || fail "未找到 config/xuqm.env,请先执行 install.sh 完成初始部署"
|
# 前置检查
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
[ -f "$ROOT_DIR/.env" ] || fail "未找到 .env,请先执行 install.sh 完成初始部署"
|
||||||
|
[ -f "$ROOT_DIR/config/xuqm.env" ] || fail "未找到 config/xuqm.env,请先执行 install.sh 完成初始部署"
|
||||||
[ -f "$ROOT_DIR/config/secrets.env" ] || fail "未找到 config/secrets.env,请先执行 install.sh 完成初始部署"
|
[ -f "$ROOT_DIR/config/secrets.env" ] || fail "未找到 config/secrets.env,请先执行 install.sh 完成初始部署"
|
||||||
|
|
||||||
command -v docker >/dev/null 2>&1 || fail "Docker 未安装"
|
command -v docker >/dev/null 2>&1 || fail "Docker 未安装"
|
||||||
command -v python3 >/dev/null 2>&1 || fail "python3 未安装"
|
command -v python3 >/dev/null 2>&1 || fail "python3 未安装"
|
||||||
docker info >/dev/null 2>&1 || fail "Docker daemon 未运行"
|
docker info >/dev/null 2>&1 || fail "Docker daemon 未运行,请执行: systemctl start docker"
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# 加载现有配置
|
# 加载现有配置
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
log "加载现有配置"
|
log "加载现有配置"
|
||||||
|
|
||||||
# shellcheck disable=SC1090,SC1091
|
|
||||||
set -a
|
set -a
|
||||||
|
# shellcheck disable=SC1090,SC1091
|
||||||
. "$ROOT_DIR/.env"
|
. "$ROOT_DIR/.env"
|
||||||
. "$ROOT_DIR/config/secrets.env"
|
. "$ROOT_DIR/config/secrets.env"
|
||||||
set +a
|
set +a
|
||||||
|
|
||||||
# 从 config/xuqm.env 中读取 CONSOLE_DOMAIN(不用 source,避免覆盖已加载值)
|
_CONSOLE_DOMAIN="$(grep '^CONSOLE_DOMAIN=' "$ROOT_DIR/config/xuqm.env" 2>/dev/null \
|
||||||
_CONSOLE_DOMAIN="$(grep '^CONSOLE_DOMAIN=' "$ROOT_DIR/config/xuqm.env" 2>/dev/null | cut -d= -f2- | tr -d '"' | tr -d "'")"
|
| cut -d= -f2- | tr -d '"' | tr -d "'")"
|
||||||
_NGINX_BIND="${NGINX_BIND:-80}"
|
_NGINX_BIND="${NGINX_BIND:-80}"
|
||||||
_NGINX_PORT="${_NGINX_BIND##*:}"
|
_NGINX_PORT="${_NGINX_BIND##*:}"
|
||||||
|
|
||||||
@ -96,45 +114,49 @@ ok "NGINX_BIND=${_NGINX_BIND}"
|
|||||||
|
|
||||||
[ -n "$_CONSOLE_DOMAIN" ] || fail "config/xuqm.env 中 CONSOLE_DOMAIN 未设置,请手动补充后重试"
|
[ -n "$_CONSOLE_DOMAIN" ] || fail "config/xuqm.env 中 CONSOLE_DOMAIN 未设置,请手动补充后重试"
|
||||||
|
|
||||||
# CONSOLE_DOMAIN 若为裸 IP(内网/公网)则强制要求更新为域名,否则 SDK URL 无法从外部访问
|
# ---------------------------------------------------------------------------
|
||||||
_domain_stripped="$(printf '%s' "$_CONSOLE_DOMAIN" | sed 's|https\?://||' | cut -d: -f1)"
|
# Step 1 — 检测 CONSOLE_DOMAIN 是否为裸 IP,提示更正
|
||||||
if printf '%s' "$_domain_stripped" | grep -qE \
|
# ---------------------------------------------------------------------------
|
||||||
'^(10\.|172\.(1[6-9]|2[0-9]|3[01])\.|192\.168\.|127\.|[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$)'; then
|
log "检查 CONSOLE_DOMAIN"
|
||||||
warn "CONSOLE_DOMAIN 当前为 IP 地址(${_CONSOLE_DOMAIN}),客户端浏览器可能无法访问"
|
|
||||||
printf ' 外部访问域名应为公网域名(如 https://console.example.com)\n'
|
_domain_host="$(printf '%s' "$_CONSOLE_DOMAIN" | sed 's|https\?://||; s|/.*||; s|:.*||')"
|
||||||
read -rp " 请输入正确的外部访问地址(直接回车保持不变): " _new_domain
|
if printf '%s' "$_domain_host" | grep -qE '^([0-9]{1,3}\.){3}[0-9]{1,3}$'; then
|
||||||
|
warn "CONSOLE_DOMAIN 当前为 IP 地址(${_CONSOLE_DOMAIN})"
|
||||||
|
printf ' SDK 配置会把 IM WebSocket 地址设成该 IP,外网客户端无法通过域名 TLS 连接。\n'
|
||||||
|
printf ' 请输入对外访问的公网域名(含协议,如 https://console.example.com)\n'
|
||||||
|
read -rp " 新的 CONSOLE_DOMAIN(直接回车保持原值): " _new_domain
|
||||||
if [ -n "$_new_domain" ]; then
|
if [ -n "$_new_domain" ]; then
|
||||||
_set_env "$ROOT_DIR/config/xuqm.env" "CONSOLE_DOMAIN" "$_new_domain"
|
_set_env "$ROOT_DIR/config/xuqm.env" "CONSOLE_DOMAIN" "$_new_domain"
|
||||||
_CONSOLE_DOMAIN="$_new_domain"
|
_CONSOLE_DOMAIN="$_new_domain"
|
||||||
ok "CONSOLE_DOMAIN 已更新 → ${_CONSOLE_DOMAIN}"
|
ok "CONSOLE_DOMAIN 已更新 → ${_CONSOLE_DOMAIN}"
|
||||||
else
|
else
|
||||||
warn "保持原值 ${_CONSOLE_DOMAIN},如外部无法访问请手动修改 config/xuqm.env 后重试"
|
warn "保持原值 ${_CONSOLE_DOMAIN},如 SDK 无法连接请修改 config/xuqm.env 后重试"
|
||||||
fi
|
fi
|
||||||
|
else
|
||||||
|
ok "CONSOLE_DOMAIN 正常: ${_CONSOLE_DOMAIN}"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# 1. 修复配置问题
|
# Step 2 — 修复 SDK URL
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
log "检查并修复配置"
|
log "检查并修复 SDK URL"
|
||||||
|
|
||||||
# 根据 CONSOLE_DOMAIN 派生正确的 SDK 地址
|
|
||||||
if printf '%s' "$_CONSOLE_DOMAIN" | grep -q '^https://'; then
|
if printf '%s' "$_CONSOLE_DOMAIN" | grep -q '^https://'; then
|
||||||
_WS_SCHEME="wss"
|
_WS_SCHEME="wss"
|
||||||
else
|
else
|
||||||
_WS_SCHEME="ws"
|
_WS_SCHEME="ws"
|
||||||
fi
|
fi
|
||||||
_DEPLOY_HOST="$(printf '%s' "$_CONSOLE_DOMAIN" | sed 's|https\?://||')"
|
_DEPLOY_HOST="$(printf '%s' "$_CONSOLE_DOMAIN" | sed 's|https\?://||; s|/.*||')"
|
||||||
_SDK_IM_WS_URL="${_WS_SCHEME}://${_DEPLOY_HOST}/ws/im"
|
_SDK_IM_WS_URL="${_WS_SCHEME}://${_DEPLOY_HOST}/ws/im"
|
||||||
_SDK_IM_API_URL="${_CONSOLE_DOMAIN}"
|
_SDK_IM_API_URL="${_CONSOLE_DOMAIN}"
|
||||||
_SDK_FILE_URL="${_CONSOLE_DOMAIN}"
|
_SDK_FILE_URL="${_CONSOLE_DOMAIN}"
|
||||||
|
|
||||||
_FIXED=0
|
_FIXED=0
|
||||||
|
|
||||||
# 检查 SDK_IM_WS_URL(xuqinmin.com 残留 或 内网/裸 IP 均需修复)
|
# SDK_IM_WS_URL:ws/wss → 转成 http/https 格式后复用 _url_needs_fix
|
||||||
_CURRENT_WS="$(grep '^SDK_IM_WS_URL=' "$ROOT_DIR/config/xuqm.env" 2>/dev/null | cut -d= -f2- || echo '')"
|
_CURRENT_WS="$(grep '^SDK_IM_WS_URL=' "$ROOT_DIR/config/xuqm.env" 2>/dev/null | cut -d= -f2- || true)"
|
||||||
# ws/wss URL 转为 http/https 格式后复用 _url_needs_fix
|
_CURRENT_WS_HTTP="$(printf '%s' "$_CURRENT_WS" | sed 's|^wss://|https://|; s|^ws://|http://|')"
|
||||||
_CURRENT_WS_AS_HTTP="$(printf '%s' "$_CURRENT_WS" | sed 's|^wss://|https://|; s|^ws://|http://|')"
|
if _url_needs_fix "$_CURRENT_WS_HTTP"; then
|
||||||
if _url_needs_fix "$_CURRENT_WS_AS_HTTP"; then
|
|
||||||
_set_env "$ROOT_DIR/config/xuqm.env" "SDK_IM_WS_URL" "$_SDK_IM_WS_URL"
|
_set_env "$ROOT_DIR/config/xuqm.env" "SDK_IM_WS_URL" "$_SDK_IM_WS_URL"
|
||||||
ok "SDK_IM_WS_URL 已更新 → ${_SDK_IM_WS_URL}"
|
ok "SDK_IM_WS_URL 已更新 → ${_SDK_IM_WS_URL}"
|
||||||
_FIXED=1
|
_FIXED=1
|
||||||
@ -142,20 +164,7 @@ else
|
|||||||
ok "SDK_IM_WS_URL 正常: ${_CURRENT_WS}"
|
ok "SDK_IM_WS_URL 正常: ${_CURRENT_WS}"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# 辅助函数:检查一个 URL 是否需要修复(空、xuqinmin.com 残留、或裸 IP)
|
_CURRENT_IM_API="$(grep '^SDK_IM_API_URL=' "$ROOT_DIR/config/xuqm.env" 2>/dev/null | cut -d= -f2- || true)"
|
||||||
_url_needs_fix() {
|
|
||||||
local val="$1"
|
|
||||||
local host
|
|
||||||
host="$(printf '%s' "$val" | sed 's|https\?://||' | cut -d/ -f1 | cut -d: -f1)"
|
|
||||||
[ -z "$val" ] && return 0
|
|
||||||
printf '%s' "$val" | grep -qi 'xuqinmin\.com' && return 0
|
|
||||||
printf '%s' "$host" | grep -qE \
|
|
||||||
'^(10\.|172\.(1[6-9]|2[0-9]|3[01])\.|192\.168\.|127\.|[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$)' && return 0
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
# 检查 SDK_IM_API_URL
|
|
||||||
_CURRENT_IM_API="$(grep '^SDK_IM_API_URL=' "$ROOT_DIR/config/xuqm.env" 2>/dev/null | cut -d= -f2- || echo '')"
|
|
||||||
if _url_needs_fix "$_CURRENT_IM_API"; then
|
if _url_needs_fix "$_CURRENT_IM_API"; then
|
||||||
_set_env "$ROOT_DIR/config/xuqm.env" "SDK_IM_API_URL" "$_SDK_IM_API_URL"
|
_set_env "$ROOT_DIR/config/xuqm.env" "SDK_IM_API_URL" "$_SDK_IM_API_URL"
|
||||||
ok "SDK_IM_API_URL 已更新 → ${_SDK_IM_API_URL}"
|
ok "SDK_IM_API_URL 已更新 → ${_SDK_IM_API_URL}"
|
||||||
@ -164,8 +173,7 @@ else
|
|||||||
ok "SDK_IM_API_URL 正常: ${_CURRENT_IM_API}"
|
ok "SDK_IM_API_URL 正常: ${_CURRENT_IM_API}"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# 检查 SDK_FILE_SERVICE_URL
|
_CURRENT_FILE="$(grep '^SDK_FILE_SERVICE_URL=' "$ROOT_DIR/config/xuqm.env" 2>/dev/null | cut -d= -f2- || true)"
|
||||||
_CURRENT_FILE="$(grep '^SDK_FILE_SERVICE_URL=' "$ROOT_DIR/config/xuqm.env" 2>/dev/null | cut -d= -f2- || echo '')"
|
|
||||||
if _url_needs_fix "$_CURRENT_FILE"; then
|
if _url_needs_fix "$_CURRENT_FILE"; then
|
||||||
_set_env "$ROOT_DIR/config/xuqm.env" "SDK_FILE_SERVICE_URL" "$_SDK_FILE_URL"
|
_set_env "$ROOT_DIR/config/xuqm.env" "SDK_FILE_SERVICE_URL" "$_SDK_FILE_URL"
|
||||||
ok "SDK_FILE_SERVICE_URL 已更新 → ${_SDK_FILE_URL}"
|
ok "SDK_FILE_SERVICE_URL 已更新 → ${_SDK_FILE_URL}"
|
||||||
@ -174,32 +182,31 @@ else
|
|||||||
ok "SDK_FILE_SERVICE_URL 正常: ${_CURRENT_FILE}"
|
ok "SDK_FILE_SERVICE_URL 正常: ${_CURRENT_FILE}"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# 检查 .env 中是否有残留 OPS_DOMAIN
|
# 清理 .env 中残留的 OPS_DOMAIN
|
||||||
if grep -q '^OPS_DOMAIN=' "$ROOT_DIR/.env" 2>/dev/null; then
|
if grep -q '^OPS_DOMAIN=' "$ROOT_DIR/.env" 2>/dev/null; then
|
||||||
python3 - "$ROOT_DIR/.env" <<'PY'
|
python3 - "$ROOT_DIR/.env" <<'PY'
|
||||||
import sys, re
|
import sys, re
|
||||||
path = sys.argv[1]
|
content = open(sys.argv[1]).read()
|
||||||
content = open(path).read()
|
|
||||||
content = re.sub(r'^OPS_DOMAIN=.*\n?', '', content, flags=re.MULTILINE)
|
content = re.sub(r'^OPS_DOMAIN=.*\n?', '', content, flags=re.MULTILINE)
|
||||||
open(path, 'w').write(content)
|
open(sys.argv[1], 'w').write(content)
|
||||||
PY
|
PY
|
||||||
ok ".env 中已清理 OPS_DOMAIN"
|
ok ".env 中已清理 OPS_DOMAIN"
|
||||||
_FIXED=1
|
_FIXED=1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# 扫描 config/xuqm.env 是否还有其它公网地址残留
|
# 扫描是否还有其他 xuqinmin.com 残留
|
||||||
if grep -qi 'xuqinmin\.com' "$ROOT_DIR/config/xuqm.env" 2>/dev/null; then
|
if grep -qi 'xuqinmin\.com' "$ROOT_DIR/config/xuqm.env" 2>/dev/null; then
|
||||||
warn "config/xuqm.env 中仍有 xuqinmin.com 字样,请人工核查:"
|
warn "config/xuqm.env 中仍有 xuqinmin.com 字样,请人工核查:"
|
||||||
grep -n 'xuqinmin\.com' "$ROOT_DIR/config/xuqm.env" | while IFS= read -r line; do
|
grep -n 'xuqinmin\.com' "$ROOT_DIR/config/xuqm.env" | while IFS= read -r _line; do
|
||||||
printf ' %s\n' "$line"
|
printf ' %s\n' "$_line"
|
||||||
done
|
done
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ "$_FIXED" -eq 0 ]; then
|
[ "$_FIXED" -eq 0 ] && ok "SDK 配置无需修复"
|
||||||
ok "配置无需修复"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# 同步更新 config/sdk/xuqm-private-sdk.json(SDK 初始化配置文件)
|
# ---------------------------------------------------------------------------
|
||||||
|
# Step 3 — 同步 SDK 初始化配置文件
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
if [ -f "$ROOT_DIR/config/sdk/xuqm-private-sdk.json" ]; then
|
if [ -f "$ROOT_DIR/config/sdk/xuqm-private-sdk.json" ]; then
|
||||||
python3 - "$ROOT_DIR/config/sdk/xuqm-private-sdk.json" \
|
python3 - "$ROOT_DIR/config/sdk/xuqm-private-sdk.json" \
|
||||||
"$_CONSOLE_DOMAIN" "$_SDK_IM_WS_URL" <<'PY'
|
"$_CONSOLE_DOMAIN" "$_SDK_IM_WS_URL" <<'PY'
|
||||||
@ -215,17 +222,192 @@ try:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f'warning: {e}', file=sys.stderr)
|
print(f'warning: {e}', file=sys.stderr)
|
||||||
PY
|
PY
|
||||||
ok "config/sdk/xuqm-private-sdk.json 已同步更新"
|
ok "config/sdk/xuqm-private-sdk.json 已同步"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# 2. 可选:拉取最新镜像
|
# Step 4 — 检测并修复宿主机 nginx WebSocket 代理头
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
log "检查宿主机 nginx WebSocket 配置"
|
||||||
|
|
||||||
|
if ! command -v nginx >/dev/null 2>&1; then
|
||||||
|
info "宿主机未安装 nginx,跳过(流量直接进入容器 nginx)"
|
||||||
|
else
|
||||||
|
_nginx_result="$(python3 - "$_NGINX_PORT" <<'PYEOF'
|
||||||
|
import sys, os, re, shutil, subprocess
|
||||||
|
|
||||||
|
port = sys.argv[1]
|
||||||
|
|
||||||
|
# 获取 nginx 完整配置
|
||||||
|
try:
|
||||||
|
r = subprocess.run(['nginx', '-T'], capture_output=True, text=True, timeout=15)
|
||||||
|
dump = r.stdout
|
||||||
|
except Exception as e:
|
||||||
|
print(f'SKIP:nginx -T 失败: {e}')
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
# 解析 nginx -T 输出,按文件分组
|
||||||
|
file_contents = {}
|
||||||
|
cur_file = None
|
||||||
|
cur_lines = []
|
||||||
|
for line in dump.splitlines():
|
||||||
|
if line.startswith('# configuration file '):
|
||||||
|
if cur_file:
|
||||||
|
file_contents[cur_file] = '\n'.join(cur_lines)
|
||||||
|
cur_file = line.removeprefix('# configuration file ').rstrip(':')
|
||||||
|
cur_lines = []
|
||||||
|
else:
|
||||||
|
cur_lines.append(line)
|
||||||
|
if cur_file:
|
||||||
|
file_contents[cur_file] = '\n'.join(cur_lines)
|
||||||
|
|
||||||
|
proxy_re = re.compile(
|
||||||
|
r'proxy_pass\s+https?://(?:127\.0\.0\.1|localhost):' + re.escape(port) + r'\s*;',
|
||||||
|
re.IGNORECASE
|
||||||
|
)
|
||||||
|
|
||||||
|
def has_ws_headers(text):
|
||||||
|
return (re.search(r'proxy_http_version\s+1\.1', text) and
|
||||||
|
re.search(r'proxy_set_header\s+Upgrade', text, re.IGNORECASE) and
|
||||||
|
re.search(r'proxy_set_header\s+Connection.*upgrade', text, re.IGNORECASE))
|
||||||
|
|
||||||
|
WS_INJECT = (
|
||||||
|
'\n proxy_http_version 1.1;'
|
||||||
|
'\n proxy_set_header Upgrade $http_upgrade;'
|
||||||
|
'\n proxy_set_header Connection "upgrade";'
|
||||||
|
'\n proxy_read_timeout 3600s;'
|
||||||
|
)
|
||||||
|
|
||||||
|
def find_location_blocks(text):
|
||||||
|
"""返回 (start, end) 列表,每个 location { ... } 块的范围(支持嵌套)"""
|
||||||
|
results = []
|
||||||
|
i = 0
|
||||||
|
while i < len(text):
|
||||||
|
m = re.search(r'\blocation\b[^{]*\{', text[i:])
|
||||||
|
if not m:
|
||||||
|
break
|
||||||
|
abs_start = i + m.start()
|
||||||
|
depth = 1
|
||||||
|
j = i + m.end()
|
||||||
|
while j < len(text) and depth:
|
||||||
|
if text[j] == '{': depth += 1
|
||||||
|
elif text[j] == '}': depth -= 1
|
||||||
|
j += 1
|
||||||
|
results.append((abs_start, j))
|
||||||
|
i = i + m.end()
|
||||||
|
return results
|
||||||
|
|
||||||
|
files_fixed = []
|
||||||
|
files_checked = []
|
||||||
|
|
||||||
|
for fpath, content in file_contents.items():
|
||||||
|
if not os.path.isfile(fpath):
|
||||||
|
continue
|
||||||
|
if not proxy_re.search(content):
|
||||||
|
continue
|
||||||
|
files_checked.append(fpath)
|
||||||
|
|
||||||
|
with open(fpath) as f:
|
||||||
|
real = f.read()
|
||||||
|
|
||||||
|
blocks = find_location_blocks(real)
|
||||||
|
changed = False
|
||||||
|
offset = 0
|
||||||
|
|
||||||
|
for start, end in blocks:
|
||||||
|
block = real[start + offset: end + offset]
|
||||||
|
if not proxy_re.search(block):
|
||||||
|
continue
|
||||||
|
if has_ws_headers(block):
|
||||||
|
continue
|
||||||
|
# 在 proxy_pass 行后注入 WebSocket 头
|
||||||
|
patched = re.sub(
|
||||||
|
r'(proxy_pass\s+[^;]+;)',
|
||||||
|
r'\1' + WS_INJECT,
|
||||||
|
block,
|
||||||
|
count=1
|
||||||
|
)
|
||||||
|
real = real[:start + offset] + patched + real[end + offset:]
|
||||||
|
offset += len(patched) - len(block)
|
||||||
|
changed = True
|
||||||
|
|
||||||
|
if not changed:
|
||||||
|
continue
|
||||||
|
|
||||||
|
backup = fpath + '.xuqm.bak'
|
||||||
|
shutil.copy2(fpath, backup)
|
||||||
|
try:
|
||||||
|
with open(fpath, 'w') as f:
|
||||||
|
f.write(real)
|
||||||
|
except Exception as e:
|
||||||
|
shutil.copy2(backup, fpath)
|
||||||
|
print(f'WARN:写入 {fpath} 失败: {e}')
|
||||||
|
continue
|
||||||
|
|
||||||
|
# 验证语法
|
||||||
|
r2 = subprocess.run(['nginx', '-t'], capture_output=True, text=True)
|
||||||
|
if r2.returncode != 0:
|
||||||
|
shutil.copy2(backup, fpath)
|
||||||
|
print(f'WARN:nginx -t 失败,已回滚 {fpath}')
|
||||||
|
print(r2.stderr.strip())
|
||||||
|
else:
|
||||||
|
files_fixed.append(fpath)
|
||||||
|
|
||||||
|
if files_fixed:
|
||||||
|
print('FIXED:' + ','.join(files_fixed))
|
||||||
|
elif files_checked:
|
||||||
|
print('OK:WebSocket 头已存在')
|
||||||
|
else:
|
||||||
|
print('NONE:未发现代理到容器 nginx 的配置')
|
||||||
|
PYEOF
|
||||||
|
)"
|
||||||
|
|
||||||
|
case "${_nginx_result%%:*}" in
|
||||||
|
FIXED)
|
||||||
|
_fixed_files="${_nginx_result#FIXED:}"
|
||||||
|
ok "宿主机 nginx 已自动补全 WebSocket 代理头"
|
||||||
|
for _f in $(printf '%s' "$_fixed_files" | tr ',' '\n'); do
|
||||||
|
info "已修改: ${_f}(原文件备份为 ${_f}.xuqm.bak)"
|
||||||
|
done
|
||||||
|
info "正在重载宿主机 nginx ..."
|
||||||
|
nginx -s reload 2>/dev/null && ok "nginx reload 成功" || warn "nginx reload 失败,请手动执行: nginx -s reload"
|
||||||
|
;;
|
||||||
|
OK)
|
||||||
|
ok "宿主机 nginx WebSocket 头已存在,无需修改"
|
||||||
|
;;
|
||||||
|
NONE)
|
||||||
|
info "未发现代理到容器 nginx(端口 ${_NGINX_PORT})的配置,如有上层代理请手动确认:"
|
||||||
|
printf '\n'
|
||||||
|
printf ' location / {\n'
|
||||||
|
printf ' proxy_pass http://<本机IP>:%s;\n' "$_NGINX_PORT"
|
||||||
|
printf ' proxy_http_version 1.1;\n'
|
||||||
|
printf ' proxy_set_header Upgrade $http_upgrade;\n'
|
||||||
|
printf ' proxy_set_header Connection "upgrade";\n'
|
||||||
|
printf ' proxy_set_header Host $host;\n'
|
||||||
|
printf ' proxy_set_header X-Real-IP $remote_addr;\n'
|
||||||
|
printf ' proxy_set_header X-Forwarded-Proto $scheme;\n'
|
||||||
|
printf ' proxy_read_timeout 3600s;\n'
|
||||||
|
printf ' }\n\n'
|
||||||
|
;;
|
||||||
|
SKIP)
|
||||||
|
warn "宿主机 nginx 检查跳过: ${_nginx_result#SKIP:}"
|
||||||
|
;;
|
||||||
|
WARN*)
|
||||||
|
warn "${_nginx_result#WARN:}"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
warn "宿主机 nginx 检查返回未知结果,请手动核查 WebSocket 头配置"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Step 5 — 可选拉取最新镜像
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
log "拉取最新镜像(可选)"
|
log "拉取最新镜像(可选)"
|
||||||
|
|
||||||
read -rp " 是否拉取最新镜像?(y/N): " _pull_choice
|
read -rp " 是否拉取最新镜像?(y/N): " _pull_choice
|
||||||
if [ "${_pull_choice:-n}" = "y" ] || [ "${_pull_choice:-n}" = "Y" ]; then
|
if [ "${_pull_choice:-n}" = "y" ] || [ "${_pull_choice:-n}" = "Y" ]; then
|
||||||
_REGISTRY="${REGISTRY:-}"
|
|
||||||
_REGISTRY_HOST="${REGISTRY_HOST:-}"
|
_REGISTRY_HOST="${REGISTRY_HOST:-}"
|
||||||
_REGISTRY_USER="${REGISTRY_USER:-}"
|
_REGISTRY_USER="${REGISTRY_USER:-}"
|
||||||
_REGISTRY_PASSWORD="${REGISTRY_PASSWORD:-}"
|
_REGISTRY_PASSWORD="${REGISTRY_PASSWORD:-}"
|
||||||
@ -234,7 +416,7 @@ if [ "${_pull_choice:-n}" = "y" ] || [ "${_pull_choice:-n}" = "Y" ]; then
|
|||||||
printf '%s' "$_REGISTRY_PASSWORD" | \
|
printf '%s' "$_REGISTRY_PASSWORD" | \
|
||||||
docker login "$_REGISTRY_HOST" -u "$_REGISTRY_USER" --password-stdin 2>/dev/null \
|
docker login "$_REGISTRY_HOST" -u "$_REGISTRY_USER" --password-stdin 2>/dev/null \
|
||||||
&& ok "镜像仓库登录成功" \
|
&& ok "镜像仓库登录成功" \
|
||||||
|| warn "镜像仓库登录失败,将使用本地缓存镜像"
|
|| warn "镜像仓库登录失败,将尝试使用本地缓存"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
docker compose \
|
docker compose \
|
||||||
@ -242,73 +424,80 @@ if [ "${_pull_choice:-n}" = "y" ] || [ "${_pull_choice:-n}" = "Y" ]; then
|
|||||||
-f "$ROOT_DIR/docker-compose.yml" \
|
-f "$ROOT_DIR/docker-compose.yml" \
|
||||||
-f "$ROOT_DIR/docker-compose.infra.yml" \
|
-f "$ROOT_DIR/docker-compose.infra.yml" \
|
||||||
pull --ignore-pull-failures 2>/dev/null \
|
pull --ignore-pull-failures 2>/dev/null \
|
||||||
&& ok "镜像已拉取" \
|
&& ok "镜像已更新" \
|
||||||
|| warn "部分镜像拉取失败,继续使用本地缓存"
|
|| warn "部分镜像拉取失败,将使用本地缓存继续"
|
||||||
else
|
else
|
||||||
info "跳过镜像拉取"
|
info "跳过镜像拉取"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# 3. 重启受影响的容器
|
# Step 6 — 重启受影响的容器
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
log "重启受影响的容器"
|
log "重启受影响的容器"
|
||||||
|
|
||||||
# 加载 secrets 到环境(docker compose 需要)
|
# 重新加载 secrets(docker compose 需要环境变量)
|
||||||
set -a
|
set -a
|
||||||
# shellcheck disable=SC1090
|
# shellcheck disable=SC1090
|
||||||
. "$ROOT_DIR/config/secrets.env"
|
. "$ROOT_DIR/config/secrets.env"
|
||||||
set +a
|
set +a
|
||||||
|
|
||||||
_COMPOSE="docker compose --env-file ${ROOT_DIR}/.env -f ${ROOT_DIR}/docker-compose.yml -f ${ROOT_DIR}/docker-compose.infra.yml"
|
_COMPOSE="docker compose --env-file ${ROOT_DIR}/.env \
|
||||||
|
-f ${ROOT_DIR}/docker-compose.yml \
|
||||||
|
-f ${ROOT_DIR}/docker-compose.infra.yml"
|
||||||
|
|
||||||
# tenant-service:读取 env_file(包含 SDK_IM_WS_URL),必须重启
|
|
||||||
info "重启 tenant-service ..."
|
info "重启 tenant-service ..."
|
||||||
$_COMPOSE restart tenant-service
|
$_COMPOSE restart tenant-service
|
||||||
ok "tenant-service 已重启"
|
ok "tenant-service 已重启"
|
||||||
|
|
||||||
# nginx:挂载 config/nginx/conf.d(可能有 sub_filter 更新),重启生效
|
|
||||||
info "重启 nginx ..."
|
info "重启 nginx ..."
|
||||||
$_COMPOSE restart nginx
|
$_COMPOSE restart nginx
|
||||||
ok "nginx 已重启"
|
ok "nginx 已重启"
|
||||||
|
|
||||||
# 等待 tenant-service 健康
|
# ---------------------------------------------------------------------------
|
||||||
|
# Step 7 — 等待 tenant-service 健康
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
printf ' 等待 tenant-service 就绪'
|
printf ' 等待 tenant-service 就绪'
|
||||||
for i in $(seq 1 30); do
|
_healthy=0
|
||||||
code="$(curl -skL --noproxy '*' -o /dev/null -w '%{http_code}' --max-time 4 \
|
for i in $(seq 1 40); do
|
||||||
|
_code="$(curl -skL --noproxy '*' -o /dev/null -w '%{http_code}' --max-time 4 \
|
||||||
"http://127.0.0.1:11224/actuator/health" 2>/dev/null || echo 000)"
|
"http://127.0.0.1:11224/actuator/health" 2>/dev/null || echo 000)"
|
||||||
if [ "$code" = "200" ]; then
|
if [ "$_code" = "200" ]; then
|
||||||
printf '\n'
|
printf '\n'
|
||||||
ok "tenant-service 健康 (HTTP 200)"
|
ok "tenant-service 健康 (HTTP 200)"
|
||||||
|
_healthy=1
|
||||||
break
|
break
|
||||||
fi
|
fi
|
||||||
printf '.'
|
printf '.'
|
||||||
sleep 3
|
sleep 3
|
||||||
[ "$i" -eq 30 ] && {
|
|
||||||
printf '\n'
|
|
||||||
warn "tenant-service 未在 90s 内响应,请手动检查:docker compose logs --tail 50 tenant-service"
|
|
||||||
break
|
|
||||||
}
|
|
||||||
done
|
done
|
||||||
|
if [ "$_healthy" -eq 0 ]; then
|
||||||
# ---------------------------------------------------------------------------
|
printf '\n'
|
||||||
# 4. 自动处理积压的 PENDING 服务开通申请
|
warn "tenant-service 在 120s 内未响应,请检查: docker compose logs --tail 60 tenant-service"
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
log "处理积压的服务开通申请"
|
|
||||||
|
|
||||||
_approve_resp="$(curl -sk --noproxy '*' -X POST \
|
|
||||||
"http://127.0.0.1:11224/api/private/admin/approve-pending-requests" \
|
|
||||||
--max-time 10 2>/dev/null || echo '')"
|
|
||||||
_approved="$(printf '%s' "$_approve_resp" | \
|
|
||||||
python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('data',{}).get('approved',0))" \
|
|
||||||
2>/dev/null || echo 0)"
|
|
||||||
if [ "${_approved:-0}" -gt 0 ]; then
|
|
||||||
ok "已自动开通 ${_approved} 条积压申请"
|
|
||||||
else
|
|
||||||
ok "无积压申请"
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# 5. 全量验证
|
# Step 8 — 自动处理积压的 PENDING 服务开通申请
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
log "处理积压的服务开通申请"
|
||||||
|
|
||||||
|
if [ "$_healthy" -eq 1 ]; then
|
||||||
|
_approve_resp="$(curl -sk --noproxy '*' -X POST \
|
||||||
|
"http://127.0.0.1:11224/api/private/admin/approve-pending-requests" \
|
||||||
|
--max-time 10 2>/dev/null || true)"
|
||||||
|
_approved="$(printf '%s' "$_approve_resp" | \
|
||||||
|
python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('data',{}).get('approved',0))" \
|
||||||
|
2>/dev/null || echo 0)"
|
||||||
|
if [ "${_approved:-0}" -gt 0 ]; then
|
||||||
|
ok "已自动开通 ${_approved} 条积压申请"
|
||||||
|
else
|
||||||
|
ok "无积压申请"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
warn "tenant-service 未健康,跳过积压申请处理"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Step 9 — 全量验证
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
log "运行全量验证"
|
log "运行全量验证"
|
||||||
|
|
||||||
@ -325,4 +514,4 @@ printf "\n${BOLD}═════════════════════
|
|||||||
printf "${BOLD} 热更新完成${RESET}\n"
|
printf "${BOLD} 热更新完成${RESET}\n"
|
||||||
printf "${BOLD}══════════════════════════════════════════════════${RESET}\n"
|
printf "${BOLD}══════════════════════════════════════════════════${RESET}\n"
|
||||||
printf "\n 访问地址:${BOLD}%s${RESET}\n" "$_CONSOLE_DOMAIN"
|
printf "\n 访问地址:${BOLD}%s${RESET}\n" "$_CONSOLE_DOMAIN"
|
||||||
printf " SDK IM WS:${BOLD}%s${RESET}\n\n" "$_SDK_IM_WS_URL"
|
printf " IM WS :${BOLD}%s${RESET}\n\n" "$_SDK_IM_WS_URL"
|
||||||
|
|||||||
正在加载...
在新工单中引用
屏蔽一个用户