Add smart ACK provider for WeChat MP replies

Replace fixed ackText with a rule-based AckProvider that picks
response templates by message type and intent (translate, summary,
rewrite, poster, ppt, mindmap, code, search, schedule). Pure sync,
zero I/O, auto-falls back to config.ackText on any error.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
john
2026-06-26 15:19:03 +08:00
parent 9ed4fd48d7
commit 9b4a25799f
162 changed files with 17276 additions and 2054 deletions
+11 -490
View File
@@ -1,496 +1,17 @@
#!/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"
cat >&2 <<'EOF'
`rsync_to_server.sh` 已禁用。
LOCAL_ROOT="$(cd "$(dirname "$0")" && pwd)"
EXCLUDE_STUDIO="${LOCAL_ROOT}/scripts/.rsync-exclude-studio"
103 / Studio 生产发布禁止再走 rsync。
请改用:
# ── 参数解析 ──────────────────────────────────────────────────────────────────
ONLY_100=0
ONLY_105=0
SKIP_BUILD=0
NO_RESTART=0
DRY_RUN=0
AUTO_YES=0
bash scripts/release-portal-runtime-prod.sh
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
规则见:
- PRODUCTION_RELEASE_RULES.md
- docs/release-deploy.md
- docs/no-source-portal-migration.md
EOF
exit 1