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 运行。 这条差异是大多数"没权限 / 看不到进程 / 改不动文件"问题的根源:
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。)
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 文件。运行时只有一处:
~/hermes-wechat/。 这里的auto-reply.log是唯一的真 live 日志。 ⚠️ 历史上有人在~/lemon-acp/server/services/wechat/留过一份完整 clone,里面的auto-reply.log是永不运行的死副本——曾导致接手者读错日志、误判"bot 没在跑"。该 clone 已于 v4 删除。只认~/hermes-wechat/。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 运行,别被带偏。改代码 → 生效:
~/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(见 §六)。
- 有 su(hermes/Termux):
TL;DR — 这是个什么系统
Pixel 手机本地的微信自动回复机器人。当好友(如 yibooo)给本机微信账号(小火子 / wxid_51ap3m6p9xs722)发消息,系统会:
- 白名单门禁(5/25 加):只对白名单的群/私聊自动回;非白名单先转给 owner 请示授权(详见 §十三)
- 轮询微信加密 DB(sqlcipher)拿到新消息
- 拉最近 N 条上下文喂给 hermes-agent (
POST 127.0.0.1:8642/v1/chat/completions) - hermes 后端走 Moonshot CN(kimi-k2.6)生成回复(通过 Clash mixed port 出网)
am broadcast触发 LSPosed 模块里的HermesAccessibilityService- A11y service UI 自动化点击微信(搜联系人 → 进聊天 → 写文本 → 点发送)
- 息屏时由
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 补丁。
按优先级:
- A11y service 重命名:
android-wechat-sender-frida.js等已加废弃注释,可改名为*.deprecated.js或删除 - 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
- 短期:开
- 群里长消息分段:单条 utterance > 500 字会被截断(
maxReplyLength),应该改成自动分段成多条 - 照片 / 文件消息:tracker 只识别 type=1 文本,type=3 (图片) 等忽略
- 群聊消息去重 / 合流:短时间内多条新消息 → bot 可能每条都触发回复,体验差。需要 debounce + 合并
- 流式重试 / 部分输出落地:当前 run 失败丢全部进度,应该让已 emit 的 utterance 保留为状态
- 多模型 fallback:当前只 Moonshot CN。Anthropic OAuth 还在 auth.json 但已 401。补一个有效的 fallback provider
- Hermes SOUL.md 优化:当前是默认描述,可针对微信场景定制 prompt 让回复更"小火子"
- 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 → 不 firemessage.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|yesvs拒绝|忽略|不同意|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 必须:
- 检测
<appmsg>/<refermsg>→ 从<title>提取真正的回复文本("同意") - 从
<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 — 改动这份文档时记得更新顶部生成时间。