diff --git a/deploy.sh b/deploy.sh index 10a427d..3880f2c 100644 --- a/deploy.sh +++ b/deploy.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash # ============================================ -# PandaWiki 一键部署脚本 +# YouduWiki 一键部署脚本 # 用法: ./deploy.sh [选项] # # 选项: @@ -17,310 +17,583 @@ 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' -NC='\033[0m' # No Color +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 + +# ============================================ +# 进度条绘制 # ============================================ -log_info() { echo -e "${GREEN}[INFO]${NC} $1"; } -log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; } -log_error() { echo -e "${RED}[ERROR]${NC} $1"; } -log_step() { echo -e "\n${CYAN}${BOLD}==>${NC} ${CYAN}$1${NC}"; } -log_title() { echo -e "\n${BLUE}${BOLD}━━━ $1 ━━━${NC}"; } +# 清空当前行 +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 " ║ PandaWiki 部署工具 ║" + echo " ║ YouduWiki 部署工具 ║" echo " ║ v3.85.0 (功能全解锁版) ║" echo " ╚══════════════════════════════════════╝" echo -e "${NC}" } # ============================================ -# 前置检查 +# 1. 环境检查 # ============================================ check_prerequisites() { - log_step "环境检查" + next_stage + stage_header "环境检查" 3 - # 检查 Docker - if ! command -v docker &> /dev/null; then + 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" - echo " 官方文档: https://docs.docker.com/engine/install/" + echo " Ubuntu: sudo apt-get install docker-ce" exit 1 fi - log_info "Docker: $(docker --version)" - # 检查 Docker Compose - if docker compose version &> /dev/null; then - log_info "Docker Compose: $(docker compose version --short)" - elif command -v docker-compose &> /dev/null; then - log_info "Docker Compose (独立版): $(docker-compose --version)" + 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" - echo " 安装: sudo apt-get install docker-compose-plugin" exit 1 fi - # 检查 docker-compose.yml - if [ ! -f "$COMPOSE_FILE" ]; then - log_error "未找到 $COMPOSE_FILE,请确保在项目根目录运行此脚本" + if [ -f "$COMPOSE_FILE" ]; then + checks+=("配置文件: ${COMPOSE_FILE}") + else + log_error "未找到 ${COMPOSE_FILE}" exit 1 fi - # 检查磁盘空间 (至少 10GB) local available_gb - available_gb=$(df -BG . | awk 'NR==2 {print $4}' | sed 's/G//') - if [ "${available_gb:-0}" -lt 10 ]; then - log_warn "磁盘可用空间不足 10GB (当前: ${available_gb}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 - log_info "环境检查通过" + 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() { - log_step "配置环境变量" + next_stage + stage_header "配置环境变量" 2 if [ -f "$ENV_FILE" ]; then - log_info "已找到 ${ENV_FILE},跳过创建" - # 加载环境变量 - set -a - source "$ENV_FILE" - set +a + set -a; source "$ENV_FILE"; set +a + log_info "已加载 ${ENV_FILE}" return fi - log_warn "未找到 ${ENV_FILE},创建默认配置..." - - # 生成随机密码 - local pg_pass=$(openssl rand -base64 16 2>/dev/null || head -c 16 /dev/urandom | base64) - local redis_pass=$(openssl rand -base64 16 2>/dev/null || head -c 16 /dev/urandom | base64) - local nats_pass=$(openssl rand -base64 16 2>/dev/null || head -c 16 /dev/urandom | base64) - local s3_pass=$(openssl rand -base64 16 2>/dev/null || head -c 16 /dev/urandom | base64) - local jwt_secret=$(openssl rand -base64 32 2>/dev/null || head -c 32 /dev/urandom | base64) + 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 -# ============================================ -# PandaWiki 环境变量配置 -# 生成时间: $(date '+%Y-%m-%d %H:%M:%S') -# ============================================ - -# 数据库 (PostgreSQL) +# YouduWiki 环境变量 - $(date '+%Y-%m-%d %H:%M:%S') POSTGRES_PASSWORD=${pg_pass} - -# 缓存 (Redis) REDIS_PASSWORD=${redis_pass} - -# 消息队列 (NATS) -NATS_USER=panda-wiki +NATS_USER=youdu-wiki NATS_PASSWORD=${nats_pass} - -# 对象存储 (MinIO / S3) -S3_ACCESS_KEY=s3panda-wiki +S3_ACCESS_KEY=s3youdu-wiki S3_SECRET_KEY=${s3_pass} - -# JWT 签名密钥 JWT_SECRET=${jwt_secret} - -# 管理员初始密码 (首次登录后请修改) ADMIN_PASSWORD=admin123 - -# RAG 向量检索服务地址 (留空使用默认) -# RAG_BASE_URL=http://your-rag-server:5050 - -# 日志级别: -4=debug, 0=info, 4=warn, 8=error LOG_LEVEL=0 - -# 端口配置 (如需修改,请确保与防火墙规则一致) ADMIN_PORT=2443 APP_PORT=3010 API_PORT=8000 EOF chmod 600 "$ENV_FILE" - log_info "已创建 ${ENV_FILE},密码已随机生成并保存" - - set -a - source "$ENV_FILE" - set +a + set -a; source "$ENV_FILE"; set +a + log_info "已生成随机密码并保存至 ${ENV_FILE}" } # ============================================ -# 构建镜像 +# 3-6. 构建镜像 # ============================================ build_images() { - log_step "构建 Docker 镜像" + # 检查是否有缓存 → 决定预估时间 + 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 - local build_start - build_start=$(date +%s) - - # 构建后端 - log_info "构建 panda-wiki-api ..." - docker compose -f "$COMPOSE_FILE" build api - - log_info "构建 panda-wiki-consumer ..." - docker compose -f "$COMPOSE_FILE" build consumer - - # 构建前端 (多阶段构建,耗时较长) - log_info "构建 panda-wiki-admin (管理后台) ..." - docker compose -f "$COMPOSE_FILE" build admin - - log_info "构建 panda-wiki-app (Wiki 用户端) ..." - docker compose -f "$COMPOSE_FILE" build app - - local build_end - build_end=$(date +%s) - local build_duration=$((build_end - build_start)) - - log_info "全部镜像构建完成 (耗时 ${build_duration} 秒)" - - # 显示构建的镜像 echo "" - docker images --format "table {{.Repository}}\t{{.Tag}}\t{{.Size}}" \ - | grep -E "panda-wiki|REPOSITORY" + 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() { - log_step "拉取基础镜像" + next_stage + stage_header "拉取基础镜像" 60 - docker pull postgres:16-alpine & - docker pull redis:7-alpine & - docker pull nats:2-alpine & - docker pull minio/minio:latest & - wait + 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+=($!) - log_info "基础镜像拉取完成" + 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_services() { - log_step "启动服务" +start_infra() { + next_stage + stage_header "启动基础设施 (PostgreSQL, Redis, NATS, MinIO)" 60 - # 先启动基础设施 - log_info "启动基础设施 (PostgreSQL, Redis, NATS, MinIO) ..." - docker compose -f "$COMPOSE_FILE" up -d postgres redis nats minio + # 启动基础服务 + start_spinner "启动容器 ..." + docker compose -f "$COMPOSE_FILE" up -d postgres redis nats minio >/dev/null 2>&1 + stop_spinner + log_info "容器已创建" - # 等待基础设施就绪 - log_info "等待基础设施就绪 ..." - local max_wait=60 - local waited=0 - - while [ $waited -lt $max_wait ]; do - if docker compose -f "$COMPOSE_FILE" ps postgres 2>/dev/null | grep -q "healthy"; then - break - fi - sleep 2 - waited=$((waited + 2)) - echo -n "." - done + # 等待 PostgreSQL 就绪 (带进度条) echo "" - - if [ $waited -ge $max_wait ]; then - log_error "基础设施启动超时,请检查: docker compose logs postgres" + 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 - log_info "基础设施就绪" + # 等待 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 - # 启动业务服务 - log_info "启动业务服务 (API, Consumer, Admin, App) ..." - docker compose -f "$COMPOSE_FILE" up -d api consumer admin app - - # 等待 API 就绪 - log_info "等待 API 服务就绪 ..." - waited=0 - while [ $waited -lt 30 ]; do - if curl -sf http://localhost:${API_PORT}/api/v1/health > /dev/null 2>&1; then - break - fi - sleep 2 - waited=$((waited + 2)) - echo -n "." - done - echo "" - - log_info "全部服务启动完成" + 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() { - log_title "部署完成" + local total_elapsed + total_elapsed=$(($(date +%s) - OVERALL_START)) - # 尝试获取服务器 IP local server_ip - if command -v hostname &> /dev/null; then + if command -v hostname &>/dev/null; then server_ip=$(hostname -I 2>/dev/null | awk '{print $1}') fi - if [ -z "$server_ip" ]; then - server_ip="服务器IP" - fi + [ -z "$server_ip" ] && server_ip="服务器IP" echo "" - echo -e " ${BOLD}PandaWiki 已成功部署${NC}" + echo -e " ${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" + echo -e " ${BOLD}${GREEN} ✓ YouduWiki 部署成功!${NC} ${DIM}总耗时: $(format_time $total_elapsed)${NC}" + echo -e " ${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" echo "" - echo -e " ${CYAN}访问地址:${NC}" - echo -e " ┌─────────────────────────────────────────────┐" - echo -e " │ 管理后台: ${GREEN}http://${server_ip}:${ADMIN_PORT}${NC}" - echo -e " │ Wiki 前端: ${GREEN}http://${server_ip}:${APP_PORT}${NC}" - echo -e " │ API 服务: ${GREEN}http://${server_ip}:${API_PORT}${NC}" - echo -e " │ MinIO 控制台: ${GREEN}http://${server_ip}:9001${NC}" - echo -e " └─────────────────────────────────────────────┘" + 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 " ${CYAN}登录信息:${NC}" - echo -e " ┌─────────────────────────────────────────────┐" - echo -e " │ 用户名: ${BOLD}admin${NC}" - echo -e " │ 密码: ${BOLD}${ADMIN_PASSWORD:-admin123}${NC} (首次登录后请修改)" - echo -e " └─────────────────────────────────────────────┘" + 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 " ${CYAN}常用命令:${NC}" - echo -e " ┌─────────────────────────────────────────────┐" - echo -e " │ 查看日志: ${BOLD}docker compose logs -f${NC}" - echo -e " │ 查看状态: ${BOLD}./deploy.sh --status${NC}" - echo -e " │ 重启服务: ${BOLD}./deploy.sh --restart${NC}" - echo -e " │ 停止服务: ${BOLD}./deploy.sh --stop${NC}" - echo -e " └─────────────────────────────────────────────┘" + echo -e " ${BOLD}${YELLOW}⚡ 下一步:${NC}" + echo -e " 1. 访问管理后台 → 登录" + echo -e " 2. 配置 AI 大模型 (设置 → 模型管理)" + echo -e " 3. 创建知识库 → 上传文档 → 等待学习" + echo -e " 4. 访问 Wiki 前端体验 AI 问答" echo "" - echo -e " ${YELLOW}下一步: 登录管理后台 → 配置 AI 模型 → 创建知识库${NC}" + 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 "" } @@ -329,64 +602,54 @@ show_info() { # ============================================ stop_services() { - log_step "停止所有服务" - docker compose -f "$COMPOSE_FILE" down - log_info "所有服务已停止" + echo -e "\n ${YELLOW}停止所有服务...${NC}" + docker compose -f "$COMPOSE_FILE" down 2>/dev/null + echo -e " ${GREEN}✓${NC} 已停止\n" } restart_services() { - log_step "重启所有服务" - docker compose -f "$COMPOSE_FILE" restart - log_info "所有服务已重启" + echo -e "\n ${YELLOW}重启所有服务...${NC}" + docker compose -f "$COMPOSE_FILE" restart 2>/dev/null + echo -e " ${GREEN}✓${NC} 已重启\n" } show_logs() { - log_step "查看服务日志 (按 Ctrl+C 退出)" + echo -e "\n ${DIM}实时日志 (Ctrl+C 退出)${NC}\n" docker compose -f "$COMPOSE_FILE" logs -f --tail=50 } show_status() { - log_step "服务运行状态" - echo "" - docker compose -f "$COMPOSE_FILE" ps + echo -e "\n ${BOLD}服务运行状态${NC}\n" + docker compose -f "$COMPOSE_FILE" ps 2>/dev/null echo "" - # 检查 API 健康状态 - if curl -sf http://localhost:${API_PORT}/api/v1/health > /dev/null 2>&1; then + 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}}\t{{.NetIO}}" \ + docker stats --no-stream \ + --format "table {{.Name}}\t{{.CPUPerc}}\t{{.MemUsage}}" \ $(docker compose -f "$COMPOSE_FILE" ps -q) 2>/dev/null || true } clean_all() { - log_step "清理所有容器和数据" - - echo -e "${RED}${BOLD}警告: 此操作将删除所有容器、数据卷和镜像!${NC}" - echo -e "${RED}数据库中的所有数据将永久丢失!${NC}" - echo "" - read -p "确认清理? 输入 DELETE 继续: " confirm - + echo -e "\n ${RED}${BOLD}⚠ 危险操作: 将删除所有容器、数据卷和镜像${NC}" + echo -e " ${RED}数据库中的所有数据将永久丢失!${NC}\n" + read -rp " 输入 DELETE 确认: " confirm if [ "$confirm" != "DELETE" ]; then - log_info "已取消清理操作" - exit 0 + echo -e " ${GREEN}✓${NC} 已取消\n"; exit 0 fi - log_info "停止并删除容器 ..." - docker compose -f "$COMPOSE_FILE" down -v --remove-orphans - - log_info "删除构建的镜像 ..." - docker images --format "{{.Repository}}:{{.Tag}}" \ - | grep "panda-wiki" \ + 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 - - log_info "清理完成" + echo -e " ${GREEN}✓${NC} 清理完成\n" } # ============================================ @@ -397,90 +660,61 @@ 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 - ;; + --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 " 用法: $0 [选项]" echo "" - echo "选项:" - echo " --build 强制重新构建所有镜像 (默认)" - echo " --skip-build 跳过镜像构建步骤" - echo " --pull 拉取最新的基础镜像" - echo " --clean 停止并清理所有容器和数据 (危险!)" - echo " --stop 停止所有服务" - echo " --restart 重启所有服务" - echo " --logs 查看所有服务日志" - echo " --status 查看服务运行状态" - echo " --help 显示此帮助信息" + 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 # 重启已部署的服务" - echo " $0 --status # 查看运行状态" + echo " 示例:" + echo " $0 # 完整部署 (构建 + 启动)" + echo " $0 --skip-build # 跳过构建,直接启动" + echo " $0 --restart # 重启已部署的服务" exit 0 ;; - *) - log_error "未知选项: $1" - echo "使用 --help 查看帮助" - exit 1 - ;; + *) 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 - log_warn "检测到已有服务在运行" - read -p "是否重新部署? [y/N]: " redeploy + echo -e " ${YELLOW}⚠ 检测到已有服务在运行${NC}" + read -rp " 是否重新部署? [y/N]: " redeploy if [ "$redeploy" != "y" ] && [ "$redeploy" != "Y" ]; then - log_info "已取消。使用 --restart 重启,或 --stop 停止" - exit 0 + echo -e " ${GREEN}✓${NC} 已取消\n"; exit 0 fi - docker compose -f "$COMPOSE_FILE" down + 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 @@ -491,27 +725,22 @@ main() { if [ "$DO_BUILD" = true ]; then build_images else - log_step "跳过镜像构建" - # 检查镜像是否存在 - local missing_images=() - for img in panda-wiki-api:latest panda-wiki-consumer:latest panda-wiki-admin:latest panda-wiki-app:latest; do - if ! docker image inspect "$img" &> /dev/null; then - missing_images+=("$img") - fi + 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_images[@]} -gt 0 ]; then - log_warn "以下镜像不存在: ${missing_images[*]}" - read -p "是否现在构建? [Y/n]: " do_build_now - if [ "$do_build_now" != "n" ] && [ "$do_build_now" != "N" ]; then - build_images - else - log_error "缺少必需镜像,无法继续" - exit 1 - fi + if [ ${#missing[@]} -gt 0 ]; then + log_error "缺少镜像: ${missing[*]}" + echo " 请先运行 ./deploy.sh (不带 --skip-build) 构建镜像" + exit 1 fi + log_info "所有镜像已存在" fi - start_services + start_infra + start_app show_info } diff --git a/docker-compose.yml b/docker-compose.yml index 9f04fe2..88b40d8 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -7,31 +7,31 @@ services: postgres: image: postgres:16-alpine - container_name: panda-wiki-postgres + container_name: youdu-wiki-postgres restart: unless-stopped environment: - POSTGRES_USER: panda-wiki + POSTGRES_USER: youdu-wiki POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-ChangeMe123!} - POSTGRES_DB: panda-wiki + POSTGRES_DB: youdu-wiki volumes: - pg_data:/var/lib/postgresql/data networks: - - panda-wiki + - youdu-wiki healthcheck: - test: ["CMD-SHELL", "pg_isready -U panda-wiki"] + test: ["CMD-SHELL", "pg_isready -U youdu-wiki"] interval: 5s timeout: 5s retries: 10 redis: image: redis:7-alpine - container_name: panda-wiki-redis + container_name: youdu-wiki-redis restart: unless-stopped command: redis-server --appendonly yes --requirepass ${REDIS_PASSWORD:-ChangeMe123!} volumes: - redis_data:/data networks: - - panda-wiki + - youdu-wiki healthcheck: test: ["CMD", "redis-cli", "-a", "${REDIS_PASSWORD:-ChangeMe123!}", "ping"] interval: 5s @@ -40,28 +40,28 @@ services: nats: image: nats:2-alpine - container_name: panda-wiki-nats + container_name: youdu-wiki-nats restart: unless-stopped command: > -js -m 8222 - --user ${NATS_USER:-panda-wiki} + --user ${NATS_USER:-youdu-wiki} --pass ${NATS_PASSWORD:-ChangeMe123!} networks: - - panda-wiki + - youdu-wiki minio: image: minio/minio:latest - container_name: panda-wiki-minio + container_name: youdu-wiki-minio restart: unless-stopped command: server /data --console-address ":9001" environment: - MINIO_ROOT_USER: ${S3_ACCESS_KEY:-s3panda-wiki} + MINIO_ROOT_USER: ${S3_ACCESS_KEY:-s3youdu-wiki} MINIO_ROOT_PASSWORD: ${S3_SECRET_KEY:-ChangeMe123!} volumes: - minio_data:/data networks: - - panda-wiki + - youdu-wiki healthcheck: test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"] interval: 10s @@ -76,18 +76,18 @@ services: build: context: ./backend dockerfile: Dockerfile.api - image: panda-wiki-api:latest - container_name: panda-wiki-api + image: youdu-wiki-api:latest + container_name: youdu-wiki-api restart: unless-stopped environment: - PG_DSN: "host=panda-wiki-postgres user=panda-wiki password=${POSTGRES_PASSWORD:-ChangeMe123!} dbname=panda-wiki port=5432 sslmode=disable TimeZone=Asia/Shanghai" - MQ_NATS_SERVER: "nats://panda-wiki-nats:4222" - NATS_USER: ${NATS_USER:-panda-wiki} + PG_DSN: "host=youdu-wiki-postgres user=youdu-wiki password=${POSTGRES_PASSWORD:-ChangeMe123!} dbname=youdu-wiki port=5432 sslmode=disable TimeZone=Asia/Shanghai" + MQ_NATS_SERVER: "nats://youdu-wiki-nats:4222" + NATS_USER: ${NATS_USER:-youdu-wiki} NATS_PASSWORD: ${NATS_PASSWORD:-ChangeMe123!} - REDIS_ADDR: "panda-wiki-redis:6379" + REDIS_ADDR: "youdu-wiki-redis:6379" REDIS_PASSWORD: ${REDIS_PASSWORD:-ChangeMe123!} - S3_ENDPOINT: "panda-wiki-minio:9000" - S3_ACCESS_KEY: ${S3_ACCESS_KEY:-s3panda-wiki} + S3_ENDPOINT: "youdu-wiki-minio:9000" + S3_ACCESS_KEY: ${S3_ACCESS_KEY:-s3youdu-wiki} S3_SECRET_KEY: ${S3_SECRET_KEY:-ChangeMe123!} JWT_SECRET: ${JWT_SECRET:-$(openssl rand -hex 32)} ADMIN_PASSWORD: ${ADMIN_PASSWORD:-admin123} @@ -98,7 +98,7 @@ services: ports: - "${API_PORT:-8000}:8000" networks: - - panda-wiki + - youdu-wiki depends_on: postgres: condition: service_healthy @@ -111,18 +111,18 @@ services: build: context: ./backend dockerfile: Dockerfile.consumer - image: panda-wiki-consumer:latest - container_name: panda-wiki-consumer + image: youdu-wiki-consumer:latest + container_name: youdu-wiki-consumer restart: unless-stopped environment: - PG_DSN: "host=panda-wiki-postgres user=panda-wiki password=${POSTGRES_PASSWORD:-ChangeMe123!} dbname=panda-wiki port=5432 sslmode=disable TimeZone=Asia/Shanghai" - MQ_NATS_SERVER: "nats://panda-wiki-nats:4222" - NATS_USER: ${NATS_USER:-panda-wiki} + PG_DSN: "host=youdu-wiki-postgres user=youdu-wiki password=${POSTGRES_PASSWORD:-ChangeMe123!} dbname=youdu-wiki port=5432 sslmode=disable TimeZone=Asia/Shanghai" + MQ_NATS_SERVER: "nats://youdu-wiki-nats:4222" + NATS_USER: ${NATS_USER:-youdu-wiki} NATS_PASSWORD: ${NATS_PASSWORD:-ChangeMe123!} - REDIS_ADDR: "panda-wiki-redis:6379" + REDIS_ADDR: "youdu-wiki-redis:6379" REDIS_PASSWORD: ${REDIS_PASSWORD:-ChangeMe123!} - S3_ENDPOINT: "panda-wiki-minio:9000" - S3_ACCESS_KEY: ${S3_ACCESS_KEY:-s3panda-wiki} + S3_ENDPOINT: "youdu-wiki-minio:9000" + S3_ACCESS_KEY: ${S3_ACCESS_KEY:-s3youdu-wiki} S3_SECRET_KEY: ${S3_SECRET_KEY:-ChangeMe123!} JWT_SECRET: ${JWT_SECRET:-$(openssl rand -hex 32)} RAG_CT_RAG_BASE_URL: ${RAG_BASE_URL:-http://host.docker.internal:5050} @@ -130,7 +130,7 @@ services: ENV: ${ENV:-production} SENTRY_ENABLED: "false" networks: - - panda-wiki + - youdu-wiki depends_on: postgres: condition: service_healthy @@ -146,13 +146,13 @@ services: build: context: ./web dockerfile: Dockerfile.admin - image: panda-wiki-admin:latest - container_name: panda-wiki-admin + image: youdu-wiki-admin:latest + container_name: youdu-wiki-admin restart: unless-stopped ports: - "${ADMIN_PORT:-2443}:8080" networks: - - panda-wiki + - youdu-wiki depends_on: - api @@ -164,13 +164,13 @@ services: build: context: ./web dockerfile: Dockerfile.app - image: panda-wiki-app:latest - container_name: panda-wiki-app + image: youdu-wiki-app:latest + container_name: youdu-wiki-app restart: unless-stopped ports: - "${APP_PORT:-3010}:3010" networks: - - panda-wiki + - youdu-wiki depends_on: - api @@ -183,5 +183,5 @@ volumes: driver: local networks: - panda-wiki: + youdu-wiki: driver: bridge