diff --git a/deploy-ai.sh b/deploy-ai.sh index acd91f5..5c6c5c0 100755 --- a/deploy-ai.sh +++ b/deploy-ai.sh @@ -24,13 +24,38 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" # ============================================================ # 颜色和日志 # ============================================================ -GREEN='\033[0;32m'; YELLOW='\033[1;33m'; CYAN='\033[0;36m'; RED='\033[0;31m'; NC='\033[0m' +GREEN='\033[0;32m'; YELLOW='\033[1;33m'; CYAN='\033[0;36m'; RED='\033[0;31m'; MAGENTA='\033[0;35m'; BOLD='\033[1m'; NC='\033[0m' log() { printf "${GREEN}[✓]${NC} %s\n" "$1"; } warn() { printf "${YELLOW}[!]${NC} %s\n" "$1"; } info() { printf "${CYAN}[i]${NC} %s\n" "$1"; } error() { printf "${RED}[✗]${NC} %s\n" "$1"; } timing(){ printf " ${CYAN}⏱${NC} %s\n" "$1"; } +# 阶段显示函数 +phase_num=0 +phase_start_time=0 +phase() { + local phase_name="$1" + phase_num=$((phase_num + 1)) + phase_start_time=$(date +%s) + echo "" + printf "${MAGENTA}${BOLD}════════════════════════════════════════════════════════════${NC}\n" + printf "${MAGENTA}${BOLD} [阶段 ${phase_num}] ${phase_name}${NC}\n" + printf "${MAGENTA}${BOLD}════════════════════════════════════════════════════════════${NC}\n" +} + +phase_done() { + local end_time=$(date +%s) + local duration=$((end_time - phase_start_time)) + printf "${GREEN}${BOLD} ✓ 阶段 ${phase_num} 完成 (${duration}s)${NC}\n" +} + +phase_fail() { + local end_time=$(date +%s) + local duration=$((end_time - phase_start_time)) + printf "${RED}${BOLD} ✗ 阶段 ${phase_num} 失败 (${duration}s)${NC}\n" +} + # ============================================================ # 检测 macOS 和 Linux sed 差异 # ============================================================ @@ -418,7 +443,7 @@ build_on_server() { --exclude='node_modules' --exclude='.next' --exclude='out' \ --exclude='data' --exclude='./reports/*.docx' --exclude='docs' \ --exclude='.env' --exclude='.env.local' --exclude='.env.development' --exclude='.env.production' \ - --exclude='.git' --exclude='._*' . || { + --exclude='.git' --exclude='._*' --exclude='shared' . || { error "打包失败" return 1 } @@ -443,10 +468,17 @@ build_on_server() { rsync -a --delete \ --exclude='node_modules' --exclude='.next' --exclude='data' --exclude='/reports/*.docx' --exclude='docs' \ --exclude='.env' --exclude='.env.local' --exclude='.env.development' --exclude='.env.production' \ - --exclude='._*' \ + --exclude='._*' --exclude='shared' \ /tmp/deploy_${SITE}/ ${REMOTE_DIR}/ && \ rm -rf /tmp/deploy_${SITE}" + # 确保 shared 符号链接正确(打包时已排除,需要重新创建) + ssh txjp "cd $REMOTE_DIR && if [ ! -L shared ]; then + rm -rf shared 2>/dev/null || true + ln -s ../shared shared + echo 'shared 符号链接已创建' + fi" + # 首次:上传 docker-compose.yml(新增 .next 卷挂载),重启容器应用 ssh txjp "cd $REMOTE_DIR && docker compose up -d" @@ -715,30 +747,54 @@ EOF # 执行部署 # ============================================================ echo "" +printf "${BOLD}${CYAN}=========================================${NC}\n" +printf "${BOLD}${CYAN} 开始部署: ${SITE}.tlyq.ai${NC}\n" +printf "${BOLD}${CYAN}=========================================${NC}\n" +DEPLOY_START=$(date +%s) case "$SITE" in www) + phase "打包配置文件" scp /Users/niuniu/programs/docker/www-ai/docker-compose.yml txjp:$REMOTE_DIR/docker-compose.yml scp /Users/niuniu/programs/docker/www-ai/nginx.conf txjp:$REMOTE_DIR/nginx.conf - build_on_server || exit 1 ;; + phase_done + + phase "构建与部署" + build_on_server || { phase_fail; exit 1; } + phase_done ;; cloud) - info "上传静态文件..." - scp "$LOCAL_DIR"/index.html txjp:$REMOTE_DIR/index.html || { error "cloud index.html 上传失败"; exit 1; } + phase "上传静态文件" + info "上传 index.html..." + scp "$LOCAL_DIR"/index.html txjp:$REMOTE_DIR/index.html || { error "cloud index.html 上传失败"; phase_fail; exit 1; } scp "$LOCAL_DIR"/nginx.conf txjp:$REMOTE_DIR/nginx.conf 2>/dev/null || true scp /Users/niuniu/programs/docker/cloud-ai/docker-compose.yml txjp:$REMOTE_DIR/docker-compose.yml 2>/dev/null || true - ssh txjp "docker restart ${SITE}-ai" ;; + phase_done + + phase "重启容器" + ssh txjp "docker restart ${SITE}-ai" + phase_done ;; token) - info "上传静态文件..." - scp "$LOCAL_DIR"/index.html txjp:$REMOTE_DIR/index.html || { error "token index.html 上传失败"; exit 1; } + phase "上传静态文件" + info "上传 index.html..." + scp "$LOCAL_DIR"/index.html txjp:$REMOTE_DIR/index.html || { error "token index.html 上传失败"; phase_fail; exit 1; } scp "$LOCAL_DIR"/nginx.conf txjp:$REMOTE_DIR/nginx.conf 2>/dev/null || true scp /Users/niuniu/programs/docker/token-ai/docker-compose.yml txjp:$REMOTE_DIR/docker-compose.yml 2>/dev/null || true - ssh txjp "docker restart ${SITE}-ai" ;; + phase_done + + phase "重启容器" + ssh txjp "docker restart ${SITE}-ai" + phase_done ;; issue) - build_on_server "http://assets-ai:3000/api" || exit 1 - info "部署 nginx 路由..." + phase "构建与部署" + build_on_server "http://assets-ai:3000/api" || { phase_fail; exit 1; } + phase_done + + phase "部署 nginx 路由" ssh txjp "cp $REMOTE_DIR/../nginx-proxy-ai/conf.d/issue-ai.conf /root/docker/nginx-proxy-ai/conf.d/ 2>/dev/null || true docker exec nginx-ai nginx -t && docker exec nginx-ai nginx -s reload" + phase_done # 验证 issue→assets API 连通性(带重试,应对同时部署时目标容器暂不可达) + phase "检查 API 连通性" info "检查 issue→assets API 连通性..." ssh txjp 'cat > /tmp/check-conn.js' << 'CHECKEOF' const http = require("http"); @@ -766,14 +822,20 @@ CHECKEOF echo " $CONN_CHECK" else log "issue→assets API 连通性正常" - fi ;; + fi + phase_done ;; assets) - build_on_server || exit 1 - info "部署 nginx 路由..." + phase "构建与部署" + build_on_server || { phase_fail; exit 1; } + phase_done + + phase "部署 nginx 路由" ssh txjp "cp $REMOTE_DIR/../nginx-proxy-ai/conf.d/assets-ai.conf /root/docker/nginx-proxy-ai/conf.d/ 2>/dev/null || true docker exec nginx-ai nginx -t && docker exec nginx-ai nginx -s reload" + phase_done # 验证 assets→issue API 连通性(带重试,应对同时部署时目标容器暂不可达) + phase "检查 API 连通性" info "检查 assets→issue API 连通性..." ssh txjp 'cat > /tmp/check-conn.js' << 'CHECKEOF' const http = require("http"); @@ -801,9 +863,14 @@ CHECKEOF echo " $CONN_CHECK" else log "assets→issue API 连通性正常" - fi ;; + fi + phase_done ;; oa) - build_on_server || exit 1 + phase "构建与部署" + build_on_server || { phase_fail; exit 1; } + phase_done + + phase "配置环境变量" # 注入 LLDAP admin 密码(OA OIDC callback 需要验证 LLDAP 用户存在性) info "注入 LLDAP admin 密码到 OA .env..." ssh txjp " @@ -814,15 +881,20 @@ CHECKEOF || echo \"LLDAP_ADMIN_PASSWORD=\$LLDAP_ADMIN_PASS\" >> $REMOTE_DIR/.env fi " - info "部署 nginx 配置..." + phase_done + + phase "部署 nginx 配置" ssh txjp "cp $REMOTE_DIR/../nginx-proxy-ai/conf.d/oa-ai.conf /root/docker/nginx-proxy-ai/conf.d/ 2>/dev/null || true docker exec nginx-ai nginx -t && docker exec nginx-ai nginx -s reload" + phase_done + + phase "同步数据" # 批量同步 LLDAP email 至各站点(新服务器首次部署填充历史数据) info "同步 LLDAP 邮箱至各站点..." ssh txjp "node /root/docker/oa-ai/scripts/sync-emails-to-sites.js 2>/dev/null" || warn "邮箱同步失败(不影响 OA 主流程)" - ;; + phase_done ;; ldap) - info "部署 LLDAP..." + phase "部署 LLDAP" scp "$LOCAL_DIR/docker-compose.yml" txjp:$REMOTE_DIR/docker-compose.yml scp "$LOCAL_DIR/Dockerfile" txjp:$REMOTE_DIR/Dockerfile ssh txjp "cd $REMOTE_DIR && \ @@ -850,28 +922,35 @@ CHECKEOF chmod 600 .env && \ sed -i '/3890:3890/d' docker-compose.yml 2>/dev/null || true && \ docker compose up -d --build && docker compose restart" - log "LLDAP 已部署" ;; + log "LLDAP 已部署" + phase_done ;; gitea) - info "部署 Gitea..." + phase "部署 Gitea" scp "$LOCAL_DIR/docker-compose.yml" txjp:$REMOTE_DIR/docker-compose.yml # 同步证书目录(若本地存在) [ -d "$LOCAL_DIR/cert" ] && scp -r "$LOCAL_DIR"/cert/* txjp:$REMOTE_DIR/cert/ 2>/dev/null || true ssh txjp "cd $REMOTE_DIR && docker compose up -d && docker restart nginx-ai" - log "Gitea 已部署" ;; + log "Gitea 已部署" + phase_done ;; sso) - deploy_sso ;; + phase "部署 Authelia SSO" + deploy_sso + phase_done ;; monitor) - build_on_server || exit 1 - info "部署 nginx 配置..." + phase "构建与部署" + build_on_server || { phase_fail; exit 1; } + phase_done + + phase "部署 nginx 配置" ssh txjp "cp $REMOTE_DIR/../nginx-proxy-ai/conf.d/monitor-ai.conf /root/docker/nginx-proxy-ai/conf.d/ 2>/dev/null || true - docker exec nginx-ai nginx -t && docker exec nginx-ai nginx -s reload" ;; + docker exec nginx-ai nginx -t && docker exec nginx-ai nginx -s reload" + phase_done ;; esac # ============================================================ # 运行时合规检查(LESSONS-LEARNED) # ============================================================ -echo "" -info "运行时合规检查..." +phase "运行时合规检查" # LL #7: SSL 证书覆盖目标域名(仅 tlyq.ai 站点) if [[ "$SITE" != "ldap" ]]; then @@ -925,8 +1004,7 @@ fi # ============================================================ # 验证 # ============================================================ -echo "" -log "验证部署..." +phase "验证部署" BUILD_END=$(date +%s) TOTAL_DUR=$((BUILD_END - BUILD_START)) @@ -950,9 +1028,15 @@ else STATUS=$(ssh txjp "curl -s -o /dev/null -w '%{http_code}' '$URL' 2>/dev/null" 2>/dev/null || echo "???") fi if [[ "$STATUS" == "200" || "$STATUS" == "307" || "$STATUS" == "301" ]]; then - log "部署成功!总耗时: ${TOTAL_DUR}s | 访问 $URL" + phase_done + echo "" + printf "${GREEN}${BOLD}════════════════════════════════════════════════════════════${NC}\n" + printf "${GREEN}${BOLD} 🎉 部署成功!${NC}\n" + printf "${GREEN}${BOLD} 总耗时: ${TOTAL_DUR}s | 访问: $URL${NC}\n" + printf "${GREEN}${BOLD}════════════════════════════════════════════════════════════${NC}\n" exit 0 else + phase_done warn "返回状态码: $STATUS,请检查" exit 1 fi