wechat-viewer-watchdog.sh 6.25 KB
#!/data/data/com.termux/files/usr/bin/bash
# WeChat Viewer 看门狗 — 检查 Flask + Hermes Gateway + 微信 Watcher 是否存活
# 由 Hermes cron 每 5 分钟调用一次

export PATH=/data/data/com.termux/files/usr/bin:$PATH

FLASK_PORT=5001
APP_DIR=/data/data/com.termux/files/home/wechat-viewer
LOG=$APP_DIR/watchdog.log
HOME_DIR=/data/data/com.termux/files/home

# ── 1. Flask ──────────────────────────────────────

ALIVE=$(su -c "ss -tlnp" 2>/dev/null | grep -c ":$FLASK_PORT")

if [ "$ALIVE" -eq 0 ]; then
    echo "[$(date)] Flask 不在线,重启中..." | tee -a "$LOG"
    cd "$APP_DIR"
    pkill -f 'python3 app.py' 2>/dev/null
    sleep 1
    fuser -k "$FLASK_PORT/tcp" 2>/dev/null
    nohup python3 app.py > $APP_DIR/flask.log 2>&1 &
    sleep 3
    ALIVE2=$(su -c "ss -tlnp" 2>/dev/null | grep -c ":$FLASK_PORT")
    if [ "$ALIVE2" -gt 0 ]; then
        echo "[$(date)] Flask 重启成功" | tee -a "$LOG"
    else
        echo "[$(date)] Flask 重启失败!" | tee -a "$LOG"
    fi
fi

# ── 2. Hermes Gateway ──────────────────────────────

GATEWAY_ALIVE=$(pgrep -f "hermes.*gateway" 2>/dev/null | wc -l)

if [ "$GATEWAY_ALIVE" -eq 0 ]; then
    echo "[$(date)] Gateway 不在线,重启中..." | tee -a "$LOG"
    nohup hermes gateway > /tmp/hermes-gateway.log 2>&1 &
    sleep 3
    GATEWAY_ALIVE2=$(pgrep -f "hermes.*gateway" 2>/dev/null | wc -l)
    if [ "$GATEWAY_ALIVE2" -gt 0 ]; then
        echo "[$(date)] Gateway 重启成功" | tee -a "$LOG"
    else
        echo "[$(date)] Gateway 重启失败!" | tee -a "$LOG"
    fi
fi

# ── 3. Stuck Job Reaper ──────────────────────────
# 清理因 Flask 重启而孤儿化的 hermes_jobs
python3 "$APP_DIR/reap_stuck_jobs.py" >> "$LOG" 2>&1

# ── 4. Orphan Report Reaper ─────────────────────
# 清理因 Flask 重启丢回调而未入库的日报文件
python3 "$APP_DIR/reap_orphan_reports.py" >> "$LOG" 2>&1

# ── 5. 微信自动回复 Watcher ────────────────────────
# 监听微信 DB 变化,写入 incoming.jsonl 队列
# 单例保护:判活用 pidfile+kill -0(不是 `ps -A | grep`)——2026-08-07 实测
# `su -c 'ps -A'` 经 adb/su 转一手会偶发返回空/截断,哪怕进程明明还活着,
# 导致看门狗把健康进程当挂了、每个 cron 周期都重置一次("重置后存活数异常"
# 的假象),既白白重启浪费资源,也在重启的 1-2 秒空窗里可能漏消息。
# 重置动作本身仍先 pkill 清场再起(不只信 pidfile 干净),避免卡死的旧实例
# 没被 pidfile 反映出来、跟新实例叠加——2026-07-25 事故就是堆到 877 个进程
# 吃满内存+swap,这条防线不能丢。

REPLY_DIR=$HOME_DIR/.hermes/wechat_reply
WATCHER_PID_FILE=$REPLY_DIR/watcher.pid
WATCHER_ALIVE=0

if [ -f "$WATCHER_PID_FILE" ]; then
    WPID=$(cat "$WATCHER_PID_FILE" 2>/dev/null)
    if [ -n "$WPID" ] && su -c "test -r /proc/$WPID/cmdline && grep -q wechat_reply_watcher /proc/$WPID/cmdline" 2>/dev/null; then
        WATCHER_ALIVE=1
    fi
fi

if [ "$WATCHER_ALIVE" -ne 1 ]; then
    echo "[$(date)] 微信 Watcher 未存活(pidfile 检测),重置中..." | tee -a "$LOG"
    su -c "pkill -f wechat_reply_watcher.py" 2>/dev/null
    sleep 1
    rm -f "$WATCHER_PID_FILE"
    su -c "/data/data/com.termux/files/usr/bin/python3 $REPLY_DIR/wechat_reply_watcher.py >> $REPLY_DIR/watcher.log 2>&1 &"
    sleep 2
    WPID2=$(cat "$WATCHER_PID_FILE" 2>/dev/null)
    if [ -n "$WPID2" ] && su -c "test -r /proc/$WPID2/cmdline" 2>/dev/null; then
        echo "[$(date)] 微信 Watcher 重置成功(pid=$WPID2)" | tee -a "$LOG"
    else
        echo "[$(date)] 微信 Watcher 重置后仍未存活" | tee -a "$LOG"
    fi
fi

# ── 6. 微信自动回复 Orchestrator Drain ──────────────
# 消费 incoming.jsonl 队列,处理候选事件

ORCH_LOCK=$REPLY_DIR/orch.lock
# 清理超过 5 分钟的残留锁
if [ -f "$ORCH_LOCK" ]; then
    LOCK_AGE=$(($(date +%s) - $(stat -c %Y "$ORCH_LOCK" 2>/dev/null || echo 0)))
    if [ "$LOCK_AGE" -gt 300 ]; then
        rm -f "$ORCH_LOCK"
    fi
fi

su -c "/data/data/com.termux/files/usr/bin/python3 $REPLY_DIR/wechat_reply_orchestrator.py --drain >> $REPLY_DIR/orchestrator.log 2>&1"

# ── 7. task.hexdo.com 隧道 ──────────────────────────
# Cloudflare 隧道 pixel-wechat-viewer(→ 127.0.0.1:5001),用 termux 用户身份跑,
# 不需要 su。2026-08-07 之前这里完全没人巡检——本地 Flask 一直活着,但对外的
# task.hexdo.com 挂了一周多都没被发现,靠这次手动排查才补上。
# 详见 pixelphone 仓库 memory task-hexdo-reverse-proxy-outage.md。

CF_DIR=$HOME_DIR/.cloudflared
TUNNEL_ALIVE=$(pgrep -f "cloudflared tunnel --config.*pixel-wechat-viewer" 2>/dev/null | wc -l)

if [ "$TUNNEL_ALIVE" -eq 0 ]; then
    echo "[$(date)] cloudflared 隧道进程不在,启动中..." | tee -a "$LOG"
    nohup cloudflared tunnel --config "$CF_DIR/config.yml" run pixel-wechat-viewer \
        > "$CF_DIR/tunnel.log" 2>&1 &
    sleep 5
fi

# 进程在不代表隧道通(连接可能卡死/边缘握手失败),实测公网端点才算数
PUBLIC_CODE=$(curl -sS -m 8 -o /dev/null -w '%{http_code}' https://task.hexdo.com/api/doctor 2>/dev/null)

if [ "$PUBLIC_CODE" != "200" ]; then
    echo "[$(date)] task.hexdo.com 探测异常(http_code=$PUBLIC_CODE),重启隧道..." | tee -a "$LOG"
    pkill -f 'cloudflared tunnel --config.*pixel-wechat-viewer' 2>/dev/null
    sleep 1
    nohup cloudflared tunnel --config "$CF_DIR/config.yml" run pixel-wechat-viewer \
        > "$CF_DIR/tunnel.log" 2>&1 &
    sleep 8
    PUBLIC_CODE2=$(curl -sS -m 8 -o /dev/null -w '%{http_code}' https://task.hexdo.com/api/doctor 2>/dev/null)
    if [ "$PUBLIC_CODE2" = "200" ]; then
        echo "[$(date)] task.hexdo.com 隧道重启后恢复" | tee -a "$LOG"
    else
        echo "[$(date)] task.hexdo.com 隧道重启后仍异常(http_code=$PUBLIC_CODE2),需要人工看一下 $CF_DIR/tunnel.log(Mac 上 bridge.hexdo.com 是另一条独立隧道,不受影响)" | tee -a "$LOG"
    fi
fi