Commit 5e6434a68621e50f35c44811378573662dd2e08b
1 parent
71e983e7
feat(keepalive): restart-flag 桥 — claude-run 无 su 也能自助重启 bot
claude-run 跑在 proot、拿不到 Magisk root,杀不了 root 的 auto-reply 进程。 keepalive.sh 主循环新增:检测到 ~/hermes-wechat/.restart-request flag 就以 root pkill auto-reply 并删 flag,随后既有的 is_auto_reply_running 检测用新代码重启。 只暴露「请求重启」一个动作,不是任意命令执行。已端到端验证:u0_a327 touch flag → keepalive ~15s 内重启 bot。 - keepalive.sh: 纳入版本控制 + 加 flag 桥 - deploy-to-phone.sh: 收 keepalive.sh(.sh 给 755)+ 末尾加重启 keepalive 提示 - AGENTS.md / HANDOFF.md §零: 文档化 flag 重启途径 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Showing
4 changed files
with
190 additions
and
6 deletions
server/services/wechat/AGENTS.md
| ... | ... | @@ -12,7 +12,7 @@ Pixel 本机的微信自动回复机器人(账号「小火子」`wxid_51ap3m6p |
| 12 | 12 | |
| 13 | 13 | | 角色 | 跑在 | 能力 | 干什么 | |
| 14 | 14 | |------|------|------|--------| |
| 15 | -| **claude-run(Claude)= 脑子** | proot 容器 | 能编辑本目录 `*.js`、读 `auto-reply.log`、读改 `.whitelist.json`;**不能 su** | 写 / 改 / 审代码、看日志诊断 | | |
| 15 | +| **claude-run(Claude)= 脑子** | proot 容器 | 能编辑本目录 `*.js`、读 `auto-reply.log`、读改 `.whitelist.json`;**不能 su**,但可 `touch .restart-request` 让 keepalive 代为重启 | 写 / 改 / 审代码、看日志诊断 | | |
| 16 | 16 | | **hermes(Kimi)= 手** | Termux(能 su) | 能 `su -c` 拿 Magisk root | 跑需要 root 的设备操作:重启 bot、查微信 DB、发测试消息、logcat、重开 A11y | |
| 17 | 17 | |
| 18 | 18 | > claude-run 在 proot 里够不到 Magisk root,也看不到微信 DB(没挂载)。需要 root 的步骤交给 hermes 或 Termux shell 执行。 |
| ... | ... | @@ -34,8 +34,10 @@ Pixel 本机的微信自动回复机器人(账号「小火子」`wxid_51ap3m6p |
| 34 | 34 | ## 改代码 → 生效(内循环) |
| 35 | 35 | |
| 36 | 36 | 1. **改**:claude-run 直接编辑本目录 `*.js`。 |
| 37 | -2. **重启**(需 root → hermes / Termux 执行):`su -c 'pkill -f auto-reply.js'` → keepalive(每 15s 巡检)自动用新代码以 root 重启。 | |
| 38 | - 确认新进程:`su -c 'ps -ef | grep -E node.auto-reply.js | grep -v grep'`(**别用 `pgrep -f`,会自匹配 su shell**)。 | |
| 37 | +2. **重启**(让新代码生效): | |
| 38 | + - **claude-run(无 su)**:`touch ~/hermes-wechat/.restart-request` —— keepalive(每 15s 巡检、已是 root)看到这个 flag 就杀掉 auto-reply 并用新代码重启,~15s 内生效。**这是 claude-run 自助重启的唯一方式**(它在 proot 里杀不了 root 进程)。 | |
| 39 | + - **hermes / Termux(有 su)**:直接 `su -c 'pkill -f auto-reply.js'`,keepalive 随即重启。 | |
| 40 | + - 确认新进程:`su -c 'ps -ef | grep -E node.auto-reply.js | grep -v grep'`(**别用 `pgrep -f`,会自匹配 su shell**)。 | |
| 39 | 41 | 3. **看**:`tail -f ~/hermes-wechat/auto-reply.log`(claude-run 不带 su 也能读,日志 644)。 |
| 40 | 42 | 4. **同步回 Mac**:手机无 git,改完把改动同步回 Mac repo `server/services/wechat/`,否则下次 Mac 跑 `deploy-to-phone.sh` 会覆盖你的改动。 |
| 41 | 43 | ... | ... |
server/services/wechat/HANDOFF.md
| ... | ... | @@ -27,7 +27,10 @@ |
| 27 | 27 | |
| 28 | 28 | 4. **bot 实跑只依赖 3 个文件**:`auto-reply.js` + `android-wechat-tracker-sqlcipher.js` + `android-wechat-sender.js`(`auto-reply.js` 只 require 这俩)。其余 `index.js / wechat-tracker.js / wechat-reader.js / *-frida.js / *-sqlite.js` 都不参与 bot 运行,别被带偏。 |
| 29 | 29 | |
| 30 | -5. **改代码 → 生效**:`~/hermes-wechat/*.js` 现在属主是 u0_a327,你可以直接编辑。改完 `su -c 'pkill -f auto-reply.js'`,keepalive(每 15s 巡检)会以 root 自动用新代码重启。改完记得把改动同步回 Mac repo(见 §六)。 | |
| 30 | +5. **改代码 → 生效**:`~/hermes-wechat/*.js` 现在属主是 u0_a327,你可以直接编辑。重启让新代码生效: | |
| 31 | + - 有 su(hermes/Termux):`su -c 'pkill -f auto-reply.js'`,keepalive(每 15s 巡检)以 root 自动重启。 | |
| 32 | + - **无 su(proot 里的 claude-run):`touch ~/hermes-wechat/.restart-request`** —— keepalive 看到这个 flag 就代为杀掉并重启 bot(~15s 内生效)。这是 claude-run 自助重启的唯一途径(它杀不了 root 进程)。 | |
| 33 | + 改完记得把改动同步回 Mac repo(见 §六)。 | |
| 31 | 34 | |
| 32 | 35 | --- |
| 33 | 36 | ... | ... |
server/services/wechat/deploy-to-phone.sh
| ... | ... | @@ -20,7 +20,9 @@ TERMUX_UID="${TERMUX_UID:-u0_a327}" # Termux app 的 uid(claude-run 也是 |
| 20 | 20 | RUNTIME="auto-reply.js android-wechat-tracker-sqlcipher.js android-wechat-sender.js" |
| 21 | 21 | # 文档(接手 / 维护必读)。AGENTS.md/CLAUDE.md 是 agent 一次性读的入口 |
| 22 | 22 | DOCS="AGENTS.md CLAUDE.md HANDOFF.md README.md SKILL.md" |
| 23 | -FILES="$RUNTIME $DOCS" | |
| 23 | +# 守护脚本(改了要手动重启 keepalive,见末尾提示) | |
| 24 | +SCRIPTS="keepalive.sh" | |
| 25 | +FILES="$RUNTIME $DOCS $SCRIPTS" | |
| 24 | 26 | |
| 25 | 27 | # md5 工具:Mac 用 `md5 -q`,Linux 用 `md5sum` |
| 26 | 28 | md5_local() { |
| ... | ... | @@ -37,9 +39,10 @@ adb shell "su -c 'mkdir -p $PHONE_DIR'" |
| 37 | 39 | fail=0 |
| 38 | 40 | for f in $FILES; do |
| 39 | 41 | if [ ! -f "$SCRIPT_DIR/$f" ]; then echo "SKIP (missing) $f"; continue; fi |
| 42 | + case "$f" in *.sh) mode=755;; *) mode=644;; esac | |
| 40 | 43 | echo "Pushing $f ..." |
| 41 | 44 | adb push "$SCRIPT_DIR/$f" "/sdcard/_deploy_$f" >/dev/null |
| 42 | - adb shell "su -c 'dd if=/sdcard/_deploy_$f of=$PHONE_DIR/$f 2>/dev/null && chown $TERMUX_UID:$TERMUX_UID $PHONE_DIR/$f && chmod 644 $PHONE_DIR/$f'" | |
| 45 | + adb shell "su -c 'dd if=/sdcard/_deploy_$f of=$PHONE_DIR/$f 2>/dev/null && chown $TERMUX_UID:$TERMUX_UID $PHONE_DIR/$f && chmod $mode $PHONE_DIR/$f'" | |
| 43 | 46 | adb shell "rm -f /sdcard/_deploy_$f" >/dev/null 2>&1 || true |
| 44 | 47 | lmd5=$(md5_local "$SCRIPT_DIR/$f") |
| 45 | 48 | pmd5=$(adb shell "su -c 'md5sum $PHONE_DIR/$f'" | awk '{print $1}' | tr -d '\r') |
| ... | ... | @@ -55,3 +58,7 @@ echo "确认新进程起来了(不能用 pgrep -f,会自匹配 su shell) |
| 55 | 58 | echo " adb shell \"su -c 'ps -ef | grep -E node.auto-reply.js | grep -v grep'\"" |
| 56 | 59 | echo "看 live 日志(注意:唯一真日志在 hermes-wechat/,别处副本是死的):" |
| 57 | 60 | echo " adb shell \"su -c 'tail -f $PHONE_DIR/auto-reply.log'\"" |
| 61 | +echo "" | |
| 62 | +echo "若改了 keepalive.sh,需重启守护进程才生效(它以 u0_a327 跑):" | |
| 63 | +echo " adb shell \"su -c 'pkill -f keepalive.sh'\" # 杀掉旧的" | |
| 64 | +echo " adb shell \"su u0_a327 -c 'cd $PHONE_DIR && export PATH=/data/data/com.termux/files/usr/bin:\\\$PATH HOME=/data/data/com.termux/files/home; nohup bash keepalive.sh > keepalive.log 2>&1 &'\"" | ... | ... |
server/services/wechat/keepalive.sh
0 → 100644
| 1 | +#!/data/data/com.termux/files/usr/bin/bash | |
| 2 | +# keepalive.sh — Pixel 微信自动回复保活脚本 | |
| 3 | +# | |
| 4 | +# 守护以下进程,挂了就重启: | |
| 5 | +# 1. termux wake-lock — 防 Doze 冻结 Termux:API(通知监听依赖它) | |
| 6 | +# 2. HermesAccessibilityService — A11y 易被系统/更新关掉,发现 enabled_accessibility_services=null 就重启 | |
| 7 | +# 3. hermes-agent gateway — :8642,LLM 网关 | |
| 8 | +# 4. auto-reply.js — 微信回复主程序 | |
| 9 | +# | |
| 10 | +# 用法(在 Termux 用户 shell 里运行;不要在 root shell 起,wake-lock 拿不住 Termux 进程): | |
| 11 | +# nohup bash ~/hermes-wechat/keepalive.sh > ~/hermes-wechat/keepalive.log 2>&1 & | |
| 12 | +# | |
| 13 | +# 前置条件(一次性设置): | |
| 14 | +# 1. Android 设置 → 应用 → Termux / Termux:API / 微信 → 电池优化 → 不优化 | |
| 15 | +# 2. Termux:API 已安装,并已授予通知访问权限 | |
| 16 | +# 3. lsposed-wechat-sendmsg APK 已安装(含 HermesAccessibilityService) | |
| 17 | +# 4. ~/hermes-wechat/start-ar.sh 存在并已设置 MY_WXID / MY_NICKNAME | |
| 18 | + | |
| 19 | +export PATH="/data/data/com.termux/files/usr/bin:$PATH" | |
| 20 | +export HOME="/data/data/com.termux/files/home" | |
| 21 | + | |
| 22 | +HERMES_DIR="${HERMES_DIR:-$HOME/hermes-wechat}" | |
| 23 | +HERMES_PORT="${HERMES_PORT:-8642}" | |
| 24 | +GATEWAY_START="${GATEWAY_START:-$HOME/.hermes/scripts/start-gateway.sh}" | |
| 25 | +A11Y_SVC="com.hermes.wechat.xposed/com.hermes.wechat.xposed.accessibility.HermesAccessibilityService" | |
| 26 | +CHECK_INTERVAL_SEC="${CHECK_INTERVAL_SEC:-15}" | |
| 27 | +# auto-reply 启动用的 env(避开 start-ar.sh 的 shell hook 污染) | |
| 28 | +MY_WXID="${MY_WXID:-wxid_51ap3m6p9xs722}" | |
| 29 | +MY_NICKNAME="${MY_NICKNAME:-小火子}" | |
| 30 | +HERMES_TIMEOUT_MS="${HERMES_TIMEOUT_MS:-600000}" | |
| 31 | +HISTORY_CONTEXT_SIZE="${HISTORY_CONTEXT_SIZE:-8}" | |
| 32 | + | |
| 33 | +log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*"; } | |
| 34 | + | |
| 35 | +# ── 1. Termux wake-lock ────────────────────────────────────────── | |
| 36 | +ensure_wake_lock() { | |
| 37 | + # 用命令行 termux-wake-lock(依赖 Termux:API app);am startservice 是备用 | |
| 38 | + if command -v termux-wake-lock >/dev/null 2>&1; then | |
| 39 | + termux-wake-lock 2>/dev/null | |
| 40 | + else | |
| 41 | + # 后备:用 su 调系统 ServiceManager | |
| 42 | + su -c "am startservice --user 0 -a com.termux.service_wake_lock com.termux/.app.TermuxService" 2>/dev/null || true | |
| 43 | + fi | |
| 44 | +} | |
| 45 | + | |
| 46 | +is_wake_lock_held() { | |
| 47 | + su -c "dumpsys power 2>/dev/null | grep -q 'termux:service-wakelock'" | |
| 48 | +} | |
| 49 | + | |
| 50 | +# ── 2. A11y service ────────────────────────────────────────────── | |
| 51 | +is_a11y_enabled() { | |
| 52 | + out=$(su -c "settings get secure enabled_accessibility_services" 2>/dev/null) | |
| 53 | + case "$out" in | |
| 54 | + *com.hermes.wechat.xposed*HermesAccessibilityService*) return 0 ;; | |
| 55 | + *) return 1 ;; | |
| 56 | + esac | |
| 57 | +} | |
| 58 | + | |
| 59 | +enable_a11y() { | |
| 60 | + log "Enabling HermesAccessibilityService..." | |
| 61 | + su -c "settings put secure enabled_accessibility_services $A11Y_SVC; settings put secure accessibility_enabled 1" 2>/dev/null | |
| 62 | +} | |
| 63 | + | |
| 64 | +# ── 3. hermes gateway ──────────────────────────────────────────── | |
| 65 | +is_gateway_running() { | |
| 66 | + # 用 /sys/class/net 风格的端口探测:通过 nc 或 curl,二选一 | |
| 67 | + if command -v curl >/dev/null 2>&1; then | |
| 68 | + # gateway 在 termux 用户下需要 Clash 代理(与本机回环无关); | |
| 69 | + # 但 127.0.0.1:8642 走本机,不用代理 | |
| 70 | + env -u HTTPS_PROXY -u HTTP_PROXY \ | |
| 71 | + curl -s --max-time 3 "http://127.0.0.1:${HERMES_PORT}/v1/models" >/dev/null 2>&1 | |
| 72 | + else | |
| 73 | + # nc fallback | |
| 74 | + echo > /dev/tcp/127.0.0.1/$HERMES_PORT 2>/dev/null | |
| 75 | + fi | |
| 76 | +} | |
| 77 | + | |
| 78 | +start_gateway() { | |
| 79 | + log "Starting hermes gateway..." | |
| 80 | + if [ -x "$GATEWAY_START" ]; then | |
| 81 | + bash "$GATEWAY_START" >> "$HOME/.hermes/logs/gateway-start.log" 2>&1 | |
| 82 | + else | |
| 83 | + log "[ERROR] $GATEWAY_START not found" | |
| 84 | + fi | |
| 85 | +} | |
| 86 | + | |
| 87 | +# ── 4. auto-reply.js ───────────────────────────────────────────── | |
| 88 | +AR_PID_FILE="$HERMES_DIR/.auto-reply.pid" | |
| 89 | + | |
| 90 | +is_auto_reply_running() { | |
| 91 | + # 不能用 pgrep -f node.*auto-reply.js — 它会自匹配 su shell 自己的 cmdline。 | |
| 92 | + # 用 ps -ef + grep 精确匹配 'node auto-reply.js'(含空格,排除 sh wrapper)。 | |
| 93 | + # su 提权因为 auto-reply 跑在 root 下,u0_a327 看不到 root 进程的 ps。 | |
| 94 | + # 用 . 占位(任何单字符)避开嵌套引号问题 | |
| 95 | + su -c "ps -ef 2>/dev/null | grep -E node.auto-reply.js | grep -v sh.-c | grep -v grep" \ | |
| 96 | + 2>/dev/null | grep -q . | |
| 97 | +} | |
| 98 | + | |
| 99 | +start_auto_reply() { | |
| 100 | + log "Starting auto-reply..." | |
| 101 | + # 不走 start-ar.sh wrapper(其 shebang 走 dash 后会触发 phone 上某个 | |
| 102 | + # claude-run/hermes 会话的 shell snapshot hook,污染 hermes-wechat/*.js)。 | |
| 103 | + # 直接以 root 起 node + 内联 env,跨过那个 shell session。 | |
| 104 | + # 起完后用 pgrep + grep 严格匹配 cmdline 确切包含 'node auto-reply.js' | |
| 105 | + # (不能用 pgrep -f 因为正则 .* 会自匹配 su shell 本身),写入 PID 文件 | |
| 106 | + su -c "cd $HERMES_DIR && \ | |
| 107 | + MY_WXID=${MY_WXID:-wxid_51ap3m6p9xs722} \ | |
| 108 | + MY_NICKNAME=\"${MY_NICKNAME:-小火子}\" \ | |
| 109 | + HERMES_HOST=127.0.0.1 \ | |
| 110 | + HERMES_PORT=$HERMES_PORT \ | |
| 111 | + HERMES_TIMEOUT=${HERMES_TIMEOUT_MS:-600000} \ | |
| 112 | + NOTIFICATION_ENABLED=true \ | |
| 113 | + HISTORY_CONTEXT_SIZE=${HISTORY_CONTEXT_SIZE:-8} \ | |
| 114 | + nohup /data/data/com.termux/files/usr/bin/node auto-reply.js \ | |
| 115 | + > $HERMES_DIR/auto-reply.log 2>&1 < /dev/null & | |
| 116 | + echo \$! > $AR_PID_FILE" | |
| 117 | + sleep 2 | |
| 118 | + if [ -f "$AR_PID_FILE" ]; then | |
| 119 | + log "Auto-reply started PID=$(cat $AR_PID_FILE 2>/dev/null)" | |
| 120 | + fi | |
| 121 | +} | |
| 122 | + | |
| 123 | +# ── 5. 主循环 ──────────────────────────────────────────────────── | |
| 124 | +log "=== Keepalive started ===" | |
| 125 | +log "HERMES_DIR=$HERMES_DIR GATEWAY_START=$GATEWAY_START" | |
| 126 | +log "MY_WXID=$MY_WXID MY_NICKNAME=$MY_NICKNAME HERMES_TIMEOUT=${HERMES_TIMEOUT_MS}ms" | |
| 127 | +log "Check interval: ${CHECK_INTERVAL_SEC}s" | |
| 128 | + | |
| 129 | +# 首次进入:把所有依赖拉到位 | |
| 130 | +ensure_wake_lock | |
| 131 | +is_a11y_enabled || enable_a11y | |
| 132 | +is_gateway_running || start_gateway | |
| 133 | +sleep 5 | |
| 134 | +is_auto_reply_running || start_auto_reply | |
| 135 | + | |
| 136 | +while true; do | |
| 137 | + sleep "$CHECK_INTERVAL_SEC" | |
| 138 | + | |
| 139 | + # wake-lock 偶尔会因 Termux 重启 / 系统重启而丢,保险起见周期性 re-acquire | |
| 140 | + if ! is_wake_lock_held; then | |
| 141 | + log "Wake lock lost, re-acquiring..." | |
| 142 | + ensure_wake_lock | |
| 143 | + fi | |
| 144 | + | |
| 145 | + # A11y 易被系统升级 / 用户在设置中关掉 | |
| 146 | + if ! is_a11y_enabled; then | |
| 147 | + log "A11y service disabled, re-enabling..." | |
| 148 | + enable_a11y | |
| 149 | + fi | |
| 150 | + | |
| 151 | + if ! is_gateway_running; then | |
| 152 | + log "Gateway not responding on :$HERMES_PORT, restarting..." | |
| 153 | + start_gateway | |
| 154 | + sleep 6 | |
| 155 | + fi | |
| 156 | + | |
| 157 | + # ── restart-flag 桥:让 claude-run(proot 里够不到 Magisk root)也能触发重启 ── | |
| 158 | + # claude-run 改完 *.js 后 `touch ~/hermes-wechat/.restart-request`,keepalive(已是 root | |
| 159 | + # 能力)看到就杀掉 auto-reply,下面的 is_auto_reply_running 检测会随即用新代码重启。 | |
| 160 | + # 只暴露「请求重启」这一个动作,不是任意命令执行。 | |
| 161 | + if [ -f "$HERMES_DIR/.restart-request" ]; then | |
| 162 | + log "Restart requested via .restart-request flag, killing auto-reply..." | |
| 163 | + rm -f "$HERMES_DIR/.restart-request" | |
| 164 | + su -c "pkill -f auto-reply.js" 2>/dev/null | |
| 165 | + sleep 1 | |
| 166 | + fi | |
| 167 | + | |
| 168 | + if ! is_auto_reply_running; then | |
| 169 | + log "auto-reply.js not running, restarting..." | |
| 170 | + start_auto_reply | |
| 171 | + fi | |
| 172 | +done | ... | ... |