HANDOFF.md 32 KB

HANDOFF — Pixel 微信自动回复部署地图

谁会读这份文档:刚接手这台 Pixel 部署的 claude-run / 开发者,没有上下文。 目标:5 分钟搞清楚正在跑什么、文件在哪、怎么改、怎么验证。

生成时间:2026-05-25(v4:加 §零「claude-run 环境须知」;删除 lemon-acp 完整 clone, 运行时收敛为单一 ~/hermes-wechat/;deploy 脚本修正为推真正的 runtime 文件;isQuoteMe 引用回复修复)。 请定期核对实际状态,因为本文件不会随代码自动更新。


零、claude-run 接手前必读(环境须知)

你(手机上的 claude-run)以 u0_a327(Termux app) 身份运行;bot 以 root 运行。 这条差异是大多数"没权限 / 看不到进程 / 改不动文件"问题的根源:

  1. Bash 工具起不来,报 EACCES: permission denied, mkdir '/root/.claude/session-env' → .claude 目录被 chown 成了 root,而你的真实 uid 是 u0_a327,写不进去。在电脑端 adb 或别的能 su 的 shell 跑一次:

    su -c 'chown -R u0_a327:u0_a327 /data/data/com.termux/files/home/.claude'
    

    然后重开 claude-run 会话。(proot 里你"看起来"是 root,但 proot 对真实文件系统的操作用的是 u0_a327。)

  2. bot 跑在 root:要看 bot 进程、读它写的 root 属主文件(auto-reply.log / .whitelist.json / .auto-reply.pid)、查微信加密 DB、重启 bot——全部要套 su -c。Termux 有 Magisk root,u0_a327 可以 su -c。直接不带 su 的 ps / cat 会看不到 root 进程、读不到 root 文件。

  3. 运行时只有一处:~/hermes-wechat/。 这里的 auto-reply.log 是唯一的真 live 日志。 ⚠️ 历史上有人在 ~/lemon-acp/server/services/wechat/ 留过一份完整 clone,里面的 auto-reply.log 是永不运行的死副本——曾导致接手者读错日志、误判"bot 没在跑"。该 clone 已于 v4 删除。只认 ~/hermes-wechat/。

  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(见 §六)。


TL;DR — 这是个什么系统

Pixel 手机本地的微信自动回复机器人。当好友(如 yibooo)给本机微信账号(小火子 / wxid_51ap3m6p9xs722)发消息,系统会:

  1. 白名单门禁(5/25 加):只对白名单的群/私聊自动回;非白名单先转给 owner 请示授权(详见 §十三)
  2. 轮询微信加密 DB(sqlcipher)拿到新消息
  3. 拉最近 N 条上下文喂给 hermes-agent (POST 127.0.0.1:8642/v1/chat/completions)
  4. hermes 后端走 Moonshot CN(kimi-k2.6)生成回复(通过 Clash mixed port 出网)
  5. am broadcast 触发 LSPosed 模块里的 HermesAccessibilityService
  6. A11y service UI 自动化点击微信(搜联系人 → 进聊天 → 写文本 → 点发送)
  7. 息屏时由 termux-notification-list 兜底,先 input keyevent 224 点亮 + wm dismiss-keyguard 解锁 + am start 拉起微信

整个链路唯一能真把消息送出去的发送方式 = A11y UI 自动化。 Frida daemon 队列 / LSPosed 反射 NetSceneSendMsg 都是死路(详见 docs)。


一、运行中进程清单

# 三个核心进程
adb shell "su -c 'pgrep -af auto-reply.js'"   # node 微信回复主程序
adb shell "su -c 'pgrep -af gateway.run'"     # hermes-agent OpenAI-compat 网关 :8642
adb shell "su -c 'pgrep -x com.tencent.mm'"   # 微信主进程
进程 启动方 作用
bash keepalive.sh 以 u0_a327 跑(nohup bash ~/hermes-wechat/keepalive.sh &),守护进程,每 15s 巡检 挂了就重启下面四项:wake-lock / A11y / gateway / auto-reply
node auto-reply.js 由 keepalive.sh 通过 su -c 内联 env 直接起,以 root 运行(不走 start-ar.sh 的 dash wrapper,避开 shell snapshot hook 污染) 微信回复主程序
python -m gateway.run ~/.hermes/scripts/start-gateway.sh,以 termux 用户 u0_a327 运行 hermes 网关,监听 127.0.0.1:8642
com.tencent.mm 系统 微信前台 UI,A11y 操作目标
HermesAccessibilityService Android AccessibilityManager 系统拉起 链路核心:执行 UI 自动化
frida-server (历史 autostart) 已废弃,可杀。A11y 路径不依赖

二、文件位置 / 与 Mac repo 对应

Mac repo: server/services/wechat/(aipcrm 仓库内)。 部署机制:Mac 上跑 ./deploy-to-phone.sh → 经 /sdcard 中转 dd 落到 ~/hermes-wechat/ + chown 回 u0_a327。 手机上 没有 git 仓库(v4 删了 lemon-acp 完整 clone);~/hermes-wechat/ 是 adb push/dd 部署的运行时,不是 checkout。

2.1 微信回复主程序

Phone 路径 Mac repo 路径
~/hermes-wechat/auto-reply.js server/services/wechat/auto-reply.js
~/hermes-wechat/android-wechat-sender.js server/services/wechat/android-wechat-sender.js
~/hermes-wechat/android-wechat-tracker-sqlcipher.js server/services/wechat/android-wechat-tracker-sqlcipher.js
~/hermes-wechat/start-ar.sh 未入库(含 MY_WXID / MY_NICKNAME)
~/hermes-wechat/auto-reply.log 运行时日志,不入库

目录里还有但已废弃(不要再用 / 不要再改):

  • android-wechat-sender-frida.js — Frida daemon 队列方案,Frida 17 注入会让微信崩
  • android-wechat-tracker-sqlite.js / android-wechat-tracker.js — 老的 tail 日志方案
  • test-hermes*.js、test-phone.js — 一次性调试脚本

2.2 hermes-agent

Phone 路径 说明
~/hermes-agent/ hermes 源码(pip 安装,不是 git 仓库)
~/.hermes/config.yaml 主配置(当前指向 Moonshot CN + kimi-k2.6)
~/.hermes/.env env 变量(KIMI_CN_API_KEY + HTTPS_PROXY=http://127.0.0.1:7890)
~/.hermes/auth.json 凭据池,自动从 env 同步(旧 sk-kimi 还在但已标 exhausted)
~/.hermes/scripts/start-gateway.sh 启 gateway
~/.hermes/scripts/stop-gateway.sh 停 gateway
~/.hermes/scripts/check-gateway.sh 检查状态
~/.hermes/scripts/auto-start.sh termux:boot 自启动
~/.hermes/logs/{gateway,agent,boot,errors}.log hermes 各类日志
~/.hermes/skills/ 用户的 skills(修改 prompt 行为靠这里)
~/.hermes/memories/ 用户的 memories
~/.hermes/sessions/ 历史会话存档
配置备份 ~/.hermes/{config.yaml,.env,auth.json}.bak.20260524_011444

2.3 LSPosed 模块 APK(含 A11y service)

位置 说明
/data/app/.../com.hermes.wechat.xposed-*/base.apk Android 系统安装位置(不能直接编辑)
Mac repo wechat_plain/scripts/lsposed-wechat-sendmsg/ 源码
主入口 app/src/main/java/com/hermes/wechat/xposed/accessibility/HermesAccessibilityService.java
重编译 Mac 上 ./gradlew assembleRelease + 签名 + adb install

2.4 微信数据库

路径 说明
/data/data/com.tencent.mm/MicroMsg/653cdc2320abb089e149a3f7a7683fcf/EnMicroMsg.db 加密 SQLite(SQLCipher),key=e03ce81
~/EnMicroMsg*.db* 历史调试 dump,可清理
~/wechat-importer.js、~/wechat-reader.js 导入工具

三、关键命令速查

启停

# auto-reply 主程序
adb shell "su -c 'pgrep -af auto-reply.js'"
adb shell "su -c 'pkill -f auto-reply.js'"
adb shell "su -c 'nohup /data/data/com.termux/files/home/hermes-wechat/start-ar.sh > /dev/null 2>&1 &'"

# hermes gateway
adb shell "su u0_a327 -c 'export PATH=/data/data/com.termux/files/usr/bin:\$PATH; export HOME=/data/data/com.termux/files/home; bash /data/data/com.termux/files/home/.hermes/scripts/start-gateway.sh'"
adb shell "su -c 'kill \$(cat /data/data/com.termux/files/home/.hermes/gateway.pid) 2>/dev/null; rm -f /data/data/com.termux/files/home/.hermes/gateway.pid'"

A11y service 状态(最常踩的坑)

# 检查
adb shell "settings get secure enabled_accessibility_services"

# 重新启用(重启 / 升级后可能被关)
adb shell "su -c 'settings put secure enabled_accessibility_services com.hermes.wechat.xposed/com.hermes.wechat.xposed.accessibility.HermesAccessibilityService; settings put secure accessibility_enabled 1'"

# dump 当前 bound 服务
adb shell "dumpsys accessibility | grep -A3 Hermes"

日志

# 主程序日志(消息检测 + hermes 调用 + 发送结果)
adb shell "su -c 'tail -f /data/data/com.termux/files/home/hermes-wechat/auto-reply.log'"

# A11y service 实际 UI 操作日志
adb shell "logcat -s HermesA11y:*"

# hermes gateway 日志(LLM 调用 + 错误)
adb shell "su -c 'tail -f /data/data/com.termux/files/home/.hermes/logs/gateway-start.log'"

手动测试发送

# 私聊 yibooo(火星人)
adb shell "am broadcast -a com.hermes.wechat.SEND_MSG --es toUser yibooo --es toUserDisplayName 火星人 --es content 'test'"

# 文件传输助手
adb shell "am broadcast -a com.hermes.wechat.SEND_MSG --es toUser filehelper --es toUserDisplayName 文件传输助手 --es content 'test'"

发送验证应看 DB而不是看 LSPosed Send result: true(后者是已知误报):

# 解密 DB 看最新 isSend=1 行
adb shell "su -c 'cp /data/data/com.tencent.mm/MicroMsg/.../EnMicroMsg.db /sdcard/check.db'"
adb pull /sdcard/check.db /tmp/check.db
sqlcipher /tmp/check.db <<EOF
PRAGMA key="e03ce81";
PRAGMA cipher_compatibility=1;
SELECT msgId, datetime(createTime/1000,'unixepoch','localtime') as t,
       isSend, talker, substr(content,1,50) as c
FROM message WHERE isSend=1 ORDER BY msgId DESC LIMIT 3;
EOF

验证 hermes API 本身

# 简单 chat 完整性测
adb shell "su u0_a327 -c 'export PATH=/data/data/com.termux/files/usr/bin:\$PATH; curl -s --max-time 60 -X POST http://127.0.0.1:8642/v1/chat/completions -H \"Content-Type: application/json\" -d \"{\\\"model\\\":\\\"hermes-agent\\\",\\\"messages\\\":[{\\\"role\\\":\\\"user\\\",\\\"content\\\":\\\"hi\\\"}]}\"'"

四、网络拓扑陷阱(重要)

Pixel 上跑着 ClashMetaForAndroid(com.github.metacubex.clash.meta,UID u0_a330)。

  • Clash 排除了 Termux 用户(u0_a327)出 VPN,所以 termux 用户没 DNS / 没默认出网
  • 但 root 调用可以走 Clash 的 fake-ip
  • hermes-agent 跑在 termux 用户 → 必须 显式走 Clash mixed port 127.0.0.1:7890 才能联网

~/.hermes/.env 里已经配好:

HTTPS_PROXY=http://127.0.0.1:7890
HTTP_PROXY=http://127.0.0.1:7890
NO_PROXY=127.0.0.1,localhost

症状识别:如果看到 hermes 日志里大量 APIConnectionError / Failed to resolve host → 99% 是 Clash 没在跑,或代理 port 变了。先:

adb shell "su -c 'netstat -tlnp 2>/dev/null | grep 7890'"

五、当前 hermes 后端配置(截至 2026-05-24)

# ~/.hermes/config.yaml 摘要
model:
  default: kimi-k2.6
  provider: kimi-coding-cn
  base_url: https://api.moonshot.cn/v1
providers:
  kimi-coding-cn:
    name: kimi-coding-cn
    base_url: https://api.moonshot.cn/v1
    api_key: <Moonshot CN key in .env>
    api_mode: chat_completions
# ~/.hermes/.env 摘要
KIMI_CN_API_KEY=sk-JX3...
HTTPS_PROXY=http://127.0.0.1:7890
HTTP_PROXY=http://127.0.0.1:7890
NO_PROXY=127.0.0.1,localhost

注意:hermes 内置支持的 provider 列表见 ~/hermes-agent/hermes_cli/auth.py(PROVIDER_CONFIGS 字典)。切换其他 LLM 改这里 + env。


六、改代码 / 部署 / 重启工作流

手机上没有 git 仓库,运行时就是 ~/hermes-wechat/。两种改法,看你在哪端:

方式 A:手机上的 claude-run 直接改(最快,适合现场调试)

# 1. ~/hermes-wechat/*.js 属主是 u0_a327,直接用 Edit 改
# 2. 重启(bot 跑在 root,要 su;keepalive 15s 内会自动重启,手动起也行)
su -c 'pkill -f auto-reply.js'
#    确认新进程(不能用 pgrep -f,会自匹配 su shell)
su -c 'ps -ef | grep -E node.auto-reply.js | grep -v grep'
# 3. 看 live 日志确认起来了
su -c 'tail -f ~/hermes-wechat/auto-reply.log'
# 4. ⚠️ 改动同步回 Mac repo(否则下次 Mac deploy 会覆盖你的改动):
#    把改过的文件内容贴回 Mac,或 adb pull 回 server/services/wechat/ 后 commit

方式 B:在 Mac 改 + deploy(适合正式提交)

# Mac 上编辑 server/services/wechat/*.js,然后:
cd server/services/wechat && ./deploy-to-phone.sh    # 推 runtime+文档,自动 chown+md5 校验
adb shell "su -c 'pkill -f auto-reply.js'"           # 重启,keepalive 自动拉起
git add -A && git commit -m "..."                    # 提交到 aipcrm 仓库

冲突警告:方式 A(手机直接改)和方式 B(Mac 改 deploy)会互相覆盖。同一时间只用一种。 手机现场改完一定要尽快同步回 Mac,因为 Mac 端 deploy-to-phone.sh 会无条件覆盖手机文件。


七、已知待办(任选)

今日已完成(5/24):群昵称查询 / A11y 长按引用 + @ 回复 / wake-lock + keepalive 守护 / 清理老凭据 / Frida 残留清理 / A11y FSM 重构 / 流式 SSE / hermes 补丁。

按优先级:

  1. A11y service 重命名:android-wechat-sender-frida.js 等已加废弃注释,可改名为 *.deprecated.js 或删除
  2. Kimi content 沉默问题:模型在 hermes agentic mode 下 content 永远空,所有内容在 reasoning_content,导致 interim_assistant_callback 无法 fire。当前两条路:
    • 短期:开 STREAM_REASONING=true,把 reasoning 加 🧠 前缀作为思考过程发给用户
    • 长期:改 hermes system prompt 或 SOUL.md,让 Kimi 在 content 字段写"对用户的话"而不是只塞 reasoning_content
  3. 群里长消息分段:单条 utterance > 500 字会被截断(maxReplyLength),应该改成自动分段成多条
  4. 照片 / 文件消息:tracker 只识别 type=1 文本,type=3 (图片) 等忽略
  5. 群聊消息去重 / 合流:短时间内多条新消息 → bot 可能每条都触发回复,体验差。需要 debounce + 合并
  6. 流式重试 / 部分输出落地:当前 run 失败丢全部进度,应该让已 emit 的 utterance 保留为状态
  7. 多模型 fallback:当前只 Moonshot CN。Anthropic OAuth 还在 auth.json 但已 401。补一个有效的 fallback provider
  8. Hermes SOUL.md 优化:当前是默认描述,可针对微信场景定制 prompt 让回复更"小火子"
  9. A11y service 故障自愈:发送失败时(如 chat 找不到)能不能更智能恢复(如点 BACK / 切微信 tab)

八、避雷清单(前人踩过的坑)

现象 真实原因 修法
接口返回 ok:true 但 yibooo 没收到 A11y service 没启用 settings put secure enabled_accessibility_services ...
HermesA11y 日志 Root package=com.termux 后退出 broadcast 触发时微信不在前台 broadcast 前先 am start -n com.tencent.mm/.ui.LauncherUI
Search button not found toUserDisplayName 错 / 当前页不是 LauncherUI 用 WeChat nickname,不是 wxid
[HermesWX] Send result: true 但 DB 没新消息 LSPosed 反射路径的误报(已知 bug) 忽略 HermesWX 日志,看 HermesA11y
hermes gateway 大量 Connection error Clash 没在跑 / 7890 端口变了 检查 `netstat -tlnp
Gateway failed to start! setsid: exec python: No such file or directory su -c PATH 缺 termux bin 用 su u0_a327 -c 'export PATH=/data/data/com.termux/files/usr/bin:$PATH; bash start-gateway.sh'
pgrep -f com.tencent.mm 找不到微信 Android 把 cmdline 设成 zygote64,-f 不匹配 用 pgrep -x com.tencent.mm(匹配 /proc/PID/comm)
频繁 403 quota exhausted LLM 后端配额耗尽 切换 provider,见第 5 节
守护脚本里 pgrep -f node.*auto-reply.js 永远返 true(导致永远不重启 / 0 次启动)或永远返 false(导致 spawn 风暴) pgrep -f 在 su -c "..." 包装下会自匹配自己 shell 的 cmdline;如果改用 PID 文件,$! 抓到的是 sh -c wrapper PID(comm=sh)而不是 node 子进程 用 `su -c "ps -ef \
部署 JS 后 node 启动报 SyntaxError: Invalid regular expression flags 指向不存在的行号(文件 wc 看是干净的) phone 上某 claude-run/hermes 会话的 shell snapshot hook 在 dash 启动时尝试写 /data/local/tmp/hermes-snap-*.sh 失败,错误以追加方式注入到 hermes-wechat/*.js 末尾 部署用 dd if=src of=dst 而不是 cp;启动 node 时直接 su -c 'cd X && env... nohup node ...',不经 start-ar.sh / dash wrapper
[HERMES] API timeout / socket hang up 在 turns=9 多上下文场景下偶发 Moonshot kimi-k2.6 长 context LLM call 可达 60s+,旧的 HERMES_TIMEOUT=120000 偶发不够 启动 env 设 HERMES_TIMEOUT=180000(3 分钟),keepalive.sh 已内联
A11y 状态机 CHAT_LIST → click → WRONG_CHAT → BACK 循环死磕直到 cap=6 给出 群聊页 obn (id=/obn) 标题带成员数后缀(如 "宜创AI全员群(60)"),而 broadcast 传的 toUserDisplayName 是 "宜创AI全员群"(chatroom.displayname 不带后缀),equals 永远 false classifyState 拿 obn.text 后用正则 \\(\\d+\\)$ 剥成员数后缀再比对(带/不带都接受)
WeChat LauncherUI 不在主"会话" tab,FLAG_ACTIVITY_CLEAR_TASK 也无法回去 LauncherUI 是 fragment 容器,CLEAR_TASK 重置 task 但不重置内部 fragment 状态。从聊天页或其它主 tab 调 startActivity 不会跳回会话列表 状态机的 NO_CHAT_VIEW 处理优先 findBottomNavTab("微信") 点击(按 Y 坐标取最下方那个,区分标题栏同名 text),fallback GLOBAL_ACTION_BACK
Hermes /v1/runs 长时间一片寂静、无任何 [UTTER] 事件输出,最后 run.completed 一次性给一大段总结 Kimi-k2.6 在 hermes agentic mode 下 assistant_message.content 永远空,所有内容在 reasoning_content;message.delta/interim_assistant_callback/message.complete 都不 fire;只有 reasoning.delta 大量喷涌 短期开 STREAM_REASONING=true 让 reasoning 加 🧠 前缀作 utterance;长期改 hermes system prompt 让 Kimi 写 content 字段。详见 §十二.4
auto-reply node 进程被 keepalive 反复重启,CPU 占用高、log 文件被洪水般写入 流式场景下高频 message.delta/reasoning.delta 事件触发同步 fs.appendFileSync + 每条都 console.log,sync I/O 卡死 node;keepalive 检测心跳超时 → kill → restart log() 去掉 appendFileSync(stdout 已由 start 脚本重定向到 log file);诊断事件加节流(高频事件 5 秒打一次)
Hermes /v1/runs 只输出"最终一条回复",没多步进度 hermes 默认 /v1/runs 只挂 stream_delta_callback + tool_progress_callback,缺 reasoning_callback + interim_assistant_callback(gateway 平台用 7+ 个回调) 补丁 gateway/platforms/api_server.py,在 _create_agent 签名 + AIAgent 构造 + run handler 三处都加上缺的两个回调,emit reasoning.delta / message.complete SSE 事件。详见 §十二.3
owner 回「同意」授权后没生效、pending 没清 owner 用微信引用回复发「同意」,content 是 type=57 appmsg XML(<title>同意</title> + <refermsg>),不是纯文本,parseApprovalCommand 正则匹配不上 parseApprovalCommand 检测 <appmsg>/<refermsg> → 提取 <title> 作回复文本 + <refermsg> 作 quotedContext 匹配目标。详见 §十三.6
群里引用机器人消息但不 @ 的回复(如引用后回「[强]」)被静默忽略、[NORMALIZE] 之后再无下文 handleMessage 先把引用回复 normalize 成 prefix+title(剥掉 <refermsg> XML),却又用这个剥过的 content 调 isQuoteMe;而 isQuoteMe 第一步就 if(!content.includes('<refermsg'))return false → 永远 false。只有带 @小火子 的引用回复能走 isAtMe 命中 已修(5/25):auto-reply.js:829 改成 isQuoteMe(rawContent, ...),用保留的原始 XML 判断引用的是不是机器人(<refermsg> 里 <displayname> 是否 == myNickname)

九、Owner / Identity

  • 本机微信账号:小火子 (wxid_51ap3m6p9xs722)
  • 测试好友:yibooo (nickname 火星人)
  • A11y 内部"系统"昵称:搜索框输的是 nickname,不是 wxid

start-ar.sh 当前 env:

MY_WXID=wxid_51ap3m6p9xs722   # 本机 wxid,用于 ignoreSelf
MY_NICKNAME=小火子              # 本机 nickname,用于 isAtMe 检测
HISTORY_CONTEXT_SIZE=8         # 喂给 hermes 的历史消息条数

十、看哪些日志能判断流程跑到哪一步

按时间顺序 grep auto-reply.log:

日志关键字 含义
[TRACKER] DB polling started sqlcipher 轮询启动
[A11Y] HermesAccessibilityService is enabled A11y 启动校验通过
[NOTIF] Notification watcher started 息屏通知轮询启动
[NOTIF] WeChat notification: ... 检测到微信通知
[PRIVATE] yibooo: ... / [GROUP-AT] xxx@chatroom tracker 捕获新消息
[HERMES] Calling API for ... turns=N 调 hermes,N 是上下文条数
[HERMES] Reply: ... hermes 生成回复
[SENT] yibooo (as "火星人") ... A11y 发送 broadcast
[SEND FAILED] 发送链路出错
[NO REPLY] Generator returned null hermes 返回空 → 看 hermes gateway 日志

A11y 那一侧看 logcat -s HermesA11y:*,正常应输出:

Received send request: to=yibooo searchName=火星人 content=...
Root package=com.tencent.mm
Already in correct chat, typing directly  # 或 Not in chat, starting search
Setting chat text: ...
Clicking send button (by ID)
State reset

十一、A11y 状态机(5/24 重构)

旧代码用嵌套 if + ad-hoc relaunch 处理"目标聊天 / 错误聊天 / 主界面 / 微信不在前台"等多种状态,边角情况覆盖不全(特别是 LauncherUI fragment 容器 + CLEAR_TASK 不重置内部 fragment)。

新实现:显式状态机(HermesAccessibilityService.java::driveStateMachine)

状态 classifier 特征 动作
TARGET_CHAT bkk + obn.text 等于 displayName(剥末尾 (数字) 后) performTypeAndSend 直走
WRONG_CHAT bkk + obn.text 不匹配 GLOBAL_ACTION_BACK → 再 classify
CHAT_LIST 无 bkk,kbq 多个 findChatListRow 点 cj1 → 再 classify
NO_CHAT_VIEW 无 bkk 无 kbq 但在 com.tencent.mm findBottomNavTab("微信") 点底部 tab,找不到就 BACK
OUTSIDE_WECHAT root.pkg != com.tencent.mm startActivity(LauncherUI) → 再 classify
  • 入口:onReceive 拉 LauncherUI 后 3.5s 调 driveStateMachine(0)
  • MAX_STATE_TRANSITIONS = 6 防死循环
  • 关键修复:chat list 优先于搜索(避开"丁鹏、🍋火星人、火星人"被搜索解读成"创建包含 3 人的新群"歧义)
  • 关键修复:obn 标题剥末尾 (\d+)(群聊页 obn 是"宜创AI全员群(60)" 含成员数后缀)

十二、流式多步消息(/v1/runs SSE,5/24 实现 + hermes 补丁)

12.1 架构

auto-reply.js                  hermes (/v1/runs)            Moonshot CN
     │                                │                         │
     ├─POST /v1/runs─────────────────→│  返回 run_id            │
     │  { input, conversation_history,│                         │
     │    session_id, instructions }  │                         │
     │←───────────── run_id ──────────┤                         │
     │                                │                         │
     ├─GET /v1/runs/{id}/events ─────→│  长连 SSE               │
     │                                │←──API call #1───────────┤
     │←─message.delta / reasoning.delta /                       │
     │  tool.started / message.complete                         │
     │                                │  ...iterating tools...  │
     │                                │←──API call #N───────────┤
     │←──run.completed ───────────────┤                         │

每个事件触发一段独立 utterance(微信单独发送一条消息),实现"hermes 多步思考 → 微信多条回复"。

12.2 SSE 事件类型(按消费优先级)

事件 payload auto-reply 处理
message.complete { text, already_streamed } 主路径:每 turn 结束 / tool 前,emit text 作为一条 utterance(已 strip <think> 块,是用户可见内容)
message.delta { delta } 累积到 textBuf,tool.started 时兜底 flush
reasoning.delta { text } 默认丢弃(模型内心独白);STREAM_REASONING=true 时累积 + 加 🧠 前缀 emit
reasoning.available { text } 同上,仅在 reasoning.delta 没累积时用
tool.started { tool, preview } flush 任何累积;STREAM_TOOL_PROGRESS=true 时额外 emit 🔧 工具: 参数
tool.completed { tool, duration, error } 忽略
run.completed { output, usage } emit output 作为最终 utterance + resolve Promise
run.failed { error } reject Promise

12.3 Hermes API server 补丁(已应用到 phone)

/v1/runs upstream 默认只挂 stream_delta_callback(content tokens)+ tool_progress_callback,缺:

  • reasoning_callback → Kimi/Moonshot 的 chain-of-thought 走不出来
  • interim_assistant_callback → TUI/Telegram/Discord 用的多步消息机制

我们在 ~/hermes-agent/gateway/platforms/api_server.py 加了两个回调 + 在 _create_agent 签名 / AIAgent 构造调用都补了参数。改动以注释 # Forward reasoning_content... 和 # Forward interim assistant messages... 标识。

改动文件:phone 上 ~/hermes-agent/gateway/platforms/api_server.py(备份 .bak)。Mac repo 上没同步——hermes-agent 不在我们的 git repo 里。如果 hermes 升级要重新补丁。

12.4 Kimi-k2.6 在 Hermes agentic mode 的限制 ⚠️

实测发现:Kimi-k2.6 在 hermes 系统提示下,assistant_message.content 永远是空字符串,所有可见内容塞 reasoning_content。导致:

  • message.delta 计数 = 0(content 字段没 token)
  • interim_assistant_callback 内部检查 visible = strip_think(content).strip() 为空就 return → 不 fire
  • message.complete 事件也不出现
  • 用户只能在 run.completed.output 时看到最终一段总结

短期 workaround:STREAM_REASONING=true(默认 OFF)让 chain-of-thought 加 🧠 前缀作为 utterance 发出去。

长期方案:改 hermes system prompt / SOUL.md 让 Kimi 把"对用户说的话"写到 content 字段。

12.5 流式相关 ENV flag

变量 默认 作用
STREAM_TOOL_PROGRESS false 每个 tool.started 发一条 🔧 工具: 参数 进度消息
STREAM_REASONING false 把 Kimi 的 reasoning_content(内心独白)加 🧠 前缀作为 utterance 发
HERMES_TIMEOUT 180000 (旧) → 600000 (新) SSE 长连 timeout,长 agentic run 经常 3-5min
HISTORY_CONTEXT_SIZE 8 喂给 hermes 的微信历史消息条数

十三、白名单门禁 + 授权流程(5/25 新增)

13.1 目的

不再对任何人自动回复。只有白名单的群 / 私聊才自动回;非白名单的发信人, 机器人先转给主人(owner)请示,owner 回「同意」才授权。

13.2 角色

角色 wxid 说明
机器人本体 wxid_51ap3m6p9xs722(小火子) 跑 auto-reply 的微信账号
owner(主人/授权者) yibooo(火星人) 收授权请求 + 发「同意/拒绝」指令;永远在白名单

ENV:OWNER_WXID(默认 yibooo)、OWNER_NICKNAME(默认 火星人,A11y 搜索用)。

13.3 门禁判定(handleMessage 内,replyType 判定之后)

senderWxid = 群聊 ? content 前缀里的发送者 wxid : talker
isFromOwner = senderWxid==OWNER || talker==OWNER

if isFromOwner:
    parseApprovalCommand(content) 命中 → handleApproval(同意/拒绝),return
    否则放行(owner 普通聊天)
elif !whitelist.isAllowed(talker):
    requestAuthorization(转 owner 请示),return   ← 不回复发信人
else:
    generateAndSendReply(正常流程)

注意:白名单 key 是 talker(私聊=对方 wxid,群聊=chatroom id),即"授权这个会话"。 群里只要 chatroom 在白名单,任何人 @ 都会自动回。owner 在任何群 @ 都放行(按 senderWxid 判断)。

13.4 授权交互

  • 请示:requestAuthorization 给 owner 发 【授权请求】X 给我发消息了:「...」回复「同意」我就自动回复 TA;回复「拒绝」忽略。 并记入 pending(同一 talker 不重复请示)。
  • 同意:owner 回 同意 / 同意 宜创AI全员群(指定)→ 加白名单 + 自动回复 TA 刚才那条 pending 消息 + 给 owner 发确认。
  • 拒绝:owner 回 拒绝 / 忽略 → 移除该 pending。
  • 指令动词:同意|授权|批准|approve|allow|yes vs 拒绝|忽略|不同意|deny|no。

13.5 .whitelist.json 格式

位于 ~/hermes-wechat/.whitelist.json(持久化,重启不丢):

{
  "allowed": ["yibooo", "34576206251@chatroom"],
  "pending": {
    "wxid_g240cr5svks122": {
      "senderName": "",
      "content": "好的哈[社会社会]",
      "displayName": "马锐模",
      "ts": 1779684269591
    }
  }
}
  • allowed:已授权的 wxid(私聊)+ chatroom id(群)。owner wxid 启动时自动加入。
  • pending:待授权请求,key 是 talker,value 含 senderName / 原始 content / displayName / 时间戳。
  • 手动加白名单:直接编辑此文件加到 allowed 数组,重启 auto-reply 生效。

13.6 ⚠️ 引用回复授权的坑

owner 如果用微信引用回复发「同意」(而不是直接打字),content 不是纯文本 「同意」,而是 type=57 appmsg XML:

<msg><appmsg><title>同意</title><type>57</type>
  <refermsg>...被引用的"【授权请求】宜创AI全员群..."原文...</refermsg>
</appmsg></msg>

parseApprovalCommand 必须:

  1. 检测 <appmsg>/<refermsg> → 从 <title> 提取真正的回复文本("同意")
  2. 从 <refermsg> 提取 quotedContext → 匹配是"同意哪条 pending"(引用了哪条授权请求)

handleApproval 目标匹配优先级:显式 target → quotedContext 里出现的 pending displayName → 最近一个 pending。

13.7 相关方法(auto-reply.js)

方法 作用
WhitelistStore 持久化 allowed/pending
parseApprovalCommand(content) 解析授权指令(含引用回复 XML 提取),返回 {approve, target, quotedContext}
requestAuthorization(talker, senderName, content) 转 owner 请示 + 记 pending
handleApproval(cmd) 同意→加白名单+回复 pending;拒绝→移除
generateAndSendReply({...}) 抽出来的回复生成(白名单通过 / 授权后复用)
sendPlainTo(toUser, displayName, text) 给 owner 发纯文本(不走 hermes)

END OF HANDOFF — 改动这份文档时记得更新顶部生成时间。