fix(deploy): 快照改整包复制 + 存活判据改看网关心跳 —— 修两个会毁掉生产的缺陷
切换三个桥时这两个缺陷都真的触发了,记下来避免重犯。
# 缺陷一:白名单拷贝漏文件 → 服务直接起不来
第一版 staging 用白名单:`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
白名单的失败模式天生如此:**默认不带**,漏一个就等启动时炸,而那时旧版本已经被换掉。
改为整包复制 + 剔除明确不需要的(`test/`、`.git`、`node_modules/.cache`、`*.log`)。
# 缺陷二:存活判据写成了某一个平台特有的措辞
第一版后置验证 grep 日志里的「已接入」。那句话**只有 pi 与 opencode 会打印**,
dsh 启动时只输出 `dsh web: http://127.0.0.1:3080` —— 于是**一次成功的 dsh 部署
被判成失败**,脚本按设计回滚……回滚到了缺 cordis.patch.yml 的坏快照,
把 dsh 推进崩溃循环。
改为查网关库(平台无关,且是真的端到端):
SELECT count(*) FROM agents WHERE agent_name='$PLUGIN' AND last_seen > '$RESTART_AT'
**时刻必须用 UTC**:`agents.last_seen` 是 UTC(CURRENT_TIMESTAMP 语义),
而 `date` 默认给本地时间 —— 拿 11:44 去比 03:44 会永远为假,于是每次部署都被判成
「桥没连上」并回滚,**门禁主动破坏生产**。已用 `date -u`。
判据双向验证过:以「3 分钟前」为重启时刻 → 命中 1;以未来时刻 → 命中 0。
# 本次切换结果
pi /opt/agentmail/plugins/pi-mail-bridge/current/src/index.mjs
opencode /opt/agentmail/plugins/opencode-mail-bridge/current/index.js
dsh /opt/agentmail/plugins/dsh-mail-bridge/current/dist/index.js
三处配置已迁移(备份在 /root/config-backups/pre-snapshot-20260912-113826/):
pi 的 unit、opencode.jsonc 的 plugin 项、dsh profile 的 link:(含 pnpm install 重建软链)。
验证:三桥进程**打开仓库文件数均为 0**;快照与仓库文件 inode 不同(独立副本);
四个 Agent 心跳新鲜;dsh 读 cordis.patch.yml 走的就是该软链(坏快照时它起不来,
好快照时它 active —— 这条是最硬的证据)。
This commit is contained in:
@ -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
|
||||
|
||||
Reference in New Issue
Block a user