#!/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