Files
memind/rsync_to_server.sh
T
john 229805a070 Improve WeChat MP replies and ship MindSpace/H5 production updates.
Add WeChat service account routing with sync acks, connectivity tests, and context isolation; document deploy runbooks; and bundle related MindSpace, voice, Plaza, and server gateway changes for production rollout.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-19 23:06:43 +08:00

497 lines
22 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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-flightStudio
# ══════════════════════════════════════════════════════════════════════════════
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/tty
if [[ ! "${confirm}" =~ ^[Yy]$ ]]; then
echo "已取消。"
exit 0
fi
}
# ══════════════════════════════════════════════════════════════════════════════
# ④ Post-rsync 安全校验
# ══════════════════════════════════════════════════════════════════════════════
verify_studio_after_rsync() {
echo ""
echo -e "${BOLD}[校验] rsync 后 Studio 生产数据完整性${NC}"
# .env 未被覆盖(大小+修改时间应与 rsync 前完全一致)
local env_stat_now
env_stat_now="$(ssh_studio "stat -f'%z %m' '${STUDIO_REMOTE}/.env'")"
if [[ "${env_stat_now}" != "${STUDIO_ENV_STAT}" ]]; then
die ".env 在 rsync 后发生变化!(before: ${STUDIO_ENV_STAT} after: ${env_stat_now}\n 立即检查 Studio .env 是否被覆盖!"
fi
ok ".env 未变动(${env_stat_now}"
# MindSpace 条目数未减少
local ms_now
ms_now="$(ssh_studio "ls '${STUDIO_REMOTE}/MindSpace' | wc -l | tr -d ' '")"
if [[ "${ms_now}" -lt "${STUDIO_MS_COUNT}" ]]; then
die "MindSpace/ 条目从 ${STUDIO_MS_COUNT} 减少到 ${ms_now}rsync 可能误删了用户数据!"
fi
ok "MindSpace/ 条目数正常(${ms_now}"
# data/ 仍存在
if ! ssh_studio "test -d '${STUDIO_REMOTE}/data'"; then
die "data/ 目录在 rsync 后消失!"
fi
ok "data/ 目录存在"
# server.mjs patch 仍在(rsync 不能丢失 patch
local patch_count
patch_count="$(ssh_studio "grep -c 'WORKSPACE_MAINTENANCE_ENABLED' '${STUDIO_REMOTE}/server.mjs' || echo 0")"
if [[ "${patch_count}" -lt 1 ]]; then
die "Studio server.mjs 的 WORKSPACE_MAINTENANCE patch 丢失!\n 这会导致 105 下次重启时卡死,请立即还原!"
fi
ok "server.mjs WORKSPACE_MAINTENANCE patch 存在(${patch_count} 处)"
# 关键文件 md5 一致
echo ""
echo -e "${BOLD}[校验] 关键文件 md5${NC}"
local all_ok=1
for f in server.mjs user-publish.mjs user-space.mjs user-auth.mjs mindspace-sandbox-mcp.mjs capabilities.mjs package.json; do
[[ ! -f "${LOCAL_ROOT}/${f}" ]] && continue
local local_sum remote_sum
local_sum="$(md5 -q "${LOCAL_ROOT}/${f}" 2>/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/tty
[[ "${confirm}" =~ ^[Yy]$ ]] || exit 1
else
warn "已自动继续重启(--yes"
fi
else
ok "build 完成"
fi
else
echo ""
echo -e "${BOLD}▶ [Studio 3/4] 跳过 build--skip-build${NC}"
fi
if [[ "${NO_RESTART}" -eq 0 ]]; then
echo ""
echo -e "${BOLD}▶ [Studio 4/4] 重启服务(portal → plaza${NC}"
wait_studio_healthy
else
echo ""
echo -e "${BOLD}▶ [Studio 4/4] 跳过重启(--no-restart${NC}"
fi
fi
if [[ "${ONLY_100}" -eq 0 ]]; then
if [[ "${DRY_RUN}" -eq 1 ]]; then
echo ""
echo "--dry-run:跳过 105 同步,未做任何修改)"
exit 0
fi
# 确保 Studio 健康再触发 105
if [[ "${NO_RESTART}" -eq 0 && "${ONLY_105}" -eq 0 ]]; then
echo ""
echo -e "${BOLD}[安全] 再次确认 Studio 健康后再推 105...${NC}"
if ! ssh_studio "curl -sf '${STUDIO_HEALTH}'" &>/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