Commit 3d4fb0d1679343cde9b7be38dab6cebd04730feb

Authored by yibo
1 parent 0a537853

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>
server/services/wechat/AGENTS.md
@@ -12,7 +12,7 @@ Pixel 本机的微信自动回复机器人(账号「小火子」`wxid_51ap3m6p @@ -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 | **hermes(Kimi)= 手** | Termux(能 su) | 能 `su -c` 拿 Magisk root | 跑需要 root 的设备操作:重启 bot、查微信 DB、发测试消息、logcat、重开 A11y | 16 | **hermes(Kimi)= 手** | Termux(能 su) | 能 `su -c` 拿 Magisk root | 跑需要 root 的设备操作:重启 bot、查微信 DB、发测试消息、logcat、重开 A11y |
17 17
18 > claude-run 在 proot 里够不到 Magisk root,也看不到微信 DB(没挂载)。需要 root 的步骤交给 hermes 或 Termux shell 执行。 18 > claude-run 在 proot 里够不到 Magisk root,也看不到微信 DB(没挂载)。需要 root 的步骤交给 hermes 或 Termux shell 执行。
@@ -34,8 +34,10 @@ Pixel 本机的微信自动回复机器人(账号「小火子」`wxid_51ap3m6p @@ -34,8 +34,10 @@ Pixel 本机的微信自动回复机器人(账号「小火子」`wxid_51ap3m6p
34 ## 改代码 → 生效(内循环) 34 ## 改代码 → 生效(内循环)
35 35
36 1. **改**:claude-run 直接编辑本目录 `*.js`。 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 3. **看**:`tail -f ~/hermes-wechat/auto-reply.log`(claude-run 不带 su 也能读,日志 644)。 41 3. **看**:`tail -f ~/hermes-wechat/auto-reply.log`(claude-run 不带 su 也能读,日志 644)。
40 4. **同步回 Mac**:手机无 git,改完把改动同步回 Mac repo `server/services/wechat/`,否则下次 Mac 跑 `deploy-to-phone.sh` 会覆盖你的改动。 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,7 +27,10 @@
27 27
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 运行,别被带偏。 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=&quot;${TERMUX_UID:-u0_a327}&quot; # Termux app 的 uid(claude-run 也是 @@ -20,7 +20,9 @@ TERMUX_UID=&quot;${TERMUX_UID:-u0_a327}&quot; # Termux app 的 uid(claude-run 也是
20 RUNTIME="auto-reply.js android-wechat-tracker-sqlcipher.js android-wechat-sender.js" 20 RUNTIME="auto-reply.js android-wechat-tracker-sqlcipher.js android-wechat-sender.js"
21 # 文档(接手 / 维护必读)。AGENTS.md/CLAUDE.md 是 agent 一次性读的入口 21 # 文档(接手 / 维护必读)。AGENTS.md/CLAUDE.md 是 agent 一次性读的入口
22 DOCS="AGENTS.md CLAUDE.md HANDOFF.md README.md SKILL.md" 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 # md5 工具:Mac 用 `md5 -q`,Linux 用 `md5sum` 27 # md5 工具:Mac 用 `md5 -q`,Linux 用 `md5sum`
26 md5_local() { 28 md5_local() {
@@ -37,9 +39,10 @@ adb shell &quot;su -c &#39;mkdir -p $PHONE_DIR&#39;&quot; @@ -37,9 +39,10 @@ adb shell &quot;su -c &#39;mkdir -p $PHONE_DIR&#39;&quot;
37 fail=0 39 fail=0
38 for f in $FILES; do 40 for f in $FILES; do
39 if [ ! -f "$SCRIPT_DIR/$f" ]; then echo "SKIP (missing) $f"; continue; fi 41 if [ ! -f "$SCRIPT_DIR/$f" ]; then echo "SKIP (missing) $f"; continue; fi
  42 + case "$f" in *.sh) mode=755;; *) mode=644;; esac
40 echo "Pushing $f ..." 43 echo "Pushing $f ..."
41 adb push "$SCRIPT_DIR/$f" "/sdcard/_deploy_$f" >/dev/null 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 adb shell "rm -f /sdcard/_deploy_$f" >/dev/null 2>&1 || true 46 adb shell "rm -f /sdcard/_deploy_$f" >/dev/null 2>&1 || true
44 lmd5=$(md5_local "$SCRIPT_DIR/$f") 47 lmd5=$(md5_local "$SCRIPT_DIR/$f")
45 pmd5=$(adb shell "su -c 'md5sum $PHONE_DIR/$f'" | awk '{print $1}' | tr -d '\r') 48 pmd5=$(adb shell "su -c 'md5sum $PHONE_DIR/$f'" | awk '{print $1}' | tr -d '\r')
@@ -55,3 +58,7 @@ echo &quot;确认新进程起来了(不能用 pgrep -f,会自匹配 su shell) @@ -55,3 +58,7 @@ echo &quot;确认新进程起来了(不能用 pgrep -f,会自匹配 su shell)
55 echo " adb shell \"su -c 'ps -ef | grep -E node.auto-reply.js | grep -v grep'\"" 58 echo " adb shell \"su -c 'ps -ef | grep -E node.auto-reply.js | grep -v grep'\""
56 echo "看 live 日志(注意:唯一真日志在 hermes-wechat/,别处副本是死的):" 59 echo "看 live 日志(注意:唯一真日志在 hermes-wechat/,别处副本是死的):"
57 echo " adb shell \"su -c 'tail -f $PHONE_DIR/auto-reply.log'\"" 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