2026-08-07-task-hexdo-tunnel-and-wechat-watcher.md 9.69 KB

task.hexdo.com 隧道重新分配 + 微信回复 watcher 误重启修复

时间:2026-08-07 触发:用户反馈「pixel 的反向代理不能用了 task.hexdo.com」


一、task.hexdo.com 反代排查与重新分配

起因排查

task.hexdo.com 最初被以为是 Pixel 手机 wechat-viewer 的反代域名,但实际排查发现:这个域名自 2026-06-15 创建起就一直 CNAME 到 Mac 上 agent-bridge 仓库的 claude-yi cloudflared 隧道(源站是 pm2 管理的 bridge 进程,health port 10003),跟手机完全无关——用 Cloudflare API 直接查 DNS 记录的 created_on 字段坐实了这一点,不是「本来指手机、后来被改掉」,而是从来没指过手机。

Mac 侧当时的故障是两个独立组件都被手动停掉、没人重新拉起:

  • pm2 里 claude-yi 这个 entry 整个不存在(不是 stopped,是没了)
  • launchd job com.hexdo.cloudflared-claude-yi 完全没有 bootstrap(launchctl list 查不到)

两边日志都在 2026-07-30 17:18 前后干净 SIGTERM 退出,时间戳吻合,判断是一次性手动停止后忘记恢复。修复:

cd ~/Documents/LemonAI/agent-bridge/agents/claude-yi && ./start.sh
launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.hexdo.cloudflared-claude-yi.plist

架构决策:域名重新分配

确认现状后,用户明确决定:task.hexdo.com 改专属 Pixel 手机的 wechat-viewer,Mac 的 agent-bridge 换一个新域名 bridge.hexdo.com。用 Cloudflare API(凭证从 Mac ~/.cloudflared/cert.pem 里解出 zoneID/accountID/apiToken,因为 cloudflared tunnel route dns --overwrite-dns 这个版本实测不生效)直接 PATCH DNS 记录完成切换:

  1. Mac 上 cloudflared tunnel create pixel-wechat-viewer 生成手机要用的新隧道凭证(~/.cloudflared/704efd12-a194-45dc-8333-4d6952eeb66f.json)
  2. cloudflared tunnel route dns claude-yi bridge.hexdo.com 给 Mac 隧道挂新域名
  3. Cloudflare API PATCH 把 task.hexdo.com 的 CNAME 记录改指向新隧道 704efd12-....cfargotunnel.com
  4. Mac ~/.cloudflared/config.yml 的 ingress hostname 从 task.hexdo.com 改成 bridge.hexdo.com,launchctl kickstart -k 重连
  5. 新隧道凭证 + config.yml(ingress task.hexdo.com → http://127.0.0.1:5001)通过 adb push /sdcard/ 中转(不能直接 push 进 Termux 私有目录,会 Permission denied)+ su -c cp/chown 落到手机 ~/.cloudflared/

踩的坑:进程属主必须是 termux 应用自己的 uid,不能是 root

手动验证时图省事用 su -c 'setsid nohup cloudflared ...' 起了一次隧道——这样起的进程属主是 root。但看门狗脚本、cron 调度器实际都以 Termux 应用自己的 uid(当时是 u0_a329,数字 uid 10329)跑,Android 有进程可见性隔离,非 root 的 pgrep/ps 看不到 root 拥有的进程,会导致看门狗永远以为隧道没起、每次都想再起一个,还会因为想写日志到 root 占用的旧文件而权限不够。

正确做法:su -G 3003 <termux_uid> -c '...' 以 termux 自己的身份起(-G 3003 是保住 inet 组权限,否则 DNS 会断,这条是这台设备的老坑,见 ~/.claude/projects/.../memory/magisk-su-android-inet-group.md)。cloudflared 二进制 + ~/.cloudflared/ 下的凭证/配置全是 termux 应用自己拥有,这个隧道进程完全不需要 root。

隧道自愈并入已有看门狗(没有另建新 skill)

Pixel 上本来就有一个每 10 分钟跑一次的 cron job「WeChat Viewer 看门狗」(~/.hermes/scripts/wechat-viewer-watchdog.sh,no_agent: true,deliver: origin)。给它新增第 7 节:

  1. pgrep 查 pixel-wechat-viewer 隧道进程在不在,不在就起
  2. 进程在不代表隧道通(连接可能卡死/边缘握手失败),实测 curl https://task.hexdo.com/api/doctor 才算数
  3. 不通就重启一次隧道进程,重启完还不通就 echo 一行诊断信息,不再无限重试

为什么 echo 一行就等于「失联通知」:Hermes cron 的 no_agent=true job 由调度器直接跑 script,stdout 原样投递回创建这个 job 时所在的会话(deliver: origin,源码 hermes-agent/tools/cronjob_tools.py 里 no_agent/deliver 参数的注释写明 "the script IS the job and its stdout is delivered verbatim");stdout 为空则静默不投递(这条 job 已经稳定跑了近 6000 次没有噪音骚扰过,验证过这个假设成立)。所以不需要另外写 am broadcast 发微信通知——脚本里但凡有异常就 echo,正常就不 echo,天然就是失联通知。

最终状态

  • task.hexdo.com → Pixel 手机 pixel-wechat-viewer 隧道 → 127.0.0.1:5001(wechat-viewer),验证 curl https://task.hexdo.com/api/doctor → 200
  • bridge.hexdo.com → Mac claude-yi 隧道 → 127.0.0.1:10003(agent-bridge),验证 curl https://bridge.hexdo.com/health → 200
  • 手机侧持久化:~/.termux/boot/start-wechat-viewer(Termux:Boot 脚本)末尾加了隧道启动命令,重启设备后自动拉起;顺手把该脚本里写死多年的 fuser -k 5000/tcp 改成了实际监听端口 5001(app.py 早就改用 5001,旧的 kill 命令一直在误杀空气)

二、微信自动回复 watcher 误重启修复

症状

同一个看门狗脚本第 5 节(微信自动回复 watcher 单例保护)里,watchdog.log 天天刷屏「微信 Watcher 存活数=0(期望 1),重置中... 重置后存活数异常: 0」,几乎每个 cron 周期(10 分钟)都触发一次。

根因:探测本身不可靠,不是进程真的挂了

第 5 节原本靠 su -c 'ps -A' 2>/dev/null | grep -c '[w]echat_reply_watcher' 判活。现场用同一个 adb 会话连续两条命令直接实锤:ps -A | grep -c 报 0,紧接着(不到 1 秒)ps -A -o PID,ARGS | grep 却找到了同一个 PID——探测本身在系统负载下会偶发返回空/截断,哪怕进程明明活着。这跟 wechat-reply-watcher-process-leak.md 记录的 2026-07-25 877 进程雪球事故是同一个底层不可靠因子(那次是"探测漏检 → 没先 pkill 就叠加新实例 → 雪球";这次探测漏检的后果没那么严重,因为 7-25 之后已经加了"重置时先 pkill 清场"的幂等防线,不会再雪球,但会白白重启健康进程,在 1-2 秒空窗期有漏消息风险,且日志噪音很大)。

修复

wechat_reply_watcher.py:daemon 启动时把自己 PID 写进 ~/.hermes/wechat_reply/watcher.pid(_write_pidfile(),正常/异常退出时 best-effort 清理,虽然 pkill 走 SIGTERM 默认终止不会触发 Python 的 finally,所以真正兜底还是靠下面的 /proc 存在性检查)。

wechat-viewer-watchdog.sh 第 5 节判活逻辑换成:

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

单次读 /proc/<pid>/cmdline 比解析整个 ps -A 输出更轻量也更可靠,同时用 cmdline 内容校验防止 PID 复用误判成"还活着"。"判活为假才重置"这个新逻辑没有削弱 7-25 加的防线——一旦触发重置,仍然是先 pkill -f wechat_reply_watcher.py 清场再起新实例,不单纯信 pidfile 干净。

顺手修的独立小 bug:日志重复写

log() 函数同时 print()(会被 daemon 启动命令 >> watcher.log 2>&1 的 stdout 重定向捕获一次)又显式 open(LOG_FILE,'a').write() 同一个文件——每行日志被写两遍。历史 watcher.log 里所有行成对重复就是这个原因,纯噪音不影响功能,删掉了显式文件写那段,只留 print()。

验证

手动清场(pkill)后跑一次看门狗,触发重置成功、pid 记入 pidfile;立即再跑第二次看门狗——即使当次 ps -A | grep -c 依然复现性地报 0(证明探测本身的不可靠是持续存在的环境事实,不是偶发),pidfile+cmdline 检测正确判定「还活着」,没有触发不必要的重置。watcher.log 新写入的行确认不再重复。

遗留、未处理的问题

诊断过程中在手动跑整个看门狗脚本时看到「Gateway 不在线,重启中... /tmp/hermes-gateway.log: Permission denied」——这是我手动用 su -G 3003 <uid> -c 从 adb 触发时的执行环境差异(缺少 Termux 自身的 LD_PRELOAD/路径重写机制),不代表真实 cron 执行环境下也会失败,没有进一步处理,如果之后在真实 watchdog.log 里持续看到这条才需要认真排查。


涉及文件

  • ~/.cloudflared/config.yml(Mac,ingress 改 bridge.hexdo.com)
  • ~/.cloudflared/pixel-config.yml → 手机 ~/.cloudflared/config.yml(新建,ingress task.hexdo.com)
  • ~/.termux/boot/start-wechat-viewer(手机,Termux:Boot 脚本,加隧道启动 + 修端口号)
  • ~/.hermes/scripts/wechat-viewer-watchdog.sh = 本仓库 wechat_plain/scripts/auto-reply/wechat-viewer-watchdog.sh(新增第 7 节隧道自愈 + 第 5 节判活改用 pidfile)
  • ~/.hermes/wechat_reply/wechat_reply_watcher.py = 本仓库 wechat_plain/scripts/auto-reply/wechat_reply_watcher.py(新增 pidfile 写入 + 修日志重复写)

设备上对应文件均留有 .bak-20260807* 备份。

相关记忆

  • task-hexdo-reverse-proxy-outage.md(pixelphone 项目 memory)
  • task-hexdo-tunnel-clash-direct.md(agent-bridge 项目 memory,Mac 侧 Clash TUN 劫持等历史故障模式,域名已改成 bridge.hexdo.com)
  • wechat-reply-watcher-process-leak.md(pixelphone 项目 memory,2026-07-25 877 进程雪球事故 + 本次后续修复)