#!/usr/bin/env bash # ============================================ # YouduWiki 一键部署脚本 # 用法: ./deploy.sh [选项] # # 选项: # --build 强制重新构建所有镜像 # --skip-build 跳过镜像构建 (使用已有镜像) # --pull 拉取最新的基础镜像 # --clean 停止并清理所有容器和数据 (危险!) # --stop 停止所有服务 # --restart 重启所有服务 # --logs 查看所有服务日志 # --status 查看服务运行状态 # --help 显示帮助信息 # ============================================ set -e SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" cd "$SCRIPT_DIR" ENV_FILE=".env" COMPOSE_FILE="docker-compose.yml" ADMIN_PORT="${ADMIN_PORT:-2443}" APP_PORT="${APP_PORT:-3010}" API_PORT="${API_PORT:-8000}" # 颜色 RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' CYAN='\033[0;36m' MAGENTA='\033[0;35m' BOLD='\033[1m' DIM='\033[2m' NC='\033[0m' # 终端宽度 TERM_WIDTH=${COLUMNS:-80} ((TERM_WIDTH < 60)) && TERM_WIDTH=60 # ============================================ # 阶段定义 (用于进度追踪) # ============================================ # 总共几个阶段 (不含子步骤): # 1. 环境检查 # 2. 配置环境变量 # 3. 构建 api 镜像 # 4. 构建 consumer 镜像 # 5. 构建 admin 镜像 # 6. 构建 app 镜像 # 7. 启动基础设施 + 等待就绪 # 8. 启动业务服务 + 等待就绪 TOTAL_STAGES=8 CURRENT_STAGE=0 # 首次构建预估耗时 (秒) # api: ~3min, consumer: ~2min, admin: ~10min (pnpm install), app: ~8min # 基础设施: ~30s, 业务服务: ~30s EST_FIRST_BUILD=$((180 + 120 + 600 + 480 + 30 + 30)) # 缓存构建预估耗时 (秒) EST_CACHED_BUILD=$((30 + 20 + 60 + 50 + 30 + 30)) OVERALL_START=0 STAGE_START=0 IS_FIRST_BUILD=true # ============================================ # 进度条绘制 # ============================================ # 清空当前行 clear_line() { printf "\r\033[K"; } # 格式化时间 format_time() { local seconds=$1 if [ "$seconds" -lt 60 ]; then printf "%ds" "$seconds" elif [ "$seconds" -lt 3600 ]; then printf "%dm%ds" $((seconds / 60)) $((seconds % 60)) else printf "%dh%dm" $((seconds / 3600)) $(((seconds % 3600) / 60)) fi } # 绘制进度条 # 参数: $1=当前值 $2=最大值 $3=前缀文本 $4=后缀文本 draw_progress_bar() { local current=$1 local total=${2:-100} local prefix="${3:-}" local suffix="${4:-}" (( total < 1 )) && total=1 (( current > total )) && current=$total local pct=$((current * 100 / total)) # 计算可用宽度 local bar_width=$((TERM_WIDTH - 45)) ((bar_width < 10)) && bar_width=10 local filled=$((pct * bar_width / 100)) local empty=$((bar_width - filled)) # 颜色随进度变化 local bar_color="${GREEN}" if [ "$pct" -lt 30 ]; then bar_color="${RED}"; fi if [ "$pct" -ge 30 ] && [ "$pct" -lt 70 ]; then bar_color="${YELLOW}"; fi # 已填充的块 local filled_bar="" local i for ((i = 0; i < filled; i++)); do filled_bar+="█"; done for ((i = 0; i < empty; i++)); do filled_bar+="░"; done printf "\r\033[K ${DIM}%s${NC} ${bar_color}%s${NC} ${BOLD}%3d%%${NC} ${DIM}%s${NC}" \ "$prefix" "$filled_bar" "$pct" "$suffix" } # ============================================ # 旋转动画 (用于不确定进度的步骤) # ============================================ SPINNER_CHARS="⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏" SPINNER_PID="" SPINNER_ACTIVE=false start_spinner() { local message="$1" SPINNER_ACTIVE=true ( local i=0 local elapsed=0 while $SPINNER_ACTIVE; do local c="${SPINNER_CHARS:$((i % 10)):1}" local elapsed_str elapsed_str=$(format_time $elapsed) # 估算剩余时间 local suffix="已运行 ${elapsed_str}" if [ -n "${STAGE_EST_SEC:-}" ] && [ "$STAGE_EST_SEC" -gt 0 ]; then local remaining=$((STAGE_EST_SEC - elapsed)) ((remaining < 0)) && remaining=0 suffix="已运行 ${elapsed_str} | 预计剩余 $(format_time $remaining)" fi printf "\r\033[K ${CYAN}%s${NC} ${DIM}%s${NC} %s" "$c" "$message" "$suffix" sleep 0.1 elapsed=$((elapsed + 1)) i=$((i + 1)) done ) & SPINNER_PID=$! } stop_spinner() { SPINNER_ACTIVE=false [ -n "$SPINNER_PID" ] && wait "$SPINNER_PID" 2>/dev/null SPINNER_PID="" } # ============================================ # 阶段计数器 # ============================================ next_stage() { CURRENT_STAGE=$((CURRENT_STAGE + 1)) STAGE_START=$(date +%s) } # 打印阶段标题 stage_header() { local stage_name="$1" local est_sec="${2:-0}" STAGE_EST_SEC=$est_sec local elapsed_total=0 if [ "$OVERALL_START" -gt 0 ]; then elapsed_total=$(($(date +%s) - OVERALL_START)) fi echo "" clear_line printf " ${BOLD}${BLUE}[%d/%d]${NC} ${BOLD}%s${NC}" "$CURRENT_STAGE" "$TOTAL_STAGES" "$stage_name" if [ "$est_sec" -gt 0 ]; then printf " ${DIM}(预计 %s)${NC}" "$(format_time $est_sec)" fi if [ "$elapsed_total" -gt 0 ]; then printf " ${DIM}总耗时: %s${NC}" "$(format_time $elapsed_total)" fi echo "" } # ============================================ # 带进度的等待函数 # ============================================ # 等待某个条件成立,显示进度条 # $1: 等待描述 $2: 最大等待秒数 $3: 检查命令 wait_with_progress() { local desc="$1" local max_wait="${2:-60}" local check_cmd="$3" local waited=0 while [ $waited -lt $max_wait ]; do if eval "$check_cmd" &>/dev/null; then draw_progress_bar "$max_wait" "$max_wait" "$desc" "就绪 ✓" echo "" return 0 fi draw_progress_bar "$waited" "$max_wait" "$desc" "$(format_time $waited)" sleep 2 waited=$((waited + 2)) done draw_progress_bar "$max_wait" "$max_wait" "$desc" "超时 ✗" echo "" return 1 } # ============================================ # 构建单个镜像 (带旋转动画) # ============================================ build_single_image() { local service="$1" local label="$2" local est_sec="${3:-120}" next_stage stage_header "$label" "$est_sec" # 创建临时文件捕获构建日志 local logfile logfile=$(mktemp /tmp/youdu-wiki-build-XXXXXX.log) # 在后台构建 docker compose -f "$COMPOSE_FILE" build --progress=plain "$service" >"$logfile" 2>&1 & local build_pid=$! # 显示旋转动画,监控构建日志中的关键行 start_spinner "构建中 (可另开终端查看: tail -f $logfile)" local last_summary="" while kill -0 "$build_pid" 2>/dev/null; do # 尝试从日志中提取当前步骤 local current_step current_step=$(grep -E "^\s*#\d+|^\s*\[.*\]|^#\d+.*RUN|DONE|CACHED|ERROR" "$logfile" 2>/dev/null | tail -1 || true) if [ -n "$current_step" ] && [ "$current_step" != "$last_summary" ]; then last_summary="$current_step" # 清理 spinner 行,打印步骤信息 printf "\r\033[K ${DIM}→ %s${NC}\n" "$(echo "$current_step" | sed 's/^[[:space:]]*//' | cut -c1-80)" start_spinner "构建中" fi sleep 1 done stop_spinner # 检查构建结果 wait "$build_pid" local exit_code=$? local elapsed=$(( $(date +%s) - STAGE_START )) if [ $exit_code -eq 0 ]; then clear_line printf " ${GREEN}✓${NC} %s ${DIM}(耗时 %s)${NC}\n" "$label 构建成功" "$(format_time $elapsed)" else clear_line printf " ${RED}✗${NC} %s ${DIM}(耗时 %s)${NC}\n" "$label 构建失败" "$(format_time $elapsed)" echo "" echo -e " ${RED}构建日志 (最后 30 行):${NC}" echo " ─────────────────────────────────────────" tail -30 "$logfile" | while IFS= read -r line; do echo " $line"; done echo " ─────────────────────────────────────────" echo " 完整日志: $logfile" rm -f "$logfile" exit 1 fi rm -f "$logfile" } # ============================================ # 检查构建缓存状态 # ============================================ check_build_cache() { # 检查是否已有构建缓存 (通过 docker layer 检查) if docker image inspect youdu-wiki-api:latest &>/dev/null && \ docker image inspect youdu-wiki-admin:latest &>/dev/null; then IS_FIRST_BUILD=false fi } # ============================================ # 日志函数 # ============================================ log_info() { echo -e " ${GREEN}✓${NC} $1"; } log_warn() { echo -e " ${YELLOW}⚠${NC} $1"; } log_error() { echo -e " ${RED}✗${NC} $1"; } banner() { clear_line echo -e "${BLUE}" echo " ╔══════════════════════════════════════╗" echo " ║ YouduWiki 部署工具 ║" echo " ║ v3.85.0 (功能全解锁版) ║" echo " ╚══════════════════════════════════════╝" echo -e "${NC}" } # ============================================ # 1. 环境检查 # ============================================ check_prerequisites() { next_stage stage_header "环境检查" 3 local checks=() if command -v docker &>/dev/null; then checks+=("Docker: $(docker --version 2>/dev/null | cut -d' ' -f3 | tr -d ',')") else log_error "未找到 Docker,请先安装 Docker Engine 20.x+" echo " Ubuntu: sudo apt-get install docker-ce" exit 1 fi if docker compose version &>/dev/null; then checks+=("Compose: v$(docker compose version --short 2>/dev/null)") elif command -v docker-compose &>/dev/null; then checks+=("Compose: $(docker-compose --version 2>/dev/null | grep -oP '\d+\.\d+\.\d+')") else log_error "未找到 Docker Compose" exit 1 fi if [ -f "$COMPOSE_FILE" ]; then checks+=("配置文件: ${COMPOSE_FILE}") else log_error "未找到 ${COMPOSE_FILE}" exit 1 fi local available_gb available_gb=$(df -BG . 2>/dev/null | awk 'NR==2 {print $4}' | sed 's/G//') if [ -n "$available_gb" ]; then if [ "${available_gb:-0}" -lt 10 ]; then checks+=("磁盘空间: ${available_gb}GB (偏小, 建议≥10GB)") else checks+=("磁盘空间: ${available_gb}GB") fi fi local mem_mb mem_mb=$(free -m 2>/dev/null | awk '/^Mem:/{print $2}') if [ -n "$mem_mb" ]; then checks+=("内存: $((mem_mb / 1024))GB") fi for check in "${checks[@]}"; do log_info "$check" done clear_line printf " ${GREEN}✓${NC} 环境检查通过 ${DIM}(耗时 %s)${NC}\n" "$(format_time $(($(date +%s) - STAGE_START)))" } # ============================================ # 2. 环境变量 # ============================================ setup_env() { next_stage stage_header "配置环境变量" 2 if [ -f "$ENV_FILE" ]; then set -a; source "$ENV_FILE"; set +a log_info "已加载 ${ENV_FILE}" return fi local pg_pass; pg_pass=$(openssl rand -base64 16 2>/dev/null || head -c 16 /dev/urandom | base64 | tr -d '\n') local redis_pass; redis_pass=$(openssl rand -base64 16 2>/dev/null || head -c 16 /dev/urandom | base64 | tr -d '\n') local nats_pass; nats_pass=$(openssl rand -base64 16 2>/dev/null || head -c 16 /dev/urandom | base64 | tr -d '\n') local s3_pass; s3_pass=$(openssl rand -base64 16 2>/dev/null || head -c 16 /dev/urandom | base64 | tr -d '\n') local jwt_secret; jwt_secret=$(openssl rand -base64 32 2>/dev/null || head -c 32 /dev/urandom | base64 | tr -d '\n') cat > "$ENV_FILE" << EOF # YouduWiki 环境变量 - $(date '+%Y-%m-%d %H:%M:%S') POSTGRES_PASSWORD=${pg_pass} REDIS_PASSWORD=${redis_pass} NATS_USER=youdu-wiki NATS_PASSWORD=${nats_pass} S3_ACCESS_KEY=s3youdu-wiki S3_SECRET_KEY=${s3_pass} JWT_SECRET=${jwt_secret} ADMIN_PASSWORD=admin123 LOG_LEVEL=0 ADMIN_PORT=2443 APP_PORT=3010 API_PORT=8000 EOF chmod 600 "$ENV_FILE" set -a; source "$ENV_FILE"; set +a log_info "已生成随机密码并保存至 ${ENV_FILE}" } # ============================================ # 3-6. 构建镜像 # ============================================ build_images() { # 检查是否有缓存 → 决定预估时间 check_build_cache local est if [ "$IS_FIRST_BUILD" = true ]; then est=$((EST_FIRST_BUILD - 30 - 30)) # 仅构建部分 else est=$((EST_CACHED_BUILD - 30 - 30)) fi echo "" clear_line if [ "$IS_FIRST_BUILD" = true ]; then printf " ${YELLOW}首次构建${NC} ${DIM}需要下载依赖包, 预计 %s${NC}\n" "$(format_time $est)" else printf " ${GREEN}检测到已有镜像${NC} ${DIM}增量构建, 预计 %s${NC}\n" "$(format_time $est)" fi # api build_single_image "api" "构建后端 API 镜像 (Go 编译)" 180 # consumer build_single_image "consumer" "构建后端 Consumer 镜像 (Go 编译)" 120 # admin (最慢: pnpm install 几百个包) build_single_image "admin" "构建管理后台镜像 (React + pnpm)" 600 # app build_single_image "app" "构建 Wiki 前端镜像 (Next.js + pnpm)" 480 echo "" clear_line printf " ${GREEN}✓${NC} 全部镜像构建完成\n" # 显示镜像列表 echo "" docker images --format "table {{.Repository}}\t{{.Tag}}\t{{.Size}}" 2>/dev/null \ | grep -E "youdu-wiki|REPOSITORY" \ | while IFS= read -r line; do echo " $line"; done } # ============================================ # 拉取基础镜像 (带并行进度) # ============================================ pull_base_images() { next_stage stage_header "拉取基础镜像" 60 local pulls=() docker pull postgres:16-alpine & pulls+=($!) docker pull redis:7-alpine & pulls+=($!) docker pull nats:2-alpine & pulls+=($!) docker pull minio/minio:latest & pulls+=($!) start_spinner "并行拉取 postgres, redis, nats, minio ..." for pid in "${pulls[@]}"; do wait "$pid" 2>/dev/null; done stop_spinner clear_line printf " ${GREEN}✓${NC} 基础镜像拉取完成 ${DIM}(耗时 %s)${NC}\n" \ "$(format_time $(($(date +%s) - STAGE_START)))" } # ============================================ # 7. 启动基础设施 # ============================================ start_infra() { next_stage stage_header "启动基础设施 (PostgreSQL, Redis, NATS, MinIO)" 60 # 启动基础服务 start_spinner "启动容器 ..." docker compose -f "$COMPOSE_FILE" up -d postgres redis nats minio >/dev/null 2>&1 stop_spinner log_info "容器已创建" # 等待 PostgreSQL 就绪 (带进度条) echo "" if wait_with_progress "PostgreSQL 启动" 60 \ "docker compose -f $COMPOSE_FILE ps postgres 2>/dev/null | grep -q healthy"; then log_info "PostgreSQL 就绪" else log_error "PostgreSQL 启动超时" echo "" echo " 排查: docker compose logs postgres" exit 1 fi # 等待 Redis 就绪 if wait_with_progress "Redis 启动 " 30 \ "docker compose -f $COMPOSE_FILE exec -T redis redis-cli -a ${REDIS_PASSWORD:-ChangeMe123!} ping 2>/dev/null | grep -q PONG"; then log_info "Redis 就绪" else log_warn "Redis 健康检查未通过,继续..." fi local elapsed=$(( $(date +%s) - STAGE_START )) clear_line printf " ${GREEN}✓${NC} 基础设施就绪 ${DIM}(耗时 %s)${NC}\n" "$(format_time $elapsed)" } # ============================================ # 8. 启动业务服务 # ============================================ start_app() { next_stage stage_header "启动业务服务 (API, Consumer, Admin, App)" 60 # 启动业务容器 start_spinner "启动容器 ..." docker compose -f "$COMPOSE_FILE" up -d api consumer admin app >/dev/null 2>&1 stop_spinner log_info "容器已创建" # 等待 API (数据库迁移 + 启动) echo "" if wait_with_progress "API 服务启动 (含数据库迁移)" 45 \ "curl -sf http://localhost:${API_PORT}/api/v1/health 2>/dev/null"; then log_info "API 服务就绪" else log_error "API 服务启动超时" echo "" echo " 排查: docker compose logs api | tail -30" exit 1 fi local elapsed=$(( $(date +%s) - STAGE_START )) clear_line printf " ${GREEN}✓${NC} 业务服务就绪 ${DIM}(耗时 %s)${NC}\n" "$(format_time $elapsed)" } # ============================================ # 部署完成信息 # ============================================ show_info() { local total_elapsed total_elapsed=$(($(date +%s) - OVERALL_START)) local server_ip if command -v hostname &>/dev/null; then server_ip=$(hostname -I 2>/dev/null | awk '{print $1}') fi [ -z "$server_ip" ] && server_ip="服务器IP" echo "" echo -e " ${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" echo -e " ${BOLD}${GREEN} ✓ YouduWiki 部署成功!${NC} ${DIM}总耗时: $(format_time $total_elapsed)${NC}" echo -e " ${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" echo "" echo -e " ${BOLD}访问地址:${NC}" echo -e " ┌─────────────────────────────────────────────────┐" printf " │ 管理后台: %-37s │\n" "http://${server_ip}:${ADMIN_PORT}" printf " │ Wiki 前端: %-37s │\n" "http://${server_ip}:${APP_PORT}" printf " │ API 服务: %-37s │\n" "http://${server_ip}:${API_PORT}" printf " │ MinIO 控制台: %-35s │\n" "http://${server_ip}:9001" echo -e " └─────────────────────────────────────────────────┘" echo "" echo -e " ${BOLD}登录凭据:${NC}" echo -e " ┌─────────────────────────────────────────────────┐" printf " │ 用户名: %-38s │\n" "admin" printf " │ 密码: %-38s │\n" "${ADMIN_PASSWORD:-admin123}" echo -e " │ ${YELLOW}(首次登录后请在管理后台修改密码)${NC} │" echo -e " └─────────────────────────────────────────────────┘" echo "" echo -e " ${BOLD}${YELLOW}⚡ 下一步:${NC}" echo -e " 1. 访问管理后台 → 登录" echo -e " 2. 配置 AI 大模型 (设置 → 模型管理)" echo -e " 3. 创建知识库 → 上传文档 → 等待学习" echo -e " 4. 访问 Wiki 前端体验 AI 问答" echo "" echo -e " ${BOLD}常用命令:${NC}" echo -e " ./deploy.sh --status 查看运行状态" echo -e " ./deploy.sh --logs 查看实时日志" echo -e " ./deploy.sh --restart 重启全部服务" echo -e " ./deploy.sh --stop 停止全部服务" echo "" } # ============================================ # 辅助命令 # ============================================ stop_services() { echo -e "\n ${YELLOW}停止所有服务...${NC}" docker compose -f "$COMPOSE_FILE" down 2>/dev/null echo -e " ${GREEN}✓${NC} 已停止\n" } restart_services() { echo -e "\n ${YELLOW}重启所有服务...${NC}" docker compose -f "$COMPOSE_FILE" restart 2>/dev/null echo -e " ${GREEN}✓${NC} 已重启\n" } show_logs() { echo -e "\n ${DIM}实时日志 (Ctrl+C 退出)${NC}\n" docker compose -f "$COMPOSE_FILE" logs -f --tail=50 } show_status() { echo -e "\n ${BOLD}服务运行状态${NC}\n" docker compose -f "$COMPOSE_FILE" ps 2>/dev/null echo "" if curl -sf http://localhost:${API_PORT}/api/v1/health &>/dev/null; then echo -e " API 健康检查: ${GREEN}正常${NC}" else echo -e " API 健康检查: ${RED}异常${NC}" fi echo "" echo " 容器资源占用:" docker stats --no-stream \ --format "table {{.Name}}\t{{.CPUPerc}}\t{{.MemUsage}}" \ $(docker compose -f "$COMPOSE_FILE" ps -q) 2>/dev/null || true } clean_all() { echo -e "\n ${RED}${BOLD}⚠ 危险操作: 将删除所有容器、数据卷和镜像${NC}" echo -e " ${RED}数据库中的所有数据将永久丢失!${NC}\n" read -rp " 输入 DELETE 确认: " confirm if [ "$confirm" != "DELETE" ]; then echo -e " ${GREEN}✓${NC} 已取消\n"; exit 0 fi echo -e " ${YELLOW}清理中...${NC}" docker compose -f "$COMPOSE_FILE" down -v --remove-orphans 2>/dev/null docker images --format "{{.Repository}}:{{.Tag}}" 2>/dev/null \ | grep "youdu-wiki" \ | xargs -r docker rmi 2>/dev/null || true echo -e " ${GREEN}✓${NC} 清理完成\n" } # ============================================ # 主流程 # ============================================ main() { local DO_BUILD=true local DO_PULL=false while [[ $# -gt 0 ]]; do case "$1" in --build) DO_BUILD=true; shift ;; --skip-build) DO_BUILD=false; shift ;; --pull) DO_PULL=true; shift ;; --clean) banner; clean_all; exit 0 ;; --stop) banner; stop_services; exit 0 ;; --restart) banner; restart_services; show_info; exit 0 ;; --logs) show_logs; exit 0 ;; --status) banner; show_status; exit 0 ;; --help|-h) banner echo " 用法: $0 [选项]" echo "" echo " 选项:" echo " --build 强制重新构建所有镜像 (默认)" echo " --skip-build 跳过镜像构建步骤" echo " --pull 拉取最新的基础镜像" echo " --clean 停止并清理所有容器和数据 (危险!)" echo " --stop 停止所有服务" echo " --restart 重启所有服务" echo " --logs 查看所有服务日志" echo " --status 查看服务运行状态" echo " --help 显示此帮助信息" echo "" echo " 示例:" echo " $0 # 完整部署 (构建 + 启动)" echo " $0 --skip-build # 跳过构建,直接启动" echo " $0 --restart # 重启已部署的服务" exit 0 ;; *) log_error "未知选项: $1"; echo " 使用 --help 查看帮助"; exit 1 ;; esac done banner # 检测已有运行中的服务 if docker compose -f "$COMPOSE_FILE" ps --services --filter "status=running" 2>/dev/null | grep -q .; then echo -e " ${YELLOW}⚠ 检测到已有服务在运行${NC}" read -rp " 是否重新部署? [y/N]: " redeploy if [ "$redeploy" != "y" ] && [ "$redeploy" != "Y" ]; then echo -e " ${GREEN}✓${NC} 已取消\n"; exit 0 fi docker compose -f "$COMPOSE_FILE" down 2>/dev/null fi OVERALL_START=$(date +%s) # 如果跳过构建,调整总阶段数 if [ "$DO_BUILD" = false ]; then TOTAL_STAGES=4 fi # ─── 执行部署阶段 ─── check_prerequisites setup_env if [ "$DO_PULL" = true ]; then pull_base_images fi if [ "$DO_BUILD" = true ]; then build_images else next_stage stage_header "跳过镜像构建" 1 local missing=() for img in youdu-wiki-api youdu-wiki-consumer youdu-wiki-admin youdu-wiki-app; do docker image inspect "${img}:latest" &>/dev/null || missing+=("$img") done if [ ${#missing[@]} -gt 0 ]; then log_error "缺少镜像: ${missing[*]}" echo " 请先运行 ./deploy.sh (不带 --skip-build) 构建镜像" exit 1 fi log_info "所有镜像已存在" fi start_infra start_app show_info } main "$@"