Prechádzať zdrojové kódy

feat(deploy): 拆分 base/app 镜像并支持代理加速构建

- 新增 Dockerfile.base:预装系统依赖和全部 pip 包,仅在 requirements.txt 变化时重建
- 修改 Dockerfile:继承 lq-agent-base,仅复制源码,日常构建秒级完成
- 新增 deploy_agent.sh:
  - 部署前检测 base 镜像,按需重建
  - 交互式询问是否使用代理(默认不走代理)
  - 支持通过 --network host + build-arg 使用宿主机 7890 代理
WangXuMing 1 mesiac pred
rodič
commit
ceb5cb06bb
3 zmenil súbory, kde vykonal 516 pridanie a 64 odobranie
  1. 14 64
      Dockerfile
  2. 78 0
      Dockerfile.base
  3. 424 0
      deploy_agent.sh

+ 14 - 64
Dockerfile

@@ -1,72 +1,22 @@
 # syntax=docker/dockerfile:1
-FROM python:3.12-slim
+# =============================================================================
+# Dockerfile — 应用镜像,基于预装依赖的 base 镜像,仅复制源码
+# 日常部署构建耗时:秒级
+#
+# 前置条件:先构建 base 镜像
+#   docker build -f Dockerfile.base -t lq-agent-base:latest .
+#
+# 日常构建:
+#   docker build -t lq_agent_platform_server_dev:vX.XX .
+# =============================================================================
+ARG BASE_IMAGE=lq-agent-base:latest
+FROM ${BASE_IMAGE}
 
-# 替换为阿里云 apt 源(Debian 12 使用 DEB822 格式)
-RUN sed -i 's|deb.debian.org|mirrors.aliyun.com|g' /etc/apt/sources.list.d/debian.sources && \
-    sed -i 's|security.debian.org|mirrors.aliyun.com|g' /etc/apt/sources.list.d/debian.sources
-
-# 安装 OpenCV 系统依赖及 LibreOffice(docx/doc 转 PDF)
-# 使用 cache mount 缓存 apt 包,避免每次重新下载
-RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
-    --mount=type=cache,target=/var/lib/apt,sharing=locked \
-    apt-get update && apt-get install -y \
-    # OpenCV 核心依赖
-    libgl1 \
-    libglib2.0-0 \
-    libsm6 \
-    libxext6 \
-    libxrender1 \
-    # X11 库(OpenCV 需要)
-    libxcb1 \
-    libxcb-shm0 \
-    libxcb-icccm4 \
-    libxcb-image0 \
-    libxcb-keysyms1 \
-    libxcb-randr0 \
-    libxcb-render-util0 \
-    libxcb-render0 \
-    libxcb-shape0 \
-    libxcb-sync1 \
-    libxcb-xfixes0 \
-    libxcb-xinerama0 \
-    libxcb-xkb1 \
-    libxkbcommon-x11-0 \
-    # 其他可能需要的库
-    libfontconfig1 \
-    libfreetype6 \
-    # LibreOffice(用于 docx/doc 转 PDF)
-    libreoffice-writer \
-    libreoffice-core \
-    # 中文字体(PDF 转换中文支持)
-    fonts-wqy-zenhei \
-    --no-install-recommends
-
-ENV DEBIAN_FRONTEND=noninteractive \
-    TZ=Asia/Shanghai
-
-# 安装系统依赖包并创建虚拟环境
-RUN chmod 777 /tmp \
-    && python -m venv /venv
-
-ENV PATH="/venv/bin:$PATH"
-
-# 先复制 requirements 文件安装依赖(利用缓存)
-COPY requirements.txt /tmp/
-# 使用 cache mount 缓存 pip 包,避免大依赖(torch/scipy 等)每次重新下载
-RUN --mount=type=cache,target=/root/.cache/pip,sharing=locked \
-    /venv/bin/pip config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple \
-    && /venv/bin/pip config set install.trusted-host pypi.tuna.tsinghua.edu.cn \
-    && /venv/bin/pip --default-timeout=1800 install -r /tmp/requirements.txt
-
-# 设置工作目录并复制项目文件
 WORKDIR /app
 COPY . /app
 
 EXPOSE 8001
-# 确保脚本可执行
 RUN chmod 777 run.sh
 
-# 使用虚拟环境运行脚本
-#CMD ["/venv/bin/gunicorn", "-c", "gunicorn_config.py", "server.app:app"]
-# 带参数的启动方式
-CMD ["/venv/bin/python", "server/app.py", "--host", "0.0.0.0", "--port", "8001"]
+# 使用虚拟环境运行(venv 已在 base 镜像中创建并设入 PATH)
+CMD ["/venv/bin/python", "server/app.py", "--host", "0.0.0.0", "--port", "8001"]

+ 78 - 0
Dockerfile.base

@@ -0,0 +1,78 @@
+# syntax=docker/dockerfile:1
+# =============================================================================
+# Dockerfile.base — 预装所有系统依赖和 Python 包的 base 镜像
+# 仅在 requirements.txt 变化时重建,日常部署无需重复安装依赖
+#
+# 构建:docker build -f Dockerfile.base -t lq-agent-base:latest .
+# 更新依赖后重建:docker build -f Dockerfile.base -t lq-agent-base:latest --no-cache .
+# =============================================================================
+FROM python:3.12-slim
+
+# 接收宿主机传入的代理配置(BuildKit 需显式声明 ARG 才能传递到 RUN 环境)
+ARG HTTP_PROXY
+ARG HTTPS_PROXY
+ARG http_proxy
+ARG https_proxy
+ARG NO_PROXY
+ARG no_proxy
+ENV HTTP_PROXY=${HTTP_PROXY} \
+    HTTPS_PROXY=${HTTPS_PROXY} \
+    http_proxy=${http_proxy} \
+    https_proxy=${https_proxy} \
+    NO_PROXY=${NO_PROXY} \
+    no_proxy=${no_proxy}
+
+# 替换为阿里云 apt 源(Debian 12 使用 DEB822 格式)
+RUN sed -i 's|deb.debian.org|mirrors.aliyun.com|g' /etc/apt/sources.list.d/debian.sources && \
+    sed -i 's|security.debian.org|mirrors.aliyun.com|g' /etc/apt/sources.list.d/debian.sources
+
+# 安装 OpenCV 系统依赖及 LibreOffice(docx/doc 转 PDF)
+# cache mount 避免每次重新下载 deb 包
+RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
+    --mount=type=cache,target=/var/lib/apt,sharing=locked \
+    apt-get update && apt-get install -y \
+    # OpenCV 核心依赖
+    libgl1 \
+    libglib2.0-0 \
+    libsm6 \
+    libxext6 \
+    libxrender1 \
+    # X11 库(OpenCV 需要)
+    libxcb1 \
+    libxcb-shm0 \
+    libxcb-icccm4 \
+    libxcb-image0 \
+    libxcb-keysyms1 \
+    libxcb-randr0 \
+    libxcb-render-util0 \
+    libxcb-render0 \
+    libxcb-shape0 \
+    libxcb-sync1 \
+    libxcb-xfixes0 \
+    libxcb-xinerama0 \
+    libxcb-xkb1 \
+    libxkbcommon-x11-0 \
+    # 其他可能需要的库
+    libfontconfig1 \
+    libfreetype6 \
+    # LibreOffice(用于 docx/doc 转 PDF)
+    libreoffice-writer \
+    libreoffice-core \
+    # 中文字体(PDF 转换中文支持)
+    fonts-wqy-zenhei \
+    --no-install-recommends
+
+ENV DEBIAN_FRONTEND=noninteractive \
+    TZ=Asia/Shanghai
+
+# 创建虚拟环境并设入 PATH
+RUN chmod 777 /tmp \
+    && python -m venv /venv
+ENV PATH="/venv/bin:$PATH"
+
+# 安装 Python 依赖(cache mount 缓存 pip 下载,requirements.txt 不变时仅首次下载)
+COPY requirements.txt /tmp/
+RUN --mount=type=cache,target=/root/.cache/pip,sharing=locked \
+    /venv/bin/pip config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple \
+    && /venv/bin/pip config set install.trusted-host pypi.tuna.tsinghua.edu.cn \
+    && /venv/bin/pip --default-timeout=1800 install -r /tmp/requirements.txt

+ 424 - 0
deploy_agent.sh

@@ -0,0 +1,424 @@
+#!/bin/bash
+
+#!/bin/bash
+
+# ============ 强制要求 Bash 执行 ============
+if [ -z "$BASH_VERSION" ]; then
+    echo "****************************************************************"
+    echo "* 错误:此脚本必须使用 bash 执行,不支持 sh/dash!"
+    echo "* 请使用以下任一方式运行:"
+    echo "*   1. 赋予执行权限后: ./deploy_agent.sh"
+    echo "*   2. 显式指定 bash:  bash deploy_agent.sh"
+    echo "****************************************************************"
+    exit 1
+fi
+# ==========================================
+
+# ================= 配置区域 =================
+# 源代码路径
+SOURCE_DIR="/home/lq/lq_workspace/LQAgentServer/source/LQAgentPlatform"
+# Docker Compose 运行路径
+DOCKER_APP_DIR="/home/lq/lq_workspace/LQAgentServer/app/docker"
+# 配置文件名称
+COMPOSE_FILE="docker-compose.yml"
+# 镜像名称 (Repository)
+IMAGE_NAME="lq_agent_platform_server_dev"
+# Git 凭证
+GIT_USER="WangXuMing"
+GIT_PASS="123456"
+# 代理配置(用于加速依赖下载)
+PROXY_HOST="127.0.0.1"    # 宿主机代理地址,配合 --network host 使用
+PROXY_PORT="7890"         # 代理端口
+
+# ================= 辅助函数 =================
+# 打印带时间戳的日志
+log_info() {
+    echo -e "\033[32m[INFO] $(date '+%Y-%m-%d %H:%M:%S') - $1\033[0m"
+}
+
+log_error() {
+    echo -e "\033[31m[ERROR] $(date '+%Y-%m-%d %H:%M:%S') - $1\033[0m"
+}
+
+log_warn() {
+    echo -e "\033[33m[WARN] $(date '+%Y-%m-%d %H:%M:%S') - $1\033[0m"
+}
+
+# 检查命令执行状态,如果失败则退出
+check_status() {
+    if [ $? -ne 0 ]; then
+        log_error "$1 执行失败,脚本终止。"
+        exit 1
+    fi
+}
+
+# 询问是否使用代理(默认不使用)
+ask_use_proxy() {
+    USE_PROXY=false
+    PROXY_URL=""
+
+    # 非交互终端直接跳过
+    if [ ! -t 0 ]; then
+        log_info "非交互终端,默认不使用代理"
+        return
+    fi
+
+    echo ""
+    echo "============================================"
+    echo "  是否使用代理加速依赖下载?"
+    echo "  默认: 不使用代理(直接走清华镜像源)"
+    echo "============================================"
+    read -p "使用代理 [y/N]: " proxy_choice
+
+    case "$proxy_choice" in
+        [yY][eE][sS]|[yY])
+            USE_PROXY=true
+            # 确定代理宿主机地址
+            if [ -z "$PROXY_HOST" ]; then
+                PROXY_HOST=$(hostname -I | awk '{print $1}')
+            fi
+            PROXY_URL="http://${PROXY_HOST}:${PROXY_PORT}"
+            log_info "已启用代理: ${PROXY_URL}"
+            ;;
+        *)
+            log_info "不使用代理,走默认镜像源"
+            ;;
+    esac
+}
+
+# 版本号比较函数
+# 返回: 0=相等, 1=第一个大于第二个, 2=第二个大于第一个
+compare_versions() {
+    v1=$(echo "$1" | sed 's/v//')
+    v2=$(echo "$2" | sed 's/v//')
+    
+    if [ "$(echo "$v1 == $v2" | bc)" -eq 1 ]; then
+        return 0
+    elif [ "$(echo "$v1 > $v2" | bc)" -eq 1 ]; then
+        return 1
+    else
+        return 2
+    fi
+}
+
+# ================= 步骤 1: Git 拉取代码 (带重试+强制拉取) =================
+log_info "步骤 1: 进入源码目录并拉取最新代码..."
+
+if [ ! -d "$SOURCE_DIR" ]; then
+    log_error "源码目录不存在: $SOURCE_DIR"
+    exit 1
+fi
+
+# 检查目录进入权限并修复
+if [ ! -x "$SOURCE_DIR" ]; then
+    log_error "源码目录无进入权限!正在修复..."
+    sudo chmod +x "$SOURCE_DIR"
+    sudo chown -R lq:lq "$SOURCE_DIR"
+fi
+
+cd "$SOURCE_DIR" || {
+    log_error "进入源码目录失败!路径:$SOURCE_DIR"
+    log_error "可能原因:1. 目录权限不足 2. 路径含特殊字符 3. 目录被删除"
+    exit 1
+}
+check_status "进入源码目录"  # 双重保障
+
+# 检查是否为 Git 仓库
+if [ ! -d ".git" ]; then
+    log_error "当前目录不是 Git 仓库!路径:$SOURCE_DIR"
+    exit 1
+fi
+
+log_info "检查本地是否存在可能与远程冲突的已修改文件..."
+
+HAS_CONFLICT_FILES=$(git status --porcelain | grep -v "^??")
+
+if [ -n "$HAS_CONFLICT_FILES" ]; then
+    log_info "发现以下文件存在本地修改(将被远程最新代码覆盖):"
+    echo "$HAS_CONFLICT_FILES" | awk '{print "  - " $2}'
+    log_info "正在强制丢弃本地修改,确保同步远程最新代码..."
+    
+    # 强制丢弃修改
+    git checkout -- .  # 仅丢弃已跟踪文件的本地修改(冲突风险文件)
+
+    
+    log_info "本地冲突文件修改已丢弃,准备拉取远程最新代码..."
+else
+    log_info "本地无可能冲突的已修改文件,直接拉取远程最新代码..."
+fi
+
+
+# 组装 Git 认证 URL(保留原逻辑)
+ORIGIN_URL=$(git remote get-url origin 2>/dev/null)
+if [ $? -ne 0 ]; then
+    log_error "获取 Git 远程地址失败!请检查 remote 配置"
+    exit 1
+fi
+
+# 初始化认证 URL(默认使用 origin 远程)
+CLEAN_URL=${ORIGIN_URL#*://}
+AUTH_URL="http://${GIT_USER}:${GIT_PASS}@${CLEAN_URL}"
+# 定义备用远程(upstream)及认证 URL
+UPSTREAM_URL=$(git remote get-url upstream 2>/dev/null)
+if [ $? -ne 0 ]; then
+    log_warn "未配置 upstream 远程,503 时无法切换备用源"
+    UPSTREAM_AVAILABLE=0
+else
+    UPSTREAM_CLEAN_URL=${UPSTREAM_URL#*://}
+    UPSTREAM_AUTH_URL="http://${GIT_USER}:${GIT_PASS}@${UPSTREAM_CLEAN_URL}"
+    UPSTREAM_AVAILABLE=1
+fi
+
+MAX_RETRIES=3
+COUNT=0
+GIT_SUCCESS=0
+CURRENT_AUTH_URL="$AUTH_URL"  # 当前使用的认证 URL
+
+while [ $COUNT -lt $MAX_RETRIES ]; do
+    log_info "正在执行 Git Pull (第 $((COUNT+1)) 次尝试) - 强制拉取 dev 分支最新代码..."
+    log_info "当前使用远程地址:${CURRENT_AUTH_URL}"
+    
+    # 执行 git pull 并捕获错误输出
+    PULL_OUTPUT=$(git pull "$CURRENT_AUTH_URL" dev --force --allow-unrelated-histories 2>&1)
+    PULL_EXIT_CODE=$?
+
+    if [ $PULL_EXIT_CODE -eq 0 ]; then
+        # 拉取成功:输出结果并退出循环
+        GIT_SUCCESS=1
+        LATEST_COMMIT=$(git log -1 --format="%h - %s ")
+        log_info "Git Pull 成功!当前部署提交版本:$LATEST_COMMIT"
+        break
+    else
+        # 拉取失败:判断错误类型(新增 returned error: 503 匹配规则)
+        if echo "$PULL_OUTPUT" | grep -qiE "503 Service Unavailable|503 Unavailable|returned error: 503" && [ $UPSTREAM_AVAILABLE -eq 1 ]; then
+            # 错误类型:503 服务不可用 + 有备用 upstream 远程
+            log_error "Git Pull 失败:当前远程(origin)返回 503 不可达,切换到备用远程(upstream)重试..."
+            log_error "错误详情:$PULL_OUTPUT"
+            CURRENT_AUTH_URL="$UPSTREAM_AUTH_URL"  # 切换为 upstream 认证 URL
+            COUNT=$((COUNT+1))
+            sleep 3
+        elif echo "$PULL_OUTPUT" | grep -qiE "503 Service Unavailable|503 Unavailable|returned error: 503" && [ $UPSTREAM_AVAILABLE -eq 0 ]; then
+            # 错误类型:503 但无备用源
+            log_error "Git Pull 失败:远程返回 503 不可达,但未配置 upstream 备用源,无法切换..."
+            log_error "错误详情:$PULL_OUTPUT"
+            COUNT=$((COUNT+1))
+            sleep 3
+        else
+            # 其他错误(如认证失败、网络不通、分支不存在等):按原逻辑重试
+            log_error "Git Pull 失败(非 503 错误),准备重试..."
+            log_error "错误详情:$PULL_OUTPUT"
+            COUNT=$((COUNT+1))
+            sleep 3
+        fi
+    fi
+done
+
+# 所有重试失败后的处理
+if [ $GIT_SUCCESS -eq 0 ]; then
+    log_error "Git Pull 已重试 $MAX_RETRIES 次,全部失败!"
+    exit 1
+fi
+
+# ================= 步骤 2: 关闭当前容器 =================
+log_info "步骤 2: 关闭正在运行的容器..."
+
+if [ ! -d "$DOCKER_APP_DIR" ]; then
+    log_error "Docker 运行目录不存在: $DOCKER_APP_DIR"
+    exit 1
+fi
+
+cd "$DOCKER_APP_DIR"
+check_status "进入 Docker 运行目录"
+
+docker compose down
+# 即使 down 失败(例如没启动),也继续执行,只记录错误
+if [ $? -ne 0 ]; then
+    log_error "警告: Docker Compose Down 返回非零状态,尝试继续..."
+fi
+
+# ================= 步骤 3: 获取当前运行版本并计算新版本号 =================
+log_info "步骤 3: 查找当前运行版本并计算新版本号..."
+
+# 获取当前 docker-compose 中指定的镜像版本
+CURRENT_CONFIG_TAG=$(grep "image: ${IMAGE_NAME}:" "$DOCKER_APP_DIR/$COMPOSE_FILE" | sed "s|.*image: ${IMAGE_NAME}:||")
+if [ -z "$CURRENT_CONFIG_TAG" ]; then
+    CURRENT_CONFIG_TAG="v0.01"
+    log_warn "未在配置文件中找到版本号,使用默认版本: $CURRENT_CONFIG_TAG"
+else
+    log_info "当前配置文件中的版本: $CURRENT_CONFIG_TAG"
+fi
+
+# 计算新版本号
+# 提取版本号数字 (去掉 'v'),例如 v0.13 -> 0.13
+VERSION_NUM=$(echo "$CURRENT_CONFIG_TAG" | sed 's/v//')
+# 计算新版本号 (这里设置为 +0.01,即 0.13 -> 0.14)
+NEW_VERSION_NUM=$(echo "$VERSION_NUM" | awk '{printf "%.2f", $1 + 0.01}')
+NEW_TAG="v$NEW_VERSION_NUM"
+
+log_info "计算出的新版本号为: $NEW_TAG"
+
+# ================= 步骤 4: 删除上上次的镜像版本 =================
+log_info "步骤 4: 清理旧镜像(保留最新版本,删除上上次版本)..."
+
+# 获取所有历史镜像,按创建时间降序排列
+# 使用 docker images --format 获取完整信息
+HISTORY_IMAGES=$(docker images --filter "reference=${IMAGE_NAME}:*" --format "{{.Tag}} {{.ID}} {{.CreatedAt}}" | sort -r)
+
+if [ -n "$HISTORY_IMAGES" ]; then
+    # 转换为数组
+    mapfile -t IMAGE_ARRAY <<< "$HISTORY_IMAGES"
+    
+    log_info "发现 ${#IMAGE_ARRAY[@]} 个历史镜像版本:"
+    for ((i=0; i<${#IMAGE_ARRAY[@]}; i++)); do
+        TAG=$(echo "${IMAGE_ARRAY[$i]}" | awk '{print $1}')
+        IMAGE_ID=$(echo "${IMAGE_ARRAY[$i]}" | awk '{print $2}')
+        CREATED=$(echo "${IMAGE_ARRAY[$i]}" | awk '{$1=$2=""; print $0}' | sed 's/^  //')
+        
+        # 标记当前运行版本
+        if [ "$TAG" = "$CURRENT_CONFIG_TAG" ]; then
+            log_info "  [$i] $TAG - $IMAGE_ID (当前运行版本) - $CREATED"
+            CURRENT_INDEX=$i
+        else
+            log_info "  [$i] $TAG - $IMAGE_ID - $CREATED"
+        fi
+    done
+    
+    # 保留策略:保留最新的(索引0)和当前的(如果有),删除上上次的(索引1,如果存在)
+    if [ ${#IMAGE_ARRAY[@]} -gt 1 ]; then
+        # 获取第二个镜像的信息(索引1)
+        SECOND_TAG=$(echo "${IMAGE_ARRAY[1]}" | awk '{print $1}')
+        SECOND_ID=$(echo "${IMAGE_ARRAY[1]}" | awk '{print $2}')
+        
+        # 检查是否是要删除的上上次版本
+        if [ "$SECOND_TAG" != "$NEW_TAG" ] && [ "$SECOND_TAG" != "$CURRENT_CONFIG_TAG" ]; then
+            log_info "正在删除上上次版本镜像: $SECOND_TAG ($SECOND_ID)"
+            
+            # 删除镜像
+            docker rmi -f "$SECOND_ID" 2>/dev/null
+            if [ $? -eq 0 ]; then
+                log_info "成功删除上上次版本镜像: $SECOND_TAG"
+            else
+                log_warn "删除镜像 $SECOND_TAG 失败(可能已被删除或正在使用),跳过..."
+            fi
+        else
+            log_info "跳过删除 $SECOND_TAG,因为它是当前运行版本或将要构建的新版本"
+        fi
+        
+        # 如果有第三个及以后的镜像,也删除(只保留最新的2个版本)
+        for ((i=2; i<${#IMAGE_ARRAY[@]}; i++)); do
+            OLD_TAG=$(echo "${IMAGE_ARRAY[$i]}" | awk '{print $1}')
+            OLD_ID=$(echo "${IMAGE_ARRAY[$i]}" | awk '{print $2}')
+            
+            if [ "$OLD_TAG" != "$NEW_TAG" ] && [ "$OLD_TAG" != "$CURRENT_CONFIG_TAG" ]; then
+                log_info "删除更旧的镜像: $OLD_TAG ($OLD_ID)"
+                docker rmi -f "$OLD_ID" 2>/dev/null
+            fi
+        done
+    else
+        log_info "只有1个历史镜像,无需清理"
+    fi
+else
+    log_info "未找到历史镜像"
+fi
+
+# ================= 步骤 5: 构建新镜像 =================
+# 5a. 检查/构建 base 镜像(含所有系统依赖和 pip 包)
+BASE_IMAGE_NAME="lq-agent-base"
+BASE_IMAGE_TAG="latest"
+REQUIREMENTS_FILE="$SOURCE_DIR/requirements.txt"
+# 哈希文件放在 DOCKER_APP_DIR,避免被 git pull 覆盖
+BASE_HASH_FILE="$DOCKER_APP_DIR/.base_image_req_hash"
+
+NEED_REBUILD_BASE=false
+
+cd "$SOURCE_DIR"
+check_status "返回源码目录"
+
+# 询问是否使用代理(仅 base 镜像构建时需要下载依赖)
+ask_use_proxy
+
+# 检查 base 镜像是否存在
+if ! docker images --format "{{.Repository}}:{{.Tag}}" | grep -q "^${BASE_IMAGE_NAME}:${BASE_IMAGE_TAG}$"; then
+    log_info "步骤 5a: base 镜像不存在,首次构建 ${BASE_IMAGE_NAME}:${BASE_IMAGE_TAG}(包含所有依赖,仅此一次)..."
+    NEED_REBUILD_BASE=true
+else
+    # 检查 requirements.txt 是否有变化
+    CURRENT_HASH=$(md5sum "$REQUIREMENTS_FILE" | awk '{print $1}')
+    if [ -f "$BASE_HASH_FILE" ]; then
+        STORED_HASH=$(cat "$BASE_HASH_FILE")
+        if [ "$CURRENT_HASH" != "$STORED_HASH" ]; then
+            log_info "步骤 5a: requirements.txt 已变化,重建 base 镜像..."
+            NEED_REBUILD_BASE=true
+        else
+            log_info "步骤 5a: base 镜像已存在且依赖未变化,跳过重建"
+        fi
+    else
+        log_info "步骤 5a: 未找到依赖哈希记录,首次记录并确保 base 镜像一致..."
+        NEED_REBUILD_BASE=true
+    fi
+fi
+
+if [ "$NEED_REBUILD_BASE" = true ]; then
+    if [ "$USE_PROXY" = true ]; then
+        log_info "步骤 5a: 使用代理构建 base 镜像 (${PROXY_URL})..."
+        docker build -f Dockerfile.base \
+            --network host \
+            --build-arg HTTP_PROXY="${PROXY_URL}" \
+            --build-arg HTTPS_PROXY="${PROXY_URL}" \
+            --build-arg http_proxy="${PROXY_URL}" \
+            --build-arg https_proxy="${PROXY_URL}" \
+            -t "${BASE_IMAGE_NAME}:${BASE_IMAGE_TAG}" .
+    else
+        log_info "步骤 5a: 不使用代理,使用默认镜像源构建 base 镜像..."
+        docker build -f Dockerfile.base -t "${BASE_IMAGE_NAME}:${BASE_IMAGE_TAG}" .
+    fi
+    check_status "base 镜像构建"
+    # 保存当前 requirements.txt 的 md5 哈希
+    md5sum "$REQUIREMENTS_FILE" | awk '{print $1}' > "$BASE_HASH_FILE"
+    log_info "base 镜像构建成功: ${BASE_IMAGE_NAME}:${BASE_IMAGE_TAG}"
+fi
+
+# 5b. 构建应用镜像(基于 base 镜像,仅复制源码,秒级完成)
+log_info "步骤 5b: 构建应用镜像 $IMAGE_NAME:$NEW_TAG ..."
+
+docker build -t "${IMAGE_NAME}:${NEW_TAG}" .
+check_status "应用镜像构建"
+log_info "应用镜像构建成功: ${IMAGE_NAME}:${NEW_TAG}"
+
+# ================= 步骤 6: 修改 docker-compose.yml 版本号 =================
+log_info "步骤 6: 更新 docker-compose.yml 中的版本号..."
+
+cd "$DOCKER_APP_DIR"
+check_status "进入 Docker 运行目录"
+
+if [ ! -f "$COMPOSE_FILE" ]; then
+    log_error "找不到配置文件: $COMPOSE_FILE"
+    exit 1
+fi
+
+# 使用 sed 正则替换
+# 匹配: image: lq_agent_platform_server_dev:任意字符
+# 替换为: image: lq_agent_platform_server_dev:新版本号
+sed -i "s|image: ${IMAGE_NAME}:.*|image: ${IMAGE_NAME}:${NEW_TAG}|" "$COMPOSE_FILE"
+check_status "修改 docker-compose.yml"
+
+# 验证修改结果
+MATCH_LINE=$(grep "image: ${IMAGE_NAME}:" "$COMPOSE_FILE")
+log_info "配置文件已更新: $MATCH_LINE"
+
+# ================= 步骤 7: 启动容器 =================
+log_info "步骤 7: 启动 Docker Compose..."
+
+docker compose up -d
+check_status "启动容器"
+
+# ================= 步骤 8: 显示当前保留的镜像 =================
+log_info "步骤 8: 当前保留的镜像版本列表:"
+docker images --filter "reference=${IMAGE_NAME}:*" --format "table {{.Tag}}\t{{.ID}}\t{{.Size}}\t{{.CreatedAt}}"
+
+log_info "===================================================="
+log_info " 开发版部署成功!"
+log_info " 当前运行端口: 8002"
+log_info " 部署版本: $NEW_TAG"
+log_info " 保留镜像: 最新版本 + 前一个版本"
+log_info "===================================================="