# 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 跑一次: ```bash 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`)发消息,系统会: 0. **白名单门禁**(5/25 加):只对白名单的群/私聊自动回;非白名单先转给 owner 请示授权(详见 §十三) 1. **轮询微信加密 DB**(sqlcipher)拿到新消息 2. **拉最近 N 条上下文**喂给 hermes-agent (`POST 127.0.0.1:8642/v1/chat/completions`) 3. hermes 后端走 **Moonshot CN**(kimi-k2.6)生成回复(通过 Clash mixed port 出网) 4. **`am broadcast`** 触发 LSPosed 模块里的 `HermesAccessibilityService` 5. A11y service **UI 自动化点击微信**(搜联系人 → 进聊天 → 写文本 → 点发送) 6. 息屏时由 `termux-notification-list` 兜底,先 `input keyevent 224` 点亮 + `wm dismiss-keyguard` 解锁 + `am start` 拉起微信 **整个链路唯一能真把消息送出去的发送方式 = A11y UI 自动化**。 Frida daemon 队列 / LSPosed 反射 NetSceneSendMsg 都是死路(详见 docs)。 --- ## 一、运行中进程清单 ```bash # 三个核心进程 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` | 导入工具 | --- ## 三、关键命令速查 ### 启停 ```bash # 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 状态(最常踩的坑) ```bash # 检查 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" ``` ### 日志 ```bash # 主程序日志(消息检测 + 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'" ``` ### 手动测试发送 ```bash # 私聊 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`(后者是已知误报): ```bash # 解密 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 </dev/null | grep 7890'" ``` --- ## 五、当前 hermes 后端配置(截至 2026-05-24) ```yaml # ~/.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: api_mode: chat_completions ``` ```bash # ~/.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 直接改(最快,适合现场调试) ```bash # 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(适合正式提交) ```bash # 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 | grep 7890` | | `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 \| grep -E node.auto-reply.js \| grep -v sh.-c \| grep -v grep"` 精确匹配 "node auto-reply.js" 实进程;pattern 用 `.` 占位单字符避免嵌套引号 | | 部署 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(`同意` + ``),不是纯文本,`parseApprovalCommand` 正则匹配不上 | parseApprovalCommand 检测 ``/`` → 提取 `` 作回复文本 + `<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: ```bash 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`(持久化,重启不丢): ```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**: ```xml <msg><appmsg><title>同意57 ...被引用的"【授权请求】宜创AI全员群..."原文... ``` `parseApprovalCommand` 必须: 1. 检测 ``/`` → 从 `` 提取真正的回复文本("同意") 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** — 改动这份文档时记得更新顶部生成时间。