核心变更: - 完全移除 ops-web 容器(私有化部署无需运营后台) - nginx sub_filter 替换前端 JS bundle 中的公网 SDK URL - deploy.sh 写入正确的 SDK_IM_WS_URL / SDK_IM_API_URL / SDK_FILE_SERVICE_URL - 新增 scripts/update.sh:热更新脚本,修复配置 + 可选拉镜像 + 重启 + 验证 - 新增 upgrade.sh:一键升级入口,curl 下载后直接执行,流程同 install.sh - install.sh 检测已有部署(.env 存在),自动路由到 update.sh 而非重跑向导 - 关键配置文件(.env / secrets.env / xuqm.env)在 tarball 解压前备份后恢复 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
282 行
11 KiB
Bash
可执行文件
282 行
11 KiB
Bash
可执行文件
#!/usr/bin/env bash
|
||
# update.sh — XuqmGroup 私有化部署热更新脚本
|
||
#
|
||
# 用途:在已部署的环境上执行,无需重新配置基础信息:
|
||
# 1. 自动读取现有配置(.env / config/secrets.env)
|
||
# 2. 修复已知配置问题(SDK 地址、残留公网 URL 等)
|
||
# 3. 可选:拉取最新镜像
|
||
# 4. 重启受影响的容器
|
||
# 5. 重新运行全量验证
|
||
#
|
||
# 前提:已执行过 install.sh 完成初始部署
|
||
|
||
set -euo pipefail
|
||
|
||
# 自动定位安装目录:优先脚本所在目录的上级,否则搜索常见路径
|
||
_script_parent="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||
if [ -f "$_script_parent/docker-compose.yml" ]; then
|
||
ROOT_DIR="$_script_parent"
|
||
else
|
||
ROOT_DIR=""
|
||
for _d in /opt/xuqm-private /opt/xuqm /root/xuqm-private; do
|
||
[ -f "$_d/docker-compose.yml" ] && ROOT_DIR="$_d" && break
|
||
done
|
||
if [ -z "$ROOT_DIR" ]; then
|
||
read -rp " 请输入部署目录路径(如 /opt/xuqm-private): " ROOT_DIR
|
||
[ -f "$ROOT_DIR/docker-compose.yml" ] || \
|
||
{ printf "\nERROR: %s 下未找到 docker-compose.yml\n" "$ROOT_DIR" >&2; exit 1; }
|
||
fi
|
||
fi
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# 工具函数
|
||
# ---------------------------------------------------------------------------
|
||
BOLD='\033[1m'; RESET='\033[0m'
|
||
CYAN='\033[1;36m'; GREEN='\033[32m'; YELLOW='\033[33m'; RED='\033[1;31m'
|
||
|
||
log() { printf "\n${CYAN}[update] %s${RESET}\n" "$*"; }
|
||
ok() { printf " ${GREEN}✓${RESET} %s\n" "$*"; }
|
||
warn() { printf " ${YELLOW}⚠${RESET} %s\n" "$*"; }
|
||
fail() { printf "\n${RED}ERROR: %s${RESET}\n" "$*" >&2; exit 1; }
|
||
info() { printf " → %s\n" "$*"; }
|
||
|
||
# 修改 env 文件中某个 key 的值;若 key 不存在则追加
|
||
_set_env() {
|
||
local file="$1" key="$2" val="$3"
|
||
local tmp; tmp="$(mktemp)"
|
||
if grep -q "^${key}=" "$file" 2>/dev/null; then
|
||
# 用 python3 替换,避免 sed 在 URL 值中的斜杠转义问题
|
||
python3 - "$file" "$key" "$val" <<'PY'
|
||
import sys, re
|
||
path, key, val = sys.argv[1], sys.argv[2], sys.argv[3]
|
||
content = open(path).read()
|
||
new = re.sub(r'^' + re.escape(key) + r'=.*$', key + '=' + val, content, flags=re.MULTILINE)
|
||
open(path, 'w').write(new)
|
||
PY
|
||
else
|
||
printf '%s=%s\n' "$key" "$val" >> "$file"
|
||
fi
|
||
rm -f "$tmp"
|
||
}
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# 前置检查
|
||
# ---------------------------------------------------------------------------
|
||
printf "\n${BOLD}══════════════════════════════════════════════════${RESET}\n"
|
||
printf "${BOLD} XuqmGroup 私有化部署热更新${RESET}\n"
|
||
printf "${BOLD}══════════════════════════════════════════════════${RESET}\n"
|
||
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/config/secrets.env" ] || fail "未找到 config/secrets.env,请先执行 install.sh 完成初始部署"
|
||
|
||
command -v docker >/dev/null 2>&1 || fail "Docker 未安装"
|
||
command -v python3 >/dev/null 2>&1 || fail "python3 未安装"
|
||
docker info >/dev/null 2>&1 || fail "Docker daemon 未运行"
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# 加载现有配置
|
||
# ---------------------------------------------------------------------------
|
||
log "加载现有配置"
|
||
|
||
# shellcheck disable=SC1090,SC1091
|
||
set -a
|
||
. "$ROOT_DIR/.env"
|
||
. "$ROOT_DIR/config/secrets.env"
|
||
set +a
|
||
|
||
# 从 config/xuqm.env 中读取 CONSOLE_DOMAIN(不用 source,避免覆盖已加载值)
|
||
_CONSOLE_DOMAIN="$(grep '^CONSOLE_DOMAIN=' "$ROOT_DIR/config/xuqm.env" 2>/dev/null | cut -d= -f2- | tr -d '"' | tr -d "'")"
|
||
_NGINX_BIND="${NGINX_BIND:-80}"
|
||
_NGINX_PORT="${_NGINX_BIND##*:}"
|
||
|
||
ok "CONSOLE_DOMAIN=${_CONSOLE_DOMAIN:-(未设置)}"
|
||
ok "NGINX_BIND=${_NGINX_BIND}"
|
||
|
||
[ -n "$_CONSOLE_DOMAIN" ] || fail "config/xuqm.env 中 CONSOLE_DOMAIN 未设置,请手动补充后重试"
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# 1. 修复配置问题
|
||
# ---------------------------------------------------------------------------
|
||
log "检查并修复配置"
|
||
|
||
# 根据 CONSOLE_DOMAIN 派生正确的 SDK 地址
|
||
if printf '%s' "$_CONSOLE_DOMAIN" | grep -q '^https://'; then
|
||
_WS_SCHEME="wss"
|
||
else
|
||
_WS_SCHEME="ws"
|
||
fi
|
||
_DEPLOY_HOST="$(printf '%s' "$_CONSOLE_DOMAIN" | sed 's|https\?://||')"
|
||
_SDK_IM_WS_URL="${_WS_SCHEME}://${_DEPLOY_HOST}/ws/im"
|
||
_SDK_IM_API_URL="${_CONSOLE_DOMAIN}"
|
||
_SDK_FILE_URL="${_CONSOLE_DOMAIN}"
|
||
|
||
_FIXED=0
|
||
|
||
# 检查 SDK_IM_WS_URL
|
||
_CURRENT_WS="$(grep '^SDK_IM_WS_URL=' "$ROOT_DIR/config/xuqm.env" 2>/dev/null | cut -d= -f2- || echo '')"
|
||
if [ -z "$_CURRENT_WS" ] || printf '%s' "$_CURRENT_WS" | grep -qi 'xuqinmin\.com'; then
|
||
_set_env "$ROOT_DIR/config/xuqm.env" "SDK_IM_WS_URL" "$_SDK_IM_WS_URL"
|
||
ok "SDK_IM_WS_URL 已更新 → ${_SDK_IM_WS_URL}"
|
||
_FIXED=1
|
||
else
|
||
ok "SDK_IM_WS_URL 正常: ${_CURRENT_WS}"
|
||
fi
|
||
|
||
# 检查 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 [ -z "$_CURRENT_IM_API" ] || printf '%s' "$_CURRENT_IM_API" | grep -qi 'xuqinmin\.com'; then
|
||
_set_env "$ROOT_DIR/config/xuqm.env" "SDK_IM_API_URL" "$_SDK_IM_API_URL"
|
||
ok "SDK_IM_API_URL 已更新 → ${_SDK_IM_API_URL}"
|
||
_FIXED=1
|
||
else
|
||
ok "SDK_IM_API_URL 正常: ${_CURRENT_IM_API}"
|
||
fi
|
||
|
||
# 检查 SDK_FILE_SERVICE_URL
|
||
_CURRENT_FILE="$(grep '^SDK_FILE_SERVICE_URL=' "$ROOT_DIR/config/xuqm.env" 2>/dev/null | cut -d= -f2- || echo '')"
|
||
if [ -z "$_CURRENT_FILE" ] || printf '%s' "$_CURRENT_FILE" | grep -qi 'xuqinmin\.com'; then
|
||
_set_env "$ROOT_DIR/config/xuqm.env" "SDK_FILE_SERVICE_URL" "$_SDK_FILE_URL"
|
||
ok "SDK_FILE_SERVICE_URL 已更新 → ${_SDK_FILE_URL}"
|
||
_FIXED=1
|
||
else
|
||
ok "SDK_FILE_SERVICE_URL 正常: ${_CURRENT_FILE}"
|
||
fi
|
||
|
||
# 检查 .env 中是否有残留 OPS_DOMAIN
|
||
if grep -q '^OPS_DOMAIN=' "$ROOT_DIR/.env" 2>/dev/null; then
|
||
python3 - "$ROOT_DIR/.env" <<'PY'
|
||
import sys, re
|
||
path = sys.argv[1]
|
||
content = open(path).read()
|
||
content = re.sub(r'^OPS_DOMAIN=.*\n?', '', content, flags=re.MULTILINE)
|
||
open(path, 'w').write(content)
|
||
PY
|
||
ok ".env 中已清理 OPS_DOMAIN"
|
||
_FIXED=1
|
||
fi
|
||
|
||
# 扫描 config/xuqm.env 是否还有其它公网地址残留
|
||
if grep -qi 'xuqinmin\.com' "$ROOT_DIR/config/xuqm.env" 2>/dev/null; then
|
||
warn "config/xuqm.env 中仍有 xuqinmin.com 字样,请人工核查:"
|
||
grep -n 'xuqinmin\.com' "$ROOT_DIR/config/xuqm.env" | while IFS= read -r line; do
|
||
printf ' %s\n' "$line"
|
||
done
|
||
fi
|
||
|
||
if [ "$_FIXED" -eq 0 ]; then
|
||
ok "配置无需修复"
|
||
fi
|
||
|
||
# 同步更新 config/sdk/xuqm-private-sdk.json(SDK 初始化配置文件)
|
||
if [ -f "$ROOT_DIR/config/sdk/xuqm-private-sdk.json" ]; then
|
||
python3 - "$ROOT_DIR/config/sdk/xuqm-private-sdk.json" \
|
||
"$_CONSOLE_DOMAIN" "$_SDK_IM_WS_URL" <<'PY'
|
||
import json, sys
|
||
path, console, ws = sys.argv[1], sys.argv[2], sys.argv[3]
|
||
try:
|
||
d = json.load(open(path))
|
||
d['controlBaseUrl'] = console
|
||
d['fileBaseUrl'] = console
|
||
d['imApiBaseUrl'] = console
|
||
d['imWsUrl'] = ws
|
||
json.dump(d, open(path, 'w'), ensure_ascii=False, indent=2)
|
||
except Exception as e:
|
||
print(f'warning: {e}', file=sys.stderr)
|
||
PY
|
||
ok "config/sdk/xuqm-private-sdk.json 已同步更新"
|
||
fi
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# 2. 可选:拉取最新镜像
|
||
# ---------------------------------------------------------------------------
|
||
log "拉取最新镜像(可选)"
|
||
|
||
read -rp " 是否拉取最新镜像?(y/N): " _pull_choice
|
||
if [ "${_pull_choice:-n}" = "y" ] || [ "${_pull_choice:-n}" = "Y" ]; then
|
||
_REGISTRY="${REGISTRY:-}"
|
||
_REGISTRY_HOST="${REGISTRY_HOST:-}"
|
||
_REGISTRY_USER="${REGISTRY_USER:-}"
|
||
_REGISTRY_PASSWORD="${REGISTRY_PASSWORD:-}"
|
||
|
||
if [ -n "$_REGISTRY_PASSWORD" ] && [ -n "$_REGISTRY_HOST" ]; then
|
||
printf '%s' "$_REGISTRY_PASSWORD" | \
|
||
docker login "$_REGISTRY_HOST" -u "$_REGISTRY_USER" --password-stdin 2>/dev/null \
|
||
&& ok "镜像仓库登录成功" \
|
||
|| warn "镜像仓库登录失败,将使用本地缓存镜像"
|
||
fi
|
||
|
||
docker compose \
|
||
--env-file "$ROOT_DIR/.env" \
|
||
-f "$ROOT_DIR/docker-compose.yml" \
|
||
-f "$ROOT_DIR/docker-compose.infra.yml" \
|
||
pull --ignore-pull-failures 2>/dev/null \
|
||
&& ok "镜像已拉取" \
|
||
|| warn "部分镜像拉取失败,继续使用本地缓存"
|
||
else
|
||
info "跳过镜像拉取"
|
||
fi
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# 3. 重启受影响的容器
|
||
# ---------------------------------------------------------------------------
|
||
log "重启受影响的容器"
|
||
|
||
# 加载 secrets 到环境(docker compose 需要)
|
||
set -a
|
||
# shellcheck disable=SC1090
|
||
. "$ROOT_DIR/config/secrets.env"
|
||
set +a
|
||
|
||
_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 ..."
|
||
$_COMPOSE restart tenant-service
|
||
ok "tenant-service 已重启"
|
||
|
||
# nginx:挂载 config/nginx/conf.d(可能有 sub_filter 更新),重启生效
|
||
info "重启 nginx ..."
|
||
$_COMPOSE restart nginx
|
||
ok "nginx 已重启"
|
||
|
||
# 等待 tenant-service 健康
|
||
printf ' 等待 tenant-service 就绪'
|
||
for i in $(seq 1 30); 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)"
|
||
if [ "$code" = "200" ]; then
|
||
printf '\n'
|
||
ok "tenant-service 健康 (HTTP 200)"
|
||
break
|
||
fi
|
||
printf '.'
|
||
sleep 3
|
||
[ "$i" -eq 30 ] && {
|
||
printf '\n'
|
||
warn "tenant-service 未在 90s 内响应,请手动检查:docker compose logs --tail 50 tenant-service"
|
||
break
|
||
}
|
||
done
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# 4. 全量验证
|
||
# ---------------------------------------------------------------------------
|
||
log "运行全量验证"
|
||
|
||
if BASE_URL="http://127.0.0.1:${_NGINX_PORT}" bash "$ROOT_DIR/scripts/verify.sh"; then
|
||
ok "全量验证通过"
|
||
else
|
||
warn "部分验证项未通过,请查看上方输出"
|
||
fi
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# 完成
|
||
# ---------------------------------------------------------------------------
|
||
printf "\n${BOLD}══════════════════════════════════════════════════${RESET}\n"
|
||
printf "${BOLD} 热更新完成${RESET}\n"
|
||
printf "${BOLD}══════════════════════════════════════════════════${RESET}\n"
|
||
printf "\n 访问地址:${BOLD}%s${RESET}\n" "$_CONSOLE_DOMAIN"
|
||
printf " SDK IM WS:${BOLD}%s${RESET}\n\n" "$_SDK_IM_WS_URL"
|