From 71e983e7bbd18c47700ed64880db99d3126e696d Mon Sep 17 00:00:00 2001 From: yibo Date: Mon, 25 May 2026 19:12:14 +0800 Subject: [PATCH] feat: 微信 bot 手机端接手配置 — isQuoteMe 修复 + deploy 修正 + 接手文档 --- server/services/wechat/AGENTS.md | 58 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ server/services/wechat/CLAUDE.md | 10 ++++++++++ server/services/wechat/HANDOFF.md | 79 +++++++++++++++++++++++++++++++++++++++++++++++++++++-------------------------- server/services/wechat/auto-reply.js | 3 ++- server/services/wechat/deploy-to-phone.sh | 65 +++++++++++++++++++++++++++++++++++++---------------------------- 5 files changed, 160 insertions(+), 55 deletions(-) create mode 100644 server/services/wechat/AGENTS.md create mode 100644 server/services/wechat/CLAUDE.md diff --git a/server/services/wechat/AGENTS.md b/server/services/wechat/AGENTS.md new file mode 100644 index 0000000..728ecc4 --- /dev/null +++ b/server/services/wechat/AGENTS.md @@ -0,0 +1,58 @@ +# AGENTS.md — 微信自动回复 bot 维护入口 + +> 接手维护这台 Pixel 微信自动回复 bot 的 agent,先读这份,再读 `HANDOFF.md`(完整细节,尤其 **§零 环境须知**)。 +> 这个文件夹 `~/hermes-wechat/` 同时是**运行时**和**唯一源**(手机上无 git,改完要同步回 Mac repo)。 + +## 这是什么 + +Pixel 本机的微信自动回复机器人(账号「小火子」`wxid_51ap3m6p9xs722`)。链路: +轮询微信加密 DB(sqlcipher)拿新消息 → 调本机 hermes 网关 `127.0.0.1:8642` 生成回复 → `am broadcast` 触发 `HermesAccessibilityService` → A11y UI 自动化点微信(搜联系人→进聊天→输入→发送)。**唯一能真把消息发出去的方式 = A11y UI 自动化。** + +## 协作分工(混合模式:claude-run + hermes) + +| 角色 | 跑在 | 能力 | 干什么 | +|------|------|------|--------| +| **claude-run(Claude)= 脑子** | proot 容器 | 能编辑本目录 `*.js`、读 `auto-reply.log`、读改 `.whitelist.json`;**不能 su** | 写 / 改 / 审代码、看日志诊断 | +| **hermes(Kimi)= 手** | Termux(能 su) | 能 `su -c` 拿 Magisk root | 跑需要 root 的设备操作:重启 bot、查微信 DB、发测试消息、logcat、重开 A11y | + +> claude-run 在 proot 里够不到 Magisk root,也看不到微信 DB(没挂载)。需要 root 的步骤交给 hermes 或 Termux shell 执行。 + +## 核心文件(bot 实跑只这三个,别被目录里其他文件带偏) + +- `auto-reply.js` — 主程序(消息 FSM、白名单门禁/授权、流式 SSE、引用回复 normalize) +- `android-wechat-tracker-sqlcipher.js` — 轮询微信加密 `EnMicroMsg.db` 检测新消息 +- `android-wechat-sender.js` — `am broadcast` → A11y service 发送 +- `HANDOFF.md` — 完整部署地图 / 命令速查 / 避雷清单 / 架构 / 白名单授权 +- 其余 `index.js / wechat-*.js / *-frida.js / *-sqlite.js / test-*.js / *.apk / *.dex` **都不参与 bot 运行** + +## 环境铁律(细节见 HANDOFF §零) + +1. **bot 以 root 跑、agent 以 u0_a327 跑** → 看进程 / 读 root 文件 / 查微信 DB / 重启 都要 `su -c`(只有 Termux 侧能 su;proot 里的 claude-run 不行)。 +2. **唯一 live 日志 = 本目录 `auto-reply.log`**。别处任何同名副本都是死的(曾坑过接手者误判「bot 没在跑」)。 +3. claude-run 的 Bash 若报 `EACCES: permission denied, mkdir '/root/.claude/session-env'`:是 `~/.claude` 被 chown 成 root 了。在 **Termux shell**(不是 claude)跑 `su -c 'chown -R u0_a327:u0_a327 /data/data/com.termux/files/home/.claude'`,再重开 claude-run。 + +## 改代码 → 生效(内循环) + +1. **改**:claude-run 直接编辑本目录 `*.js`。 +2. **重启**(需 root → hermes / Termux 执行):`su -c 'pkill -f auto-reply.js'` → keepalive(每 15s 巡检)自动用新代码以 root 重启。 + 确认新进程:`su -c 'ps -ef | grep -E node.auto-reply.js | grep -v grep'`(**别用 `pgrep -f`,会自匹配 su shell**)。 +3. **看**:`tail -f ~/hermes-wechat/auto-reply.log`(claude-run 不带 su 也能读,日志 644)。 +4. **同步回 Mac**:手机无 git,改完把改动同步回 Mac repo `server/services/wechat/`,否则下次 Mac 跑 `deploy-to-phone.sh` 会覆盖你的改动。 + +## 调试设备状态的常用 su 命令(交给 hermes / Termux) + +```bash +# 查微信 DB 最近消息(确认 tracker 是否捕获/被过滤) +su -c 'cp /data/data/com.tencent.mm/MicroMsg/653cdc2320abb089e149a3f7a7683fcf/EnMicroMsg.db ~/_chk.db' +sqlcipher ~/_chk.db "PRAGMA key=\"e03ce81\"; PRAGMA cipher_compatibility=1; SELECT msgId,type,isSend,talker,substr(content,1,60) FROM message ORDER BY msgId DESC LIMIT 10;" +# 手动发测试消息 +su -c "am broadcast -a com.hermes.wechat.SEND_MSG --es toUser yibooo --es toUserDisplayName 火星人 --es content 'test'" +# A11y service 实际 UI 操作日志 +su -c 'logcat -s HermesA11y:*' +# A11y 被关了重新启用 +su -c 'settings put secure enabled_accessibility_services com.hermes.wechat.xposed/com.hermes.wechat.xposed.accessibility.HermesAccessibilityService; settings put secure accessibility_enabled 1' +``` + +## 已知 bug / TODO + +见 `HANDOFF.md` §八(避雷清单)和 §七(待办)。bot 回复模型当前是 hermes 默认的 Kimi-k2.6(claude-run 用 Claude 写代码;hermes 因 Anthropic 第三方计费策略不复用订阅 token)。 diff --git a/server/services/wechat/CLAUDE.md b/server/services/wechat/CLAUDE.md new file mode 100644 index 0000000..db4972d --- /dev/null +++ b/server/services/wechat/CLAUDE.md @@ -0,0 +1,10 @@ +# CLAUDE.md + +微信自动回复 bot 目录。接手维护 / 调试前,**先读 `AGENTS.md`(入口分工 + 环境铁律)和 `HANDOFF.md`(完整细节)**。 + +要点(细节见那两份): +- 这个文件夹在 Pixel 上是 `~/hermes-wechat/`,既是运行时也是唯一源(手机无 git,改完同步回此 Mac repo 目录)。 +- 混合分工:**claude-run(你,Claude)改代码**;**hermes(Termux,能 su)跑需要 root 的设备操作**(重启 bot / 查微信 DB / 发测试 / logcat)。 +- bot 以 root 跑、agent 以 u0_a327 跑 → 需 root 的步骤都要 `su -c`,proot 里的 claude-run 做不了,交给 hermes / Termux。 +- bot 实跑只依赖 `auto-reply.js` + `android-wechat-tracker-sqlcipher.js` + `android-wechat-sender.js`。 +- 唯一 live 日志 = 本目录 `auto-reply.log`。 diff --git a/server/services/wechat/HANDOFF.md b/server/services/wechat/HANDOFF.md index 3bc1baa..f8a7598 100644 --- a/server/services/wechat/HANDOFF.md +++ b/server/services/wechat/HANDOFF.md @@ -3,8 +3,31 @@ > **谁会读这份文档**:刚接手这台 Pixel 部署的 claude-run / 开发者,没有上下文。 > **目标**:5 分钟搞清楚正在跑什么、文件在哪、怎么改、怎么验证。 > -> **生成时间**:2026-05-25(v3,加 FSM / 流式 / hermes 补丁 / 白名单授权 章节)。请定期核对实际状态, -> 因为本文件不会随代码自动更新。 +> **生成时间**: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(见 §六)。 --- @@ -36,7 +59,8 @@ adb shell "su -c 'pgrep -x com.tencent.mm'" # 微信主进程 | 进程 | 启动方 | 作用 | |------|--------|------| -| `node auto-reply.js` | `~/hermes-wechat/start-ar.sh` 通过 `su -c` 起,**以 root 运行** | 微信回复主程序 | +| `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 自动化 | @@ -46,8 +70,9 @@ adb shell "su -c 'pgrep -x com.tencent.mm'" # 微信主进程 ## 二、文件位置 / 与 Mac repo 对应 -> Mac repo: `git@gitlab-wuwei.hexpm.com:yibo/pixel-wechat.git`(remote 名 `pixel-wechat`) -> 注意:手机上 **没有 git 仓库**,是 `adb push` 单文件部署的。 +> 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 微信回复主程序 @@ -229,34 +254,35 @@ NO_PROXY=127.0.0.1,localhost --- -## 六、给 claude-run 接手的两种开发流程 +## 六、改代码 / 部署 / 重启工作流 -### 方式 1(推荐):在 phone 上 clone 同一个 gitlab 仓库到独立目录 +手机上**没有 git 仓库**,运行时就是 `~/hermes-wechat/`。两种改法,看你在哪端: + +### 方式 A:手机上的 claude-run 直接改(最快,适合现场调试) ```bash -# 在 phone termux shell 里(不要污染现有 ~/hermes-wechat/) -cd ~ -git clone git@gitlab-wuwei.hexpm.com:yibo/pixel-wechat.git pixel-wechat-dev -# 或 HTTPS(需要 PAT): -# git clone https://gitlab-wuwei.hexpm.com/yibo/pixel-wechat.git pixel-wechat-dev - -# 编辑 ~/pixel-wechat-dev/server/services/wechat/*.js -# commit + push -git add -A && git commit -m "..." && git push pixel-wechat main - -# 部署到运行目录 -cp ~/pixel-wechat-dev/server/services/wechat/{auto-reply,android-wechat-sender,android-wechat-tracker-sqlcipher}.js ~/hermes-wechat/ - -# 重启 -pkill -f auto-reply.js -~/hermes-wechat/start-ar.sh & +# 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 ``` -> 第一次 push 需要在 phone 上配 git 用户 + SSH key 推到 gitlab,或者用 HTTPS PAT。 +### 方式 B:在 Mac 改 + deploy(适合正式提交) -### 方式 2:直接改 `~/hermes-wechat/` 下文件 + 手动 scp 回 Mac 提交 +```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 仓库 +``` -简单但容易和 Mac 端开发冲突,**不推荐多人协作**。 +> **冲突警告**:方式 A(手机直接改)和方式 B(Mac 改 deploy)会互相覆盖。同一时间只用一种。 +> 手机现场改完一定要尽快同步回 Mac,因为 Mac 端 `deploy-to-phone.sh` 会无条件覆盖手机文件。 --- @@ -301,6 +327,7 @@ pkill -f auto-reply.js | 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) | --- diff --git a/server/services/wechat/auto-reply.js b/server/services/wechat/auto-reply.js index 2f4c455..f63c097 100644 --- a/server/services/wechat/auto-reply.js +++ b/server/services/wechat/auto-reply.js @@ -826,7 +826,8 @@ class AutoReplyBot { if (CONFIG.replyAtInGroup && isAtMe(content, CONFIG.myNickname)) { replyType = 'group-at'; - } else if (CONFIG.replyQuoteInGroup && isQuoteMe(content, CONFIG.myNickname)) { + } else if (CONFIG.replyQuoteInGroup && isQuoteMe(rawContent, CONFIG.myNickname)) { + // 用 rawContent:normalize 已把 <refermsg> XML 剥掉,isQuoteMe 需要原始 XML 判断引用的是不是机器人 replyType = 'group-quote'; } } diff --git a/server/services/wechat/deploy-to-phone.sh b/server/services/wechat/deploy-to-phone.sh index 0f4f07e..6fa6073 100755 --- a/server/services/wechat/deploy-to-phone.sh +++ b/server/services/wechat/deploy-to-phone.sh @@ -1,5 +1,10 @@ #!/bin/bash -# deploy-to-phone.sh — 把 wechat 服务代码推送到 Pixel 手机 +# deploy-to-phone.sh — 把 wechat 运行时代码 + 文档推送到 Pixel 的 hermes-wechat 运行目录 +# +# 在 Mac 上运行。机制:adb push 到 /sdcard 中转 → su dd 落到 /data/data → chown 回 Termux uid。 +# - 用 dd 而不是 cp:避开 phone 上 dash shell snapshot hook 往 *.js 末尾追内容的坑 +# - chown 回 u0_a327:否则 adb/root 写出来的文件属主是 root,claude-run(u0_a327) 改不动 +# - 推完做 md5 校验 # # 用法: # ./deploy-to-phone.sh [手机目标目录] @@ -9,40 +14,44 @@ set -e SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" PHONE_DIR="${1:-/data/data/com.termux/files/home/hermes-wechat}" +TERMUX_UID="${TERMUX_UID:-u0_a327}" # Termux app 的 uid(claude-run 也是它)。换机器需改。 + +# 运行时必需:auto-reply.js 只 require 这三个文件,bot 实跑就这些 +RUNTIME="auto-reply.js android-wechat-tracker-sqlcipher.js android-wechat-sender.js" +# 文档(接手 / 维护必读)。AGENTS.md/CLAUDE.md 是 agent 一次性读的入口 +DOCS="AGENTS.md CLAUDE.md HANDOFF.md README.md SKILL.md" +FILES="$RUNTIME $DOCS" + +# md5 工具:Mac 用 `md5 -q`,Linux 用 `md5sum` +md5_local() { + if command -v md5 >/dev/null 2>&1; then md5 -q "$1"; else md5sum "$1" | awk '{print $1}'; fi +} -echo "=== Deploying wechat services to phone ===" +echo "=== Deploying wechat runtime + docs to phone ===" echo "Source: $SCRIPT_DIR" -echo "Target: $PHONE_DIR" +echo "Target: $PHONE_DIR (owner -> $TERMUX_UID)" echo "" -# 确保手机目录存在 adb shell "su -c 'mkdir -p $PHONE_DIR'" -# 推送所有 js 文件和 SKILL.md -for f in \ - android-wechat-sender.js \ - android-wechat-tracker.js \ - wechat-sender.js \ - wechat-tracker.js \ - wechat-reader.js \ - wechat-importer.js \ - index.js \ - test-phone.js \ - SKILL.md -do - if [ -f "$SCRIPT_DIR/$f" ]; then - echo "Pushing $f ..." - adb push "$SCRIPT_DIR/$f" "$PHONE_DIR/$f" >/dev/null - fi +fail=0 +for f in $FILES; do + if [ ! -f "$SCRIPT_DIR/$f" ]; then echo "SKIP (missing) $f"; continue; fi + echo "Pushing $f ..." + adb push "$SCRIPT_DIR/$f" "/sdcard/_deploy_$f" >/dev/null + adb shell "su -c 'dd if=/sdcard/_deploy_$f of=$PHONE_DIR/$f 2>/dev/null && chown $TERMUX_UID:$TERMUX_UID $PHONE_DIR/$f && chmod 644 $PHONE_DIR/$f'" + adb shell "rm -f /sdcard/_deploy_$f" >/dev/null 2>&1 || true + lmd5=$(md5_local "$SCRIPT_DIR/$f") + pmd5=$(adb shell "su -c 'md5sum $PHONE_DIR/$f'" | awk '{print $1}' | tr -d '\r') + if [ "$lmd5" = "$pmd5" ]; then echo " ok ($lmd5)"; else echo " !! MD5 MISMATCH local=$lmd5 phone=$pmd5"; fail=1; fi done echo "" -echo "=== Deploy complete ===" -echo "Files in $PHONE_DIR:" -adb shell "su -c 'ls -la $PHONE_DIR'" +if [ "$fail" = 0 ]; then echo "=== Deploy OK ==="; else echo "=== Deploy had MD5 mismatches — check above ==="; fi echo "" -echo "Quick test:" -echo " adb shell \"su -c 'cd $PHONE_DIR && node -e \\\\\" -const {sendTextMessageBroadcast} = require('./android-wechat-sender'); -sendTextMessageBroadcast('filehelper', 'hermes phone test').then(r=>console.log(r)); -\\\\\"'\"" +echo "重启 bot 让新代码生效(keepalive 会在 ~15s 内自动以 root 重新拉起):" +echo " adb shell \"su -c 'pkill -f auto-reply.js'\"" +echo "确认新进程起来了(不能用 pgrep -f,会自匹配 su shell):" +echo " adb shell \"su -c 'ps -ef | grep -E node.auto-reply.js | grep -v grep'\"" +echo "看 live 日志(注意:唯一真日志在 hermes-wechat/,别处副本是死的):" +echo " adb shell \"su -c 'tail -f $PHONE_DIR/auto-reply.log'\"" -- libgit2 0.21.4