#!/usr/bin/env bash # 一键部署:本地 dev → Studio(100) → 105 # # 用法: # ./rsync_to_server.sh # 全量部署(100 先,105 后) # ./rsync_to_server.sh --only-100 # 只更新 Studio # ./rsync_to_server.sh --only-105 # 只触发 105 同步(Studio 代码已是最新) # ./rsync_to_server.sh --skip-build # 跳过 vite build(纯后端改动时加速) # ./rsync_to_server.sh --no-restart # 只同步代码,不重启服务 # ./rsync_to_server.sh --dry-run # 预览将同步哪些文件,不做任何修改 set -euo pipefail # ── 配置 ────────────────────────────────────────────────────────────────────── STUDIO_HOST="${STUDIO_HOST:-100.99.38.66}" STUDIO_REMOTE="${STUDIO_REMOTE:-/Users/john/Project/Memind}" STUDIO_NODE_BIN="${STUDIO_NODE_BIN:-/opt/homebrew/opt/node@24/bin}" STUDIO_LAUNCHAGENT_PORTAL="cn.tkmind.memind-portal" STUDIO_LAUNCHAGENT_PLAZA="cn.tkmind.plaza" STUDIO_HEALTH="http://127.0.0.1:8081/api/status" STUDIO_PLAZA_HEALTH="http://127.0.0.1:3001/plaza" STUDIO_LOG_PORTAL="$HOME/Library/Logs/memind-portal.log" STUDIO_LOG_PLAZA="$HOME/Library/Logs/memind-plaza.log" LOCAL_ROOT="$(cd "$(dirname "$0")" && pwd)" EXCLUDE_STUDIO="${LOCAL_ROOT}/scripts/.rsync-exclude-studio" # ── 参数解析 ────────────────────────────────────────────────────────────────── ONLY_100=0 ONLY_105=0 SKIP_BUILD=0 NO_RESTART=0 DRY_RUN=0 AUTO_YES=0 for arg in "$@"; do case "$arg" in --only-100) ONLY_100=1 ;; --only-105) ONLY_105=1 ;; --skip-build) SKIP_BUILD=1 ;; --no-restart) NO_RESTART=1 ;; --dry-run) DRY_RUN=1 ;; --yes|-y) AUTO_YES=1 ;; -h|--help) sed -n '2,9p' "$0" exit 0 ;; *) echo "未知参数: $arg" >&2 echo "用法: $0 [--only-100] [--only-105] [--skip-build] [--no-restart] [--dry-run]" >&2 exit 1 ;; esac done if [[ "${ONLY_100}" -eq 1 && "${ONLY_105}" -eq 1 ]]; then echo "错误: --only-100 与 --only-105 不能同时使用" >&2; exit 1 fi # ── 工具函数 ────────────────────────────────────────────────────────────────── RED='\033[0;31m'; YELLOW='\033[1;33m'; GREEN='\033[0;32m'; BOLD='\033[1m'; NC='\033[0m' hr() { echo "──────────────────────────────────────────────────────"; } ok() { echo -e " ${GREEN}✓${NC} $*"; } warn() { echo -e " ${YELLOW}⚠${NC} $*"; } fail() { echo -e " ${RED}✗${NC} $*" >&2; } die() { fail "$*"; hr; exit 1; } ssh_studio() { ssh -o BatchMode=yes -o ConnectTimeout=15 "${STUDIO_HOST}" "$@"; } ssh_105() { ssh_studio "ssh -o BatchMode=yes -o ConnectTimeout=15 ssh105 '$*'" 2>/dev/null; } # ══════════════════════════════════════════════════════════════════════════════ # ① 本地 Pre-flight # ══════════════════════════════════════════════════════════════════════════════ preflight_local() { echo "" echo -e "${BOLD}[Pre-flight] 本地检查${NC}" # exclude 文件 [[ -f "${EXCLUDE_STUDIO}" ]] \ && ok "exclude 文件存在: scripts/.rsync-exclude-studio" \ || die "缺少 ${EXCLUDE_STUDIO},无法继续" # 关键源码文件 local missing=() for f in server.mjs user-auth.mjs user-publish.mjs user-space.mjs package.json; do [[ -f "${LOCAL_ROOT}/${f}" ]] || missing+=("${f}") done if [[ ${#missing[@]} -gt 0 ]]; then die "本地缺少关键文件: ${missing[*]}" fi ok "关键源码文件存在" # WORKSPACE_MAINTENANCE patch —— 必须存在,否则 105 下次重启会卡死 if ! grep -q 'WORKSPACE_MAINTENANCE_ENABLED' "${LOCAL_ROOT}/server.mjs"; then die "server.mjs 缺少 WORKSPACE_MAINTENANCE_ENABLED patch!\n 此 patch 防止 105 在 rclone 挂载上启动时卡死。\n 请合并 Studio 生产版的 server.mjs patch 后再部署。" fi ok "server.mjs 含 WORKSPACE_MAINTENANCE patch" # 本地 .env 不能被意外同步(确认在 exclude 里) if grep -q '^\.env$' "${EXCLUDE_STUDIO}"; then ok ".env 已在 exclude 列表,不会被同步到 Studio" else die "exclude 文件未排除 .env!请检查 scripts/.rsync-exclude-studio" fi # MindSpace 已被排除 if grep -q '^MindSpace/' "${EXCLUDE_STUDIO}"; then ok "MindSpace/ 已在 exclude 列表" else die "exclude 文件未排除 MindSpace/!" fi } # ══════════════════════════════════════════════════════════════════════════════ # ② 远端 Pre-flight(Studio) # ══════════════════════════════════════════════════════════════════════════════ preflight_studio() { echo "" echo -e "${BOLD}[Pre-flight] Studio(${STUDIO_HOST}) 检查${NC}" # SSH 连通性 if ! ssh_studio "echo ok" &>/dev/null; then die "无法 SSH 到 Studio (${STUDIO_HOST})" fi ok "SSH 连通" # .env 存在且非空 local env_size env_size="$(ssh_studio "stat -f%z '${STUDIO_REMOTE}/.env' 2>/dev/null || echo 0")" if [[ "${env_size}" -lt 10 ]]; then die "Studio .env 不存在或为空!(size=${env_size})" fi ok ".env 存在(${env_size} bytes)" # MindSpace 有内容 local ms_count ms_count="$(ssh_studio "ls '${STUDIO_REMOTE}/MindSpace' | wc -l | tr -d ' '")" if [[ "${ms_count}" -lt 1 ]]; then die "Studio MindSpace/ 为空!部署前请确认生产数据正常。" fi ok "MindSpace/ 有 ${ms_count} 个条目" # data/ 目录存在 if ! ssh_studio "test -d '${STUDIO_REMOTE}/data'"; then die "Studio data/ 目录不存在!" fi ok "data/ 目录存在" # 记录 .env 快照(大小 + 修改时间),用于 post-rsync 验证 STUDIO_ENV_STAT="$(ssh_studio "stat -f'%z %m' '${STUDIO_REMOTE}/.env'")" STUDIO_MS_COUNT="${ms_count}" } # ══════════════════════════════════════════════════════════════════════════════ # ③ Dry-run 预览 # ══════════════════════════════════════════════════════════════════════════════ show_dry_run() { echo "" echo -e "${BOLD}[预览] 将同步以下文件变更到 Studio${NC}" rsync -az --delete --dry-run --itemize-changes \ --exclude-from="${EXCLUDE_STUDIO}" \ "${LOCAL_ROOT}/" \ "${STUDIO_HOST}:${STUDIO_REMOTE}/" \ 2>/dev/null \ | grep -v '^\.' \ | head -80 \ || true echo "" if [[ "${DRY_RUN}" -eq 1 ]]; then echo "(--dry-run 模式,未做任何实际修改)" return 0 fi if [[ "${AUTO_YES}" -eq 1 ]]; then ok "已自动确认(--yes)" return 0 fi # 交互确认(直接读 /dev/tty,不受 stdin pipe 影响) local confirm read -r -p " 确认同步?[y/N] " confirm /dev/null || md5sum "${LOCAL_ROOT}/${f}" | awk '{print $1}')" remote_sum="$(ssh_studio "openssl md5 '${STUDIO_REMOTE}/${f}' 2>/dev/null | awk '{print \$2}'")" if [[ "${local_sum}" == "${remote_sum}" ]]; then ok "${f}" else fail "${f} md5 不一致(local=${local_sum} remote=${remote_sum})" all_ok=0 fi done [[ "${all_ok}" -eq 1 ]] || die "md5 校验失败,请检查文件同步是否正常" } # ══════════════════════════════════════════════════════════════════════════════ # ⑤ Studio 重启并健康检查(Portal → Plaza 顺序) # ══════════════════════════════════════════════════════════════════════════════ wait_studio_healthy() { local STUDIO_UID STUDIO_UID="$(ssh_studio "id -u")" # ── Portal ────────────────────────────────────────────────────────────────── echo "" echo -e "${BOLD}[重启] Studio portal (cn.tkmind.memind-portal)${NC}" ssh_studio "launchctl kickstart -k gui/${STUDIO_UID}/${STUDIO_LAUNCHAGENT_PORTAL}" >/dev/null echo -n " 等待 portal :8081..." local ok_flag=0 for i in $(seq 1 20); do sleep 2 if ssh_studio "curl -sf '${STUDIO_HEALTH}'" &>/dev/null; then echo " ✓ (${i}×2s = $((i*2))s)" ok_flag=1 break fi printf "." done if [[ "${ok_flag}" -eq 0 ]]; then echo "" fail "portal 40s 内未就绪" echo " 最近日志:" ssh_studio "tail -30 '${STUDIO_LOG_PORTAL}' 2>/dev/null" | sed 's/^/ /' die "Studio portal 启动失败,已中止,不会继续重启 Plaza 或同步 105" fi # 日志扫描 local portal_errors portal_errors="$(ssh_studio "tail -60 '${STUDIO_LOG_PORTAL}' 2>/dev/null \ | grep -iE 'error|exception|uncaught|fatal' || true")" if [[ -n "${portal_errors}" ]]; then warn "portal 启动日志含警告/错误(请人工确认):" echo "${portal_errors}" | head -8 | sed 's/^/ /' else ok "portal 启动日志无 ERROR" fi # ── Plaza ─────────────────────────────────────────────────────────────────── # Plaza 依赖 portal 先就绪(run-plaza-prod.sh 内有等待逻辑),所以 portal 健康后才重启 echo "" echo -e "${BOLD}[重启] Studio plaza (cn.tkmind.plaza)${NC}" ssh_studio "launchctl kickstart -k gui/${STUDIO_UID}/${STUDIO_LAUNCHAGENT_PLAZA}" >/dev/null echo -n " 等待 plaza :3001..." ok_flag=0 for i in $(seq 1 30); do sleep 3 if ssh_studio "curl -sf -o /dev/null '${STUDIO_PLAZA_HEALTH}'" &>/dev/null; then echo " ✓ (${i}×3s = $((i*3))s)" ok_flag=1 break fi printf "." done if [[ "${ok_flag}" -eq 0 ]]; then echo "" warn "plaza 90s 内未就绪(非致命,portal 和 105 仍可用)" echo " 最近日志:" ssh_studio "tail -20 '${STUDIO_LOG_PLAZA}' 2>/dev/null" | sed 's/^/ /' else local plaza_errors plaza_errors="$(ssh_studio "tail -40 '${STUDIO_LOG_PLAZA}' 2>/dev/null \ | grep -iE 'error|exception|uncaught|fatal' || true")" if [[ -n "${plaza_errors}" ]]; then warn "plaza 启动日志含警告/错误:" echo "${plaza_errors}" | head -8 | sed 's/^/ /' else ok "plaza 启动日志无 ERROR" fi fi } # ══════════════════════════════════════════════════════════════════════════════ # ⑥ 105 Post-sync 额外校验 # ══════════════════════════════════════════════════════════════════════════════ verify_105_after_sync() { echo "" echo -e "${BOLD}[校验] 105 部署后检查${NC}" # goose-h5 正在运行 local svc_status svc_status="$(ssh_studio "ssh ssh105 'systemctl is-active goose-h5 2>/dev/null'" 2>/dev/null || echo "unknown")" if [[ "${svc_status}" == "active" ]]; then ok "105 goose-h5 状态: active" else die "105 goose-h5 不是 active 状态(${svc_status})" fi # 105 .env 关键开关仍然正确 local maintenance_val maintenance_val="$(ssh_studio "ssh ssh105 'grep MEMIND_WORKSPACE_MAINTENANCE /root/tkmind_go/ui/h5/.env 2>/dev/null'" 2>/dev/null || echo "")" if echo "${maintenance_val}" | grep -q 'MEMIND_WORKSPACE_MAINTENANCE=0'; then ok "105 .env: MEMIND_WORKSPACE_MAINTENANCE=0 ✓" else die "105 .env 缺少或错误设置 MEMIND_WORKSPACE_MAINTENANCE!\n 当前值: ${maintenance_val}\n 必须为 0,否则 105 重启会卡死!" fi # 105 server.mjs patch 存在 local patch_count_105 patch_count_105="$(ssh_studio "ssh ssh105 'grep -c WORKSPACE_MAINTENANCE_ENABLED /root/tkmind_go/ui/h5/server.mjs 2>/dev/null || echo 0'" 2>/dev/null || echo "0")" if [[ "${patch_count_105}" -ge 1 ]]; then ok "105 server.mjs WORKSPACE_MAINTENANCE patch 存在" else die "105 server.mjs 缺少 WORKSPACE_MAINTENANCE patch!\n 105 重启时将对 rclone 挂载做 fs.watch,导致卡死!" fi # 105 MindSpace 挂载有内容 local ms_105 ms_105="$(ssh_studio "ssh ssh105 'ls /mnt/memind-shared/MindSpace 2>/dev/null | wc -l | tr -d \" \"'" 2>/dev/null || echo "0")" if [[ "${ms_105}" -gt 0 ]]; then ok "105 MindSpace 挂载正常(${ms_105} 个条目)" else warn "105 /mnt/memind-shared/MindSpace 为空,请确认 rclone 挂载是否正常" fi # 105 goose-h5 健康检查 if ssh_studio "ssh ssh105 'curl -sf http://127.0.0.1:8080/api/status'" &>/dev/null; then ok "105 goose-h5 :8080/api/status 健康" else die "105 goose-h5 健康检查失败" fi # 105 goose-plaza-web(可选服务,不在 sync-to-105.sh 里重启,检查状态即可) local plaza_105_status plaza_105_status="$(ssh_studio "ssh ssh105 'systemctl is-active goose-plaza-web 2>/dev/null || echo inactive'" 2>/dev/null || echo "unknown")" if [[ "${plaza_105_status}" == "active" ]]; then ok "105 goose-plaza-web 状态: active" else warn "105 goose-plaza-web 不是 active(${plaza_105_status})" warn "如有 plaza 代码变更,需手动重启: ssh ssh105 systemctl restart goose-plaza-web" fi } # ══════════════════════════════════════════════════════════════════════════════ # Banner # ══════════════════════════════════════════════════════════════════════════════ hr echo -e " ${BOLD}Memind 一键部署${NC} $(date '+%Y-%m-%d %H:%M:%S')" [[ "${ONLY_105}" -eq 0 ]] && echo " 本地 → Studio: ${STUDIO_HOST}:${STUDIO_REMOTE}" [[ "${ONLY_100}" -eq 0 ]] && echo " Studio → 105: 经 sync-to-105.sh 触发" [[ "${SKIP_BUILD}" -eq 1 ]] && echo " 模式: --skip-build(跳过 vite build)" [[ "${NO_RESTART}" -eq 1 ]] && echo " 模式: --no-restart(只同步代码)" [[ "${DRY_RUN}" -eq 1 ]] && echo " 模式: --dry-run(预览,不做修改)" hr # ══════════════════════════════════════════════════════════════════════════════ # 主流程 # ══════════════════════════════════════════════════════════════════════════════ if [[ "${ONLY_105}" -eq 0 ]]; then preflight_local preflight_studio show_dry_run if [[ "${DRY_RUN}" -eq 1 ]]; then echo "" echo "(--dry-run 完成:以上仅为预览,未对 Studio/105 做任何修改)" exit 0 fi echo "" echo -e "${BOLD}▶ [Studio 1/4] rsync 代码...${NC}" rsync -az --delete \ --exclude-from="${EXCLUDE_STUDIO}" \ "${LOCAL_ROOT}/" \ "${STUDIO_HOST}:${STUDIO_REMOTE}/" ok "代码同步完成" verify_studio_after_rsync echo "" echo -e "${BOLD}▶ [Studio 2/4] npm install...${NC}" ssh_studio "cd '${STUDIO_REMOTE}' && PATH='${STUDIO_NODE_BIN}':'\$PATH' npm install --prefer-offline 2>&1 | tail -5" if [[ "${SKIP_BUILD}" -eq 0 ]]; then echo "" echo -e "${BOLD}▶ [Studio 3/4] npm run build...${NC}" if ! ssh_studio "cd '${STUDIO_REMOTE}' && PATH='${STUDIO_NODE_BIN}':'\$PATH' npm run build"; then warn "vite build 失败,服务仍在运行旧产物" if [[ "${AUTO_YES}" -eq 0 ]]; then local confirm read -r -p " 继续重启服务?[y/N] " confirm /dev/null; then die "Studio portal 此刻不健康,已暂停 105 同步,请先排查 Studio 问题" fi ok "Studio portal 确认健康" fi echo "" echo -e "${BOLD}▶ [105] 触发 Studio → 105 同步...${NC}" SYNC_ENV="" [[ "${SKIP_BUILD}" -eq 1 ]] && SYNC_ENV="${SYNC_ENV} SKIP_BUILD=1" [[ "${NO_RESTART}" -eq 1 ]] && SYNC_ENV="${SYNC_ENV} NO_RESTART=1" ssh_studio "cd '${STUDIO_REMOTE}' && env ${SYNC_ENV} bash scripts/sync-to-105.sh" if [[ "${NO_RESTART}" -eq 0 ]]; then verify_105_after_sync fi fi # ══════════════════════════════════════════════════════════════════════════════ # 完成 # ══════════════════════════════════════════════════════════════════════════════ echo "" hr if [[ "${ONLY_105}" -eq 1 ]]; then echo -e " ${GREEN}${BOLD}✅ 105 已更新${NC}" elif [[ "${ONLY_100}" -eq 1 ]]; then echo -e " ${GREEN}${BOLD}✅ Studio(100) 已更新${NC}" else echo -e " ${GREEN}${BOLD}✅ Studio(100) + 105 均已更新${NC}" fi echo " $(date '+%Y-%m-%d %H:%M:%S')" hr