From 5e6434a68621e50f35c44811378573662dd2e08b Mon Sep 17 00:00:00 2001 From: yibo Date: Mon, 25 May 2026 19:18:11 +0800 Subject: [PATCH] feat(keepalive): restart-flag 桥 — claude-run 无 su 也能自助重启 bot --- server/services/wechat/AGENTS.md | 8 +++++--- server/services/wechat/HANDOFF.md | 5 ++++- server/services/wechat/deploy-to-phone.sh | 11 +++++++++-- server/services/wechat/keepalive.sh | 172 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 190 insertions(+), 6 deletions(-) create mode 100644 server/services/wechat/keepalive.sh diff --git a/server/services/wechat/AGENTS.md b/server/services/wechat/AGENTS.md index 728ecc4..b0dc642 100644 --- a/server/services/wechat/AGENTS.md +++ b/server/services/wechat/AGENTS.md @@ -12,7 +12,7 @@ Pixel 本机的微信自动回复机器人(账号「小火子」`wxid_51ap3m6p | 角色 | 跑在 | 能力 | 干什么 | |------|------|------|--------| -| **claude-run(Claude)= 脑子** | proot 容器 | 能编辑本目录 `*.js`、读 `auto-reply.log`、读改 `.whitelist.json`;**不能 su** | 写 / 改 / 审代码、看日志诊断 | +| **claude-run(Claude)= 脑子** | proot 容器 | 能编辑本目录 `*.js`、读 `auto-reply.log`、读改 `.whitelist.json`;**不能 su**,但可 `touch .restart-request` 让 keepalive 代为重启 | 写 / 改 / 审代码、看日志诊断 | | **hermes(Kimi)= 手** | Termux(能 su) | 能 `su -c` 拿 Magisk root | 跑需要 root 的设备操作:重启 bot、查微信 DB、发测试消息、logcat、重开 A11y | > claude-run 在 proot 里够不到 Magisk root,也看不到微信 DB(没挂载)。需要 root 的步骤交给 hermes 或 Termux shell 执行。 @@ -34,8 +34,10 @@ Pixel 本机的微信自动回复机器人(账号「小火子」`wxid_51ap3m6p ## 改代码 → 生效(内循环) 1. **改**:claude-run 直接编辑本目录 `*.js`。 -2. **重启**(需 root → hermes / Termux 执行):`su -c 'pkill -f auto-reply.js'` → keepalive(每 15s 巡检)自动用新代码以 root 重启。 - 确认新进程:`su -c 'ps -ef | grep -E node.auto-reply.js | grep -v grep'`(**别用 `pgrep -f`,会自匹配 su shell**)。 +2. **重启**(让新代码生效): + - **claude-run(无 su)**:`touch ~/hermes-wechat/.restart-request` —— keepalive(每 15s 巡检、已是 root)看到这个 flag 就杀掉 auto-reply 并用新代码重启,~15s 内生效。**这是 claude-run 自助重启的唯一方式**(它在 proot 里杀不了 root 进程)。 + - **hermes / Termux(有 su)**:直接 `su -c 'pkill -f auto-reply.js'`,keepalive 随即重启。 + - 确认新进程:`su -c 'ps -ef | grep -E node.auto-reply.js | grep -v grep'`(**别用 `pgrep -f`,会自匹配 su shell**)。 3. **看**:`tail -f ~/hermes-wechat/auto-reply.log`(claude-run 不带 su 也能读,日志 644)。 4. **同步回 Mac**:手机无 git,改完把改动同步回 Mac repo `server/services/wechat/`,否则下次 Mac 跑 `deploy-to-phone.sh` 会覆盖你的改动。 diff --git a/server/services/wechat/HANDOFF.md b/server/services/wechat/HANDOFF.md index f8a7598..f7ea1c8 100644 --- a/server/services/wechat/HANDOFF.md +++ b/server/services/wechat/HANDOFF.md @@ -27,7 +27,10 @@ 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 运行,别被带偏。 -5. **改代码 → 生效**:`~/hermes-wechat/*.js` 现在属主是 u0_a327,你可以直接编辑。改完 `su -c 'pkill -f auto-reply.js'`,keepalive(每 15s 巡检)会以 root 自动用新代码重启。改完记得把改动同步回 Mac repo(见 §六)。 +5. **改代码 → 生效**:`~/hermes-wechat/*.js` 现在属主是 u0_a327,你可以直接编辑。重启让新代码生效: + - 有 su(hermes/Termux):`su -c 'pkill -f auto-reply.js'`,keepalive(每 15s 巡检)以 root 自动重启。 + - **无 su(proot 里的 claude-run):`touch ~/hermes-wechat/.restart-request`** —— keepalive 看到这个 flag 就代为杀掉并重启 bot(~15s 内生效)。这是 claude-run 自助重启的唯一途径(它杀不了 root 进程)。 + 改完记得把改动同步回 Mac repo(见 §六)。 --- diff --git a/server/services/wechat/deploy-to-phone.sh b/server/services/wechat/deploy-to-phone.sh index 6fa6073..90053f9 100755 --- a/server/services/wechat/deploy-to-phone.sh +++ b/server/services/wechat/deploy-to-phone.sh @@ -20,7 +20,9 @@ TERMUX_UID="${TERMUX_UID:-u0_a327}" # Termux app 的 uid(claude-run 也是å® RUNTIME="auto-reply.js android-wechat-tracker-sqlcipher.js android-wechat-sender.js" # 文档(接手 / 维护必读)。AGENTS.md/CLAUDE.md 是 agent 一次性读的入口 DOCS="AGENTS.md CLAUDE.md HANDOFF.md README.md SKILL.md" -FILES="$RUNTIME $DOCS" +# 守护脚本(改了要手动重启 keepalive,见末尾提示) +SCRIPTS="keepalive.sh" +FILES="$RUNTIME $DOCS $SCRIPTS" # md5 工具:Mac 用 `md5 -q`,Linux 用 `md5sum` md5_local() { @@ -37,9 +39,10 @@ adb shell "su -c 'mkdir -p $PHONE_DIR'" fail=0 for f in $FILES; do if [ ! -f "$SCRIPT_DIR/$f" ]; then echo "SKIP (missing) $f"; continue; fi + case "$f" in *.sh) mode=755;; *) mode=644;; esac echo "Pushing $f ..." adb push "$SCRIPT_DIR/$f" "/sdcard/_deploy_$f" >/dev/null - 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'" + 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'" adb shell "rm -f /sdcard/_deploy_$f" >/dev/null 2>&1 || true lmd5=$(md5_local "$SCRIPT_DIR/$f") pmd5=$(adb shell "su -c 'md5sum $PHONE_DIR/$f'" | awk '{print $1}' | tr -d '\r') @@ -55,3 +58,7 @@ echo "确认新进程起来了(不能用 pgrep -f,会自匹配 su shell)ï¼ echo " adb shell \"su -c 'ps -ef | grep -E node.auto-reply.js | grep -v grep'\"" echo "看 live 日志(注意:唯一真日志在 hermes-wechat/,别处副本是死的):" echo " adb shell \"su -c 'tail -f $PHONE_DIR/auto-reply.log'\"" +echo "" +echo "若改了 keepalive.sh,需重启守护进程才生效(它以 u0_a327 跑):" +echo " adb shell \"su -c 'pkill -f keepalive.sh'\" # 杀掉旧的" +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 &'\"" diff --git a/server/services/wechat/keepalive.sh b/server/services/wechat/keepalive.sh new file mode 100644 index 0000000..437bd58 --- /dev/null +++ b/server/services/wechat/keepalive.sh @@ -0,0 +1,172 @@ +#!/data/data/com.termux/files/usr/bin/bash +# keepalive.sh — Pixel 微信自动回复保活脚本 +# +# 守护以下进程,挂了就重启: +# 1. termux wake-lock — 防 Doze 冻结 Termux:API(通知监听依赖它) +# 2. HermesAccessibilityService — A11y 易被系统/更新关掉,发现 enabled_accessibility_services=null 就重启 +# 3. hermes-agent gateway — :8642,LLM 网关 +# 4. auto-reply.js — 微信回复主程序 +# +# 用法(在 Termux 用户 shell 里运行;不要在 root shell 起,wake-lock 拿不住 Termux 进程): +# nohup bash ~/hermes-wechat/keepalive.sh > ~/hermes-wechat/keepalive.log 2>&1 & +# +# 前置条件(一次性设置): +# 1. Android 设置 → 应用 → Termux / Termux:API / 微信 → 电池优化 → 不优化 +# 2. Termux:API 已安装,并已授予通知访问权限 +# 3. lsposed-wechat-sendmsg APK 已安装(含 HermesAccessibilityService) +# 4. ~/hermes-wechat/start-ar.sh 存在并已设置 MY_WXID / MY_NICKNAME + +export PATH="/data/data/com.termux/files/usr/bin:$PATH" +export HOME="/data/data/com.termux/files/home" + +HERMES_DIR="${HERMES_DIR:-$HOME/hermes-wechat}" +HERMES_PORT="${HERMES_PORT:-8642}" +GATEWAY_START="${GATEWAY_START:-$HOME/.hermes/scripts/start-gateway.sh}" +A11Y_SVC="com.hermes.wechat.xposed/com.hermes.wechat.xposed.accessibility.HermesAccessibilityService" +CHECK_INTERVAL_SEC="${CHECK_INTERVAL_SEC:-15}" +# auto-reply 启动用的 env(避开 start-ar.sh 的 shell hook 污染) +MY_WXID="${MY_WXID:-wxid_51ap3m6p9xs722}" +MY_NICKNAME="${MY_NICKNAME:-小火子}" +HERMES_TIMEOUT_MS="${HERMES_TIMEOUT_MS:-600000}" +HISTORY_CONTEXT_SIZE="${HISTORY_CONTEXT_SIZE:-8}" + +log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*"; } + +# ── 1. Termux wake-lock ────────────────────────────────────────── +ensure_wake_lock() { + # 用命令行 termux-wake-lock(依赖 Termux:API app);am startservice 是备用 + if command -v termux-wake-lock >/dev/null 2>&1; then + termux-wake-lock 2>/dev/null + else + # 后备:用 su 调系统 ServiceManager + su -c "am startservice --user 0 -a com.termux.service_wake_lock com.termux/.app.TermuxService" 2>/dev/null || true + fi +} + +is_wake_lock_held() { + su -c "dumpsys power 2>/dev/null | grep -q 'termux:service-wakelock'" +} + +# ── 2. A11y service ────────────────────────────────────────────── +is_a11y_enabled() { + out=$(su -c "settings get secure enabled_accessibility_services" 2>/dev/null) + case "$out" in + *com.hermes.wechat.xposed*HermesAccessibilityService*) return 0 ;; + *) return 1 ;; + esac +} + +enable_a11y() { + log "Enabling HermesAccessibilityService..." + su -c "settings put secure enabled_accessibility_services $A11Y_SVC; settings put secure accessibility_enabled 1" 2>/dev/null +} + +# ── 3. hermes gateway ──────────────────────────────────────────── +is_gateway_running() { + # 用 /sys/class/net 风格的端口探测:通过 nc 或 curl,二选一 + if command -v curl >/dev/null 2>&1; then + # gateway 在 termux 用户下需要 Clash 代理(与本机回环无关); + # 但 127.0.0.1:8642 走本机,不用代理 + env -u HTTPS_PROXY -u HTTP_PROXY \ + curl -s --max-time 3 "http://127.0.0.1:${HERMES_PORT}/v1/models" >/dev/null 2>&1 + else + # nc fallback + echo > /dev/tcp/127.0.0.1/$HERMES_PORT 2>/dev/null + fi +} + +start_gateway() { + log "Starting hermes gateway..." + if [ -x "$GATEWAY_START" ]; then + bash "$GATEWAY_START" >> "$HOME/.hermes/logs/gateway-start.log" 2>&1 + else + log "[ERROR] $GATEWAY_START not found" + fi +} + +# ── 4. auto-reply.js ───────────────────────────────────────────── +AR_PID_FILE="$HERMES_DIR/.auto-reply.pid" + +is_auto_reply_running() { + # 不能用 pgrep -f node.*auto-reply.js — 它会自匹配 su shell 自己的 cmdline。 + # 用 ps -ef + grep 精确匹配 'node auto-reply.js'(含空格,排除 sh wrapper)。 + # su 提权因为 auto-reply 跑在 root 下,u0_a327 看不到 root 进程的 ps。 + # 用 . 占位(任何单字符)避开嵌套引号问题 + su -c "ps -ef 2>/dev/null | grep -E node.auto-reply.js | grep -v sh.-c | grep -v grep" \ + 2>/dev/null | grep -q . +} + +start_auto_reply() { + log "Starting auto-reply..." + # 不走 start-ar.sh wrapper(其 shebang 走 dash 后会触发 phone 上某个 + # claude-run/hermes 会话的 shell snapshot hook,污染 hermes-wechat/*.js)。 + # 直接以 root 起 node + 内联 env,跨过那个 shell session。 + # 起完后用 pgrep + grep 严格匹配 cmdline 确切包含 'node auto-reply.js' + # (不能用 pgrep -f 因为正则 .* 会自匹配 su shell 本身),写入 PID 文件 + su -c "cd $HERMES_DIR && \ + MY_WXID=${MY_WXID:-wxid_51ap3m6p9xs722} \ + MY_NICKNAME=\"${MY_NICKNAME:-小火子}\" \ + HERMES_HOST=127.0.0.1 \ + HERMES_PORT=$HERMES_PORT \ + HERMES_TIMEOUT=${HERMES_TIMEOUT_MS:-600000} \ + NOTIFICATION_ENABLED=true \ + HISTORY_CONTEXT_SIZE=${HISTORY_CONTEXT_SIZE:-8} \ + nohup /data/data/com.termux/files/usr/bin/node auto-reply.js \ + > $HERMES_DIR/auto-reply.log 2>&1 < /dev/null & + echo \$! > $AR_PID_FILE" + sleep 2 + if [ -f "$AR_PID_FILE" ]; then + log "Auto-reply started PID=$(cat $AR_PID_FILE 2>/dev/null)" + fi +} + +# ── 5. 主循环 ──────────────────────────────────────────────────── +log "=== Keepalive started ===" +log "HERMES_DIR=$HERMES_DIR GATEWAY_START=$GATEWAY_START" +log "MY_WXID=$MY_WXID MY_NICKNAME=$MY_NICKNAME HERMES_TIMEOUT=${HERMES_TIMEOUT_MS}ms" +log "Check interval: ${CHECK_INTERVAL_SEC}s" + +# 首次进入:把所有依赖拉到位 +ensure_wake_lock +is_a11y_enabled || enable_a11y +is_gateway_running || start_gateway +sleep 5 +is_auto_reply_running || start_auto_reply + +while true; do + sleep "$CHECK_INTERVAL_SEC" + + # wake-lock 偶尔会因 Termux 重启 / 系统重启而丢,保险起见周期性 re-acquire + if ! is_wake_lock_held; then + log "Wake lock lost, re-acquiring..." + ensure_wake_lock + fi + + # A11y 易被系统升级 / 用户在设置中关掉 + if ! is_a11y_enabled; then + log "A11y service disabled, re-enabling..." + enable_a11y + fi + + if ! is_gateway_running; then + log "Gateway not responding on :$HERMES_PORT, restarting..." + start_gateway + sleep 6 + fi + + # ── restart-flag 桥:让 claude-run(proot 里够不到 Magisk root)也能触发重启 ── + # claude-run 改完 *.js 后 `touch ~/hermes-wechat/.restart-request`,keepalive(已是 root + # 能力)看到就杀掉 auto-reply,下面的 is_auto_reply_running 检测会随即用新代码重启。 + # 只暴露「请求重启」这一个动作,不是任意命令执行。 + if [ -f "$HERMES_DIR/.restart-request" ]; then + log "Restart requested via .restart-request flag, killing auto-reply..." + rm -f "$HERMES_DIR/.restart-request" + su -c "pkill -f auto-reply.js" 2>/dev/null + sleep 1 + fi + + if ! is_auto_reply_running; then + log "auto-reply.js not running, restarting..." + start_auto_reply + fi +done -- libgit2 0.21.4