#!/usr/bin/env bash # deploy.sh — 部署/升级 moziplus v2 set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" PROJECT_DIR="$(dirname "$SCRIPT_DIR")" # 默认值 SOURCE_DIR="$PROJECT_DIR" TARGET_DIR="${MOZIPLUS_V2_DIR:-$HOME/.sanguo_projects/sanguo_moziplus_v2}" PM2_NAME="sanguo-moziplus-v2" SKIP_BUILD=false usage() { echo "Usage: $0 [options]" echo "" echo "Options:" echo " --source=DIR 源码目录 (default: 项目开发目录)" echo " --target=DIR 安装目标目录 (default: ~/.sanguo_projects/sanguo_moziplus_v2)" echo " --skip-build 跳过前端构建" echo " --version 显示当前部署版本" echo " --rollback 回滚到上一个部署版本" echo " -h, --help 显示帮助" exit 0 } for arg in "$@"; do case "$arg" in --source=*) SOURCE_DIR="${arg#*=}" ;; --target=*) TARGET_DIR="${arg#*=}" ;; --skip-build) SKIP_BUILD=true ;; --version) ACTION=version ;; --rollback) ACTION=rollback ;; --health-check) ;; # 保留兼容,无额外操作 -h|--help) usage ;; esac done ACTION="${ACTION:-deploy}" # ── 部署历史文件 ── HISTORY_FILE="$TARGET_DIR/data/deploy-history.jsonl" # ── version 分支 ── if [ "$ACTION" = "version" ]; then if [ -f "$HISTORY_FILE" ]; then LAST_COMMIT=$(tail -1 "$HISTORY_FILE" | python3 -c 'import sys,json; print(json.load(sys.stdin).get("commit","unknown"))' 2>/dev/null || echo "unknown") echo "$LAST_COMMIT" else echo "No deployment history found." fi exit 0 fi # ── rollback 分支 ── if [ "$ACTION" = "rollback" ]; then if [ ! -f "$HISTORY_FILE" ]; then echo "❌ No deployment history, cannot rollback." exit 1 fi LINE_COUNT=$(wc -l < "$HISTORY_FILE") if [ "$LINE_COUNT" -lt 2 ]; then echo "❌ Not enough history for rollback (need at least 2 entries)." exit 1 fi ROLLBACK_COMMIT=$(tail -2 "$HISTORY_FILE" | head -1 | python3 -c 'import sys,json; print(json.load(sys.stdin).get("commit",""))' 2>/dev/null || echo "") if [ -z "$ROLLBACK_COMMIT" ]; then echo "❌ Could not parse previous commit from history." exit 1 fi echo "🔄 Rolling back to commit: $ROLLBACK_COMMIT" # 保存当前分支/commit 以便恢复 CURRENT_REF=$(git -C "$SOURCE_DIR" rev-parse HEAD 2>/dev/null || echo "") # checkout 到目标 commit git -C "$SOURCE_DIR" checkout "$ROLLBACK_COMMIT" 2>/dev/null # 使用 DEPLOY_OVERRIDE_COMMIT 显式传 commit hash DEPLOY_OVERRIDE_COMMIT="$ROLLBACK_COMMIT" bash "$0" --source="$SOURCE_DIR" --target="$TARGET_DIR" --skip-build # 恢复到原来的 commit if [ -n "$CURRENT_REF" ]; then git -C "$SOURCE_DIR" checkout "$CURRENT_REF" 2>/dev/null || true fi exit 0 fi echo "🚀 Deploying moziplus v2" echo " Source: $SOURCE_DIR" echo " Target: $TARGET_DIR" # ── 前置检查 ── echo "" echo "📋 Pre-flight checks..." if ! command -v python3 &>/dev/null; then echo "❌ python3 not found" && exit 1 fi PYTHON_VERSION=$(python3 -c 'import sys; print(f"{sys.version_info.major}.{sys.version_info.minor}")') echo " Python: $PYTHON_VERSION ✅" if ! command -v pm2 &>/dev/null; then echo "❌ pm2 not found" && exit 1 fi echo " PM2: $(pm2 --version) ✅" if [ ! -d "$SOURCE_DIR/src" ]; then echo "❌ Source directory invalid: $SOURCE_DIR/src not found" && exit 1 fi # ── 前端构建 ── if [ "$SKIP_BUILD" = false ]; then echo "" echo "🔨 Building frontend..." bash "$SCRIPT_DIR/build-frontend.sh" else echo "" echo "⏭️ Skipping frontend build (--skip-build)" # 确保前端 dist 存在 if [ ! -d "$SOURCE_DIR/src/frontend/dist" ]; then echo "❌ Frontend dist not found. Run without --skip-build first." && exit 1 fi fi # ── 创建目标目录 ── FIRST_INSTALL=false if [ ! -d "$TARGET_DIR" ]; then FIRST_INSTALL=true echo "" echo "📁 Creating target directory: $TARGET_DIR" mkdir -p "$TARGET_DIR" fi # ── 同步代码 ── echo "" echo "🔄 Syncing code..." rsync -a --delete \ --exclude='.git/' \ --exclude='docs/' \ --exclude='tests/' \ --exclude='scripts/' \ --exclude='data/' \ --exclude='logs/' \ --exclude='inbox/' \ --exclude='node_modules/' \ --exclude='tsconfig.tsbuildinfo' \ --exclude='__pycache__/' \ --exclude='.venv/' \ --exclude='.DS_Store' \ --exclude='config/' \ "$SOURCE_DIR/" "$TARGET_DIR/" # 但不覆盖运行时配置和数据 # rsync --delete 不影响 exclude 的目录 echo " Code synced ✅" # ── 确保目录结构 ── mkdir -p "$TARGET_DIR/data" mkdir -p "$TARGET_DIR/logs" mkdir -p "$TARGET_DIR/inbox" # 确保默认配置存在(不覆盖已有配置) if [ ! -f "$TARGET_DIR/config/default.yaml" ]; then if [ -f "$SOURCE_DIR/config/default.yaml" ]; then cp "$SOURCE_DIR/config/default.yaml" "$TARGET_DIR/config/default.yaml" echo " Created default config ✅" fi fi if [ ! -f "$TARGET_DIR/config/guardrails.yaml" ]; then if [ -f "$SOURCE_DIR/config/guardrails.yaml" ]; then cp "$SOURCE_DIR/config/guardrails.yaml" "$TARGET_DIR/config/guardrails.yaml" echo " Created guardrails config ✅" fi fi # ── PM2 管理 ── echo "" if [ "$FIRST_INSTALL" = true ]; then echo "🆕 First install — starting with PM2..." cd "$TARGET_DIR" pm2 start ecosystem.config.cjs else echo "♻️ Upgrading — restarting PM2..." pm2 restart "$PM2_NAME" --update-env fi pm2 save # ── 前端健康检查 ── echo " Checking frontend..." FRONTEND_OK=false for i in $(seq 1 5); do if curl -sf http://localhost:8083/ >/dev/null 2>&1; then FRONTEND_OK=true break fi sleep 1 done if [ "$FRONTEND_OK" = true ]; then echo " Frontend check passed ✅" else echo " ⚠️ Frontend check timeout" fi # ── 后端健康检查 ── echo " Checking backend..." HEALTH_URL="http://localhost:8083/api/daemon/status" OK=false for i in $(seq 1 10); do if curl -sf "$HEALTH_URL" >/dev/null 2>&1; then OK=true break fi sleep 1 done if [ "$OK" = true ]; then echo " Health check passed ✅" else echo " ⚠️ Health check timeout (service may still be starting)" echo " Check: pm2 logs $PM2_NAME" fi # ── 记录部署历史 ── mkdir -p "$(dirname "$HISTORY_FILE")" if [ -n "${DEPLOY_OVERRIDE_COMMIT:-}" ]; then DEPLOYED_COMMIT="$DEPLOY_OVERRIDE_COMMIT" else DEPLOYED_COMMIT=$(git -C "$SOURCE_DIR" rev-parse --short HEAD 2>/dev/null || echo "unknown") fi DEPLOY_TIMESTAMP=$(date -u +%Y-%m-%dT%H:%M:%SZ) DEPLOY_SOURCE="${SOURCE_DIR}" HISTORY_ENTRY=$(printf '%s' '{"timestamp":"'$DEPLOY_TIMESTAMP'","commit":"'$DEPLOYED_COMMIT'","source":"'$DEPLOY_SOURCE'"}') echo "$HISTORY_ENTRY" >> "$HISTORY_FILE" # 保留最近 10 条 if [ -f "$HISTORY_FILE" ]; then TMPFILE=$(mktemp) tail -10 "$HISTORY_FILE" > "$TMPFILE" mv "$TMPFILE" "$HISTORY_FILE" fi echo " Deploy history recorded ($DEPLOYED_COMMIT) ✅" # ── 完成 ── echo "" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" if [ "$FIRST_INSTALL" = true ]; then echo "✅ moziplus v2 installed successfully!" else echo "✅ moziplus v2 upgraded successfully!" fi echo " URL: http://localhost:8083/" echo " PM2: pm2 status $PM2_NAME" echo " Logs: pm2 logs $PM2_NAME" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"