#!/bin/bash set -e # 发生错误时终止脚本执行 # 仅支持 -h/--help 快速查看 if [[ "${1:-}" =~ ^(-h|--help)$ ]]; then echo "用法: $0" 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" echo " UPDATE_HEALTH_TIMEOUT_SECONDS=秒数" echo " 等待核心服务恢复可用的最长时间,默认 300 秒" echo "" echo "仓库更新后会自动执行 scripts/update-env.d 中的幂等环境配置迁移。" exit 0 fi # 进入脚本所在目录(项目根目录) SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" && pwd)" cd "$SCRIPT_DIR" # 命令行内选择更新方式(默认:更新并拉取仓库) SKIP_REPO_UPDATE=false 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 echo "" echo "===========================" echo "📥 拉取仓库最新代码" echo "===========================" if [ ! -d .git ]; then echo "❌ 当前目录不是 Git 仓库,无法执行 git pull" echo " 请使用 git clone 克隆项目后即可使用 update.sh 更新" echo " 克隆命令: git clone https://git.51easyai.com/wangbo/easyai && cd easyai" exit 1 fi before_revision="$(git rev-parse HEAD)" echo "📥 正在执行 git pull --ff-only..." if git pull --ff-only; then echo "✅ 仓库已更新到最新版本" else echo "❌ git pull 失败,请检查网络或远程仓库配置" exit 1 fi # 确保环境配置文件存在(从 .sample 生成,不覆盖已有文件) echo "" echo "📝 检查环境配置文件..." [ ! -f .env ] && [ -f .env.sample ] && cp .env.sample .env && echo " ✓ .env" [ ! -f .env.tools ] && [ -f .env.tools.sample ] && cp .env.tools.sample .env.tools && echo " ✓ .env.tools" [ ! -f .env.ASG ] && [ -f .env.ASG.sample ] && cp .env.ASG.sample .env.ASG && echo " ✓ .env.ASG" [ ! -f .env.AMS ] && [ -f .env.AMS.sample ] && cp .env.AMS.sample .env.AMS && echo " ✓ .env.AMS" after_revision="$(git rev-parse HEAD)" if [ "$before_revision" != "$after_revision" ] && [ "${EASYAI_UPDATE_REEXECUTED:-0}" != "1" ]; then echo "🔄 检测到部署仓库已更新,切换到新版更新脚本继续执行..." exec env EASYAI_UPDATE_REEXECUTED=1 UPDATE_SKIP_REPO_UPDATE=true "$SCRIPT_DIR/update.sh" fi echo "" else echo "⏭️ 跳过仓库更新,仅更新镜像并重启" echo "" fi # 升级已有部署时,将空值、历史默认 JWT 密钥和缺失的 WS ticket 密钥 # 一次性替换为持久化随机值,并确保网关声明 ws_ticket;已有强密钥保持不变。 if [ ! -f .env ]; then echo "❌ 未找到 .env,请先执行 start.sh 完成初始化" exit 1 fi # 每次更新都执行版本化、幂等的环境配置迁移。迁移只补缺失/空值或必要的列表项, # 不覆盖用户已经配置的非空值。后续新增必需配置时只需增加编号迁移文件。 bash ./scripts/apply-update-env-migrations.sh .env "$SCRIPT_DIR" # 老部署缺少绑定配置时,根据现有公开 API 地址迁移:域名/本机保持回环,IP 直连保持可用。 # shellcheck source=scripts/init-server-http-bind-ip.sh . ./scripts/init-server-http-bind-ip.sh init_server_http_bind_ip .env # shellcheck source=scripts/init-security-env.sh . ./scripts/init-security-env.sh init_security_env .env upgrade # 在更新镜像和重建服务前迁移并校验后端公开 API 地址。 # shellcheck source=scripts/init-public-api-base-url.sh . ./scripts/init-public-api-base-url.sh init_public_api_base_url .env upgrade "${DEPLOY_PUBLIC_API_BASE_URL:-}" # 旧部署可能缺少浏览器 Origin;在重建主服务前从已校验的公开 API 地址补齐。 # shellcheck source=scripts/init-security-origin.sh . ./scripts/init-security-origin.sh init_security_origin .env echo "===========================" echo "🚀 开始自动安装 Docker 和 Docker Compose" echo "===========================" # 检查 Docker 是否已安装 if command -v docker &> /dev/null; then echo "✅ Docker 已安装,跳过安装步骤" else # 原有的 Docker 安装步骤 echo "📦 安装依赖..." sudo apt update -y sudo apt install -y apt-transport-https ca-certificates curl software-properties-common gnupg lsb-release echo "🔑 添加 Docker GPG 密钥..." curl -fsSL https://mirrors.nwafu.edu.cn/docker-ce/linux/ubuntu/gpg | sudo apt-key add - echo "🌍 添加 Docker 源..." sudo add-apt-repository -y "deb [arch=amd64] https://mirrors.nwafu.edu.cn生成注释ker-ce/linux/ubuntu/ $(lsb_release -cs) stable" sudo apt-get update -y echo "🐳 安装 Docker..." sudo apt-get install -y docker.io echo "✅ 启动并设置 Docker 开机自启..." sudo systemctl enable docker sudo systemctl start docker fi # 检测并使用正确的 Docker Compose 命令 # 优先使用新版本的 docker compose(作为 docker 的子命令) if docker compose version &> /dev/null; then DOCKER_COMPOSE_CMD="docker compose" echo "✅ 检测到 Docker Compose (新版本: docker compose)" elif command -v docker-compose &> /dev/null; then DOCKER_COMPOSE_CMD="docker-compose" echo "✅ 检测到 Docker Compose (旧版本: docker-compose)" else echo "⚙️ 安装 Docker Compose..." COMPOSE_BIN_URL="https://static.51easyai.com/docker-compose-linux-x86_64" TMP_COMPOSE_BIN="/tmp/docker-compose-linux-x86_64" curl -fL "$COMPOSE_BIN_URL" -o "$TMP_COMPOSE_BIN" sudo install -m 0755 "$TMP_COMPOSE_BIN" /usr/bin/docker-compose rm -f "$TMP_COMPOSE_BIN" DOCKER_COMPOSE_CMD="docker-compose" echo "✅ Docker Compose 安装完成" fi echo "📌 Docker 运行状态:" sudo systemctl status docker --no-pager || true echo "📌 Docker Compose 版本:" $DOCKER_COMPOSE_CMD version echo "🎉 Docker 和 Docker Compose 已就绪!" wait_for_core_services() { local timeout_seconds="${UPDATE_HEALTH_TIMEOUT_SECONDS:-300}" local deadline=$((SECONDS + timeout_seconds)) local services=(easyai-server ws-gateway easyai-web) local service container_id state running health all_ready if ! [[ "$timeout_seconds" =~ ^[1-9][0-9]*$ ]]; then echo "❌ UPDATE_HEALTH_TIMEOUT_SECONDS 必须是正整数,当前为: $timeout_seconds" return 1 fi echo "⏳ 正在等待核心服务恢复可用(最长 ${timeout_seconds} 秒)..." while (( SECONDS < deadline )); do all_ready=true for service in "${services[@]}"; do container_id="$(sudo $DOCKER_COMPOSE_CMD ps -q "$service" 2>/dev/null || true)" if [ -z "$container_id" ]; then all_ready=false continue fi state="$(sudo docker inspect --format '{{.State.Running}}|{{if .State.Health}}{{.State.Health.Status}}{{else}}none{{end}}' "$container_id" 2>/dev/null || true)" running="${state%%|*}" health="${state#*|}" if [ "$running" != "true" ] || { [ "$health" != "none" ] && [ "$health" != "healthy" ]; }; then all_ready=false fi done if [ "$all_ready" = true ]; then echo "✅ 核心服务已恢复可用" return 0 fi sleep 5 done echo "❌ 更新后核心服务未在 ${timeout_seconds} 秒内恢复可用" echo " 当前状态:" sudo $DOCKER_COMPOSE_CMD ps easyai-server ws-gateway easyai-web || true echo " 请运行 '$DOCKER_COMPOSE_CMD logs --tail=100 easyai-server ws-gateway easyai-web' 查看原因" return 1 } # 旧 Redis 若仍把 AOF/RDB 留在容器写层,直接 up 会丢失数据。 sudo ./scripts/check-redis-persistence.sh echo "🚀 重新启动EasyAI" echo "📦 当前磁盘空间:" df -h "$SCRIPT_DIR" | tail -n 1 if ! sudo $DOCKER_COMPOSE_CMD pull; then echo "❌ 镜像拉取失败,EasyAI 未被标记为更新成功" echo " 请检查网络、镜像仓库权限和服务器磁盘空间后重试" exit 1 fi if ! sudo $DOCKER_COMPOSE_CMD up -d; then echo "❌ 容器启动失败,EasyAI 未被标记为更新成功" exit 1 fi if ! wait_for_core_services; then exit 1 fi echo "🎉 EasyAI 应用更新成功"