#!/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"