wechat-viewer-watchdog.sh
6.25 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
#!/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