keepalive.sh 6.85 KB
#!/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:-180000}"
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:-180000} \
        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

    if ! is_auto_reply_running; then
        log "auto-reply.js not running, restarting..."
        start_auto_reply
    fi
done