diff --git a/deploy/redeploy-plugin.sh b/deploy/redeploy-plugin.sh index b50101e..063d828 100755 --- a/deploy/redeploy-plugin.sh +++ b/deploy/redeploy-plugin.sh @@ -32,6 +32,7 @@ set -uo pipefail REPO=${REPO:-/home/program/agentmail} +GATEWAY_DB=${GATEWAY_DB:-/opt/agentmail/data/agentmail.db} DEST_ROOT=${DEST_ROOT:-/opt/agentmail/plugins} STAGE_ONLY=0 PLUGIN="" @@ -73,6 +74,7 @@ say "部署 $PLUGIN-mail-bridge → $SNAP" [ -d "$SRC" ] || { bad "源目录不存在: $SRC"; exit 2; } [ -f "$SRC/package.json" ] || { bad "缺少 package.json: $SRC"; exit 2; } command -v systemctl >/dev/null 2>&1 || { bad "缺少 systemctl"; exit 2; } +[ -f "$GATEWAY_DB" ] || { bad "找不到网关库 $GATEWAY_DB(无法验证桥是否连上)"; exit 2; } if ! systemctl cat "$UNIT" >/dev/null 2>&1; then bad "找不到 systemd 单元 $UNIT(插件要先有一个运行宿主才能谈部署)"; exit 2 fi @@ -87,11 +89,20 @@ mkdir -p "$DEST" rm -rf "$STAGING" mkdir -p "$STAGING" -# 拷运行时需要的东西。测试与构建脚本不进生产快照 —— -# 快照的意义就是「冻结成一份能跑的东西」,不是把仓库复制一遍。 -for item in package.json lib src index.js; do - [ -e "$SRC/$item" ] && cp -a "$SRC/$item" "$STAGING/" -done +# 整包复制,再剔除开发用目录。 +# +# **不能白名单列出要拷什么。** 第一版白名单是 `package.json lib src index.js dist`, +# 漏掉了 dsh 的 `cordis.patch.yml`(dsh 读它做 overlay 配置)。后果不是"少个文件" +# 而是**服务起不来**,而且只在重启那一刻才暴露: +# +# Error: dsh: failed to read overlay .../dsh-mail-bridge/cordis.patch.yml: ENOENT +# +# 白名单的失败模式天生如此:漏一个就等着启动时炸,而启动时旧版本已经被换掉了。 +# 黑名单反过来 —— 默认带走,只排除明确不需要的。 +cp -a "$SRC/." "$STAGING/" +rm -rf "$STAGING/test" "$STAGING/.git" "$STAGING/node_modules/.cache" \ + "$STAGING/.DS_Store" "$STAGING/dist.old" 2>/dev/null +find "$STAGING" -maxdepth 1 -name '*.log' -delete 2>/dev/null # 依赖必须进快照:仓库外没有 node_modules 可借,缺了它入口根本起不来。 if [ -d "$SRC/node_modules" ]; then cp -a "$SRC/node_modules" "$STAGING/" @@ -167,16 +178,32 @@ rollback() { } # ── 6. 重启 + 后置验证 ───────────────────────────────────────── +# 记下重启时刻,后面用「网关库里的 last_seen 是否比它新」判断桥真的连上了。 +# **必须用 UTC。** `agents.last_seen` 是 UTC(SQLite 的 CURRENT_TIMESTAMP 语义), +# 而 `date` 默认给本地时间。拿本地时间(如 11:44)去比 UTC 值(03:44)会**永远为假**, +# 于是每一次部署都被判成"桥没连上"并回滚 —— 判据写错会让门禁主动破坏生产。 +RESTART_AT=$(date -u '+%Y-%m-%d %H:%M:%S') systemctl restart "$UNIT" || { bad "重启 $UNIT 失败"; rollback; exit 1; } -# 只看 is-active 不够:桥可能进程活着却没连上 Gateway(密钥过期、 -# Gateway 未起、依赖缺失在惰加载时才暴露)。必须以**日志出现「已接入」**为准。 -DEADLINE=$(( $(date +%s) + 45 )) -CONNECTED=0 +# 存活判据分两层: +# +# 1. `systemctl is-active` —— 进程在不在。**不够**:桥可能进程活着却没连上 +# Gateway(密钥过期、Gateway 未起、依赖在惰加载时才暴露)。 +# 2. **网关库里 `last_seen` 是否比重启时刻新** —— 平台无关,且是真的端到端 +# (桥 → 心跳 → 服务端落库)。这一条替代了第一版的「日志出现『已接入』」: +# 那句话只有 **pi 与 opencode** 会打印,dsh 启动时只输出 +# `dsh web: http://127.0.0.1:3080` —— 照那个判据,**一次成功的 dsh 部署 +# 会被判成失败并回滚**(实测就是这么把 dsh 弄进崩溃循环的:回滚到缺 +# cordis.patch.yml 的坏快照)。 +# +# 教训:判据里不要写某一个平台特有的措辞。 +command -v sqlite3 >/dev/null 2>&1 || { bad "缺少 sqlite3,无法验证桥是否连上网关"; rollback; exit 1; } +DEADLINE=$(( $(date +%s) + 90 )) +HEARTBEAT=0 while [ "$(date +%s)" -lt "$DEADLINE" ]; do - if journalctl -u "$UNIT" --since "-50 seconds" --no-pager 2>/dev/null | grep -q '已接入'; then - CONNECTED=1; break - fi + SEEN=$(sqlite3 "$GATEWAY_DB" \ + "SELECT count(*) FROM agents WHERE agent_name='$PLUGIN' AND last_seen > '$RESTART_AT';" 2>/dev/null) + if [ "${SEEN:-0}" != "0" ]; then HEARTBEAT=1; break; fi sleep 3 done @@ -184,13 +211,14 @@ ACTIVE=$(systemctl is-active "$UNIT" 2>/dev/null) say "后置验证" [ "$ACTIVE" = active ] && ok "$UNIT 处于 active" || bad "$UNIT 状态为 $ACTIVE" -if [ "$CONNECTED" = 1 ]; then - ok "桥日志出现「已接入」(连上 Gateway)" +if [ "$HEARTBEAT" = 1 ]; then + ok "网关收到 $PLUGIN 的新心跳(last_seen 晚于重启时刻)—— 桥真的连上了" else - bad "45 秒内未见「已接入」—— 插件可能起不来或连不上 Gateway" + bad "90 秒内网关未收到 $PLUGIN 的新心跳 —— 桥没连上(进程活着不等于连上了)" + info "诊断:journalctl -u $UNIT --since '-3 minutes' | tail -30" fi -if [ "$ACTIVE" != active ] || [ "$CONNECTED" != 1 ]; then +if [ "$ACTIVE" != active ] || [ "$HEARTBEAT" != 1 ]; then say "结论: 验证不过 —— 回滚,不要「先上着再修」" rollback exit 1