Commit ec27d4eb4a85ecd460f54b34040707b950aa7323
1 parent
81d1c534
feat(wechat-auto-reply): 环节2 读上下文 — 补进 wechat-db-decrypt skill 配方(不写模块)
环节2 无需新代码:Hermes 是有 shell+SQL 的 agent,给 wechat-db-decrypt skill 补「读会话上下文」一节即可——一次拷库查齐发送者身份(rcontact,显示名优先级 conRemark>nickname>alias)、最近 N 条历史(剥群 wxid: 前缀/type→占位符/只喂 文本)、refermsg 引用解析 + 群名,并给出喂环节3 的上下文骨架。真机验证配方 产出干净上下文。仓库留档 skill/wechat-db-decrypt-SKILL.md。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Showing
2 changed files
with
152 additions
and
3 deletions
md/wechat-auto-reply-requirements.md
| ... | ... | @@ -60,9 +60,12 @@ |
| 60 | 60 | - 群 @我:群消息 content 前缀 `wxid:\n`,需匹配我的昵称判 @ |
| 61 | 61 | - 群引用我:看 refermsg 是否指向我的 svrid |
| 62 | 62 | |
| 63 | -### 环节 2 · 读取上下文 | |
| 64 | -- 从 `message` 表取该会话最近 N 条 + 发送者身份(`rcontact` 昵称/备注)。 | |
| 65 | -- **二期加分**:回链 lifelog `Person` 库做人物增强("这个人是谁、跟小火子什么关系")。一期不做。 | |
| 63 | +### 环节 2 · 读取上下文 —— ✅ **不写模块,走 skill 配方(2026-07-09)** | |
| 64 | +结论:Hermes 是有 shell+SQL 的 agent,已有 `wechat-db-decrypt` skill,环节2 无需新代码。 | |
| 65 | +- 给 skill 补了「读会话上下文」一节(`~/.hermes/skills/wechat-db-decrypt/SKILL.md`,仓库留档 `wechat_plain/scripts/auto-reply/skill/`):一次拷库查齐 ①发送者身份 `rcontact`(显示名优先级 conRemark>nickname>alias)②最近 N 条 `message` 历史(剥群 `wxid:` 前缀、type→占位符、只喂文本)③引用 refermsg 解析 + 群名;末尾给出喂 env3 的上下文骨架。 | |
| 66 | +- 真机验证配方查询产出干净上下文(身份+时间正序历史)。 | |
| 67 | +- 架构澄清:环节 3(生成)= Hermes 这个 LLM 本身;环节 4(策略)= 它的推理;只有环节 4/5 的「审核队列 + viewer 接发送」还需写代码。 | |
| 68 | +- **二期加分**:回链 lifelog `Person` 库做人物增强。一期不做。 | |
| 66 | 69 | |
| 67 | 70 | ### 环节 3 · 生成回复 |
| 68 | 71 | - Hermes(Kimi / 本地 llama-swap):**上下文 + SOUL.md → 回复**。 | ... | ... |
wechat_plain/scripts/auto-reply/skill/wechat-db-decrypt-SKILL.md
0 → 100644
| 1 | +--- | |
| 2 | +name: wechat-db-decrypt | |
| 3 | +description: 端上解密打开微信 EnMicroMsg.db(SQLCipher),拿聊天/联系人/朋友圈原始数据。纯本地 su,不经 Mac/Frida。含 key 推导 + 多账号选库 + 已验证密钥缓存。 | |
| 4 | +version: 1.0 | |
| 5 | +category: communication | |
| 6 | +--- | |
| 7 | + | |
| 8 | +# WeChat DB Decrypt(微信数据库端上解密) | |
| 9 | + | |
| 10 | +微信的 `EnMicroMsg.db` 是 **SQLCipher 加密**的。这个 skill 让你在设备内直接算出密钥、拷库、解密只读打开, | |
| 11 | +拿到 `rcontact`(联系人)、`message`(聊天)、`SnsInfo`(朋友圈)等原始表。**纯 su + sqlcipher,不经 Mac、不用 Frida。** | |
| 12 | + | |
| 13 | +## When to Use | |
| 14 | +- 需要读微信联系人 / 聊天记录 / 朋友圈的**结构化原始数据**(不是截图 OCR) | |
| 15 | +- 给 lifelog / CRM 灌微信数据、做人物档案、群消息摘要 | |
| 16 | +- 任何"要查微信数据库"的任务 | |
| 17 | + | |
| 18 | +## 密钥推导公式(真机验证) | |
| 19 | + | |
| 20 | +``` | |
| 21 | +key = MD5( IMEI + uin ) 取 hex 前 7 位 | |
| 22 | +IMEI = "1234567890ABCDEF" # 微信默认值,本机适用 | |
| 23 | +uin = 从 shared_prefs 读 last_login_uin(当前登录账号) | |
| 24 | +SQLCipher 参数: cipher_compatibility = 1, cipher_page_size = 1024 | |
| 25 | +``` | |
| 26 | + | |
| 27 | +## ⭐ 已验证密钥缓存(2026-07-08,先查这里,命中就别重算) | |
| 28 | + | |
| 29 | +| uin | MicroMsg 目录 | key | 库大小 | 备注 | | |
| 30 | +|---|---|---|---|---| | |
| 31 | +| 257966415 | `ec4f423b316d41e9af5e8398155cf105` | `1986554` | ~90MB | **当前主账号**,已实测 720 表 / rcontact 42271 条 | | |
| 32 | + | |
| 33 | +> 只要 `last_login_uin` 还是 257966415,直接用 key `1986554`,跳过推导。 | |
| 34 | +> 换号了才需要走下面的"重新推导"。 | |
| 35 | + | |
| 36 | +## 选对账号目录(多账号必看) | |
| 37 | + | |
| 38 | +设备上 `MicroMsg/` 下可能有**多个账号目录**,目录名 = `MD5("mm" + uin)`。 | |
| 39 | +**不要用 `ls | head -1`**——那会按字典序选错账号。正确定位: | |
| 40 | + | |
| 41 | +```bash | |
| 42 | +BASE=/data/user/0/com.tencent.mm | |
| 43 | +UIN=$(su --mount-master -c "grep -hoE 'last_login_uin\">[0-9-]+' $BASE/shared_prefs/*.xml" | grep -oE '[0-9-]+' | head -1) | |
| 44 | +DIR=$(printf '%s' "mm${UIN}" | md5sum | cut -c1-32) # = 当前登录账号的目录名 | |
| 45 | +echo "uin=$UIN dir=$DIR" | |
| 46 | +``` | |
| 47 | + | |
| 48 | +## 完整流程(重新推导 + 打开) | |
| 49 | + | |
| 50 | +```bash | |
| 51 | +BASE=/data/user/0/com.tencent.mm | |
| 52 | +export PATH=/system/bin:/system/xbin:$PATH | |
| 53 | + | |
| 54 | +# 1. 读当前账号 uin,算目录、算 key | |
| 55 | +UIN=$(su --mount-master -c "grep -hoE 'last_login_uin\">[0-9-]+' $BASE/shared_prefs/*.xml" | grep -oE '[0-9-]+' | head -1) | |
| 56 | +DIR=$(printf '%s' "mm${UIN}" | md5sum | cut -c1-32) | |
| 57 | +KEY=$(printf '%s' "1234567890ABCDEF${UIN}" | md5sum | cut -c1-7) | |
| 58 | +echo "uin=$UIN key=$KEY dir=$DIR" | |
| 59 | + | |
| 60 | +# 2. su 拷出该账号的库到可读位置(含 wal/shm,保证最新数据) | |
| 61 | +su --mount-master -c "D=$BASE/MicroMsg/$DIR; cp \$D/EnMicroMsg.db /sdcard/en.db; \ | |
| 62 | + [ -f \$D/EnMicroMsg.db-wal ] && cp \$D/EnMicroMsg.db-wal /sdcard/en.db-wal; \ | |
| 63 | + [ -f \$D/EnMicroMsg.db-shm ] && cp \$D/EnMicroMsg.db-shm /sdcard/en.db-shm; \ | |
| 64 | + chmod 666 /sdcard/en.db*" | |
| 65 | + | |
| 66 | +# 3. sqlcipher 打开验证 + 查数据 | |
| 67 | +sqlcipher /sdcard/en.db <<EOF | |
| 68 | +PRAGMA key = '$KEY'; | |
| 69 | +PRAGMA cipher_compatibility = 1; | |
| 70 | +PRAGMA cipher_page_size = 1024; | |
| 71 | +SELECT count(*) FROM sqlite_master; -- 能出数字=解开了 | |
| 72 | +SELECT count(*) FROM rcontact; -- 联系人数 | |
| 73 | +-- .headers on | |
| 74 | +-- SELECT username, nickname, conRemark FROM rcontact LIMIT 20; | |
| 75 | +EOF | |
| 76 | + | |
| 77 | +# 4. 用完删明文 | |
| 78 | +rm -f /sdcard/en.db* | |
| 79 | +``` | |
| 80 | + | |
| 81 | +若返回 `file is not a database (26)`:key 不对 → 兜底把 IMEI 换成空串再试一把 | |
| 82 | +(`KEY=$(printf '%s' "${UIN}" | md5sum | cut -c1-7)`);仍不行则微信大版本可能改了 KDF 参数,需重新逆向。 | |
| 83 | + | |
| 84 | +## 两个 su 坑(否则静默失败) | |
| 85 | + | |
| 86 | +1. **必须 `su --mount-master`**:否则看不到 `com.tencent.mm` 的真实数据目录(mount namespace 隔离)。 | |
| 87 | +2. **`export PATH=/system/bin:/system/xbin`**:app 侧起的 su 环境 PATH 可能是空的,`grep/ls/cp/md5sum` 全找不到。 | |
| 88 | + | |
| 89 | +## 常用表速查 | |
| 90 | +- `rcontact` — 联系人(username 微信号, nickname 昵称, conRemark 备注, alias 微信号别名) | |
| 91 | +- `message` — 聊天消息(talker 会话对象, content 内容, createTime, isSend, type 类型) | |
| 92 | +- `SnsInfo` — 朋友圈 | |
| 93 | +- `img_flag` / 头像等资源在 `MicroMsg/<dir>/avatar/` 及独立库 | |
| 94 | + | |
| 95 | +--- | |
| 96 | + | |
| 97 | +## 读会话上下文(自动回复·环节2:给回复生成备料) | |
| 98 | + | |
| 99 | +拿到一条触发事件 `{talker, isGroup, sender(wxid), kind}`(来自 watcher 的 `incoming.jsonl`), | |
| 100 | +**一次拷库**即可查齐下面三块,拼成喂给 LLM 的上下文。别分三次拷库。 | |
| 101 | + | |
| 102 | +### 1. 发送者身份 | |
| 103 | +```sql | |
| 104 | +SELECT nickname, conRemark, alias FROM rcontact WHERE username='<sender_wxid>'; | |
| 105 | +``` | |
| 106 | +- 私聊:sender = talker;群聊:sender = 群消息 `wxid:` 前缀那个 | |
| 107 | +- **显示名优先级**:conRemark(备注) > nickname(昵称) > alias > wxid | |
| 108 | +- 群名:`SELECT nickname FROM rcontact WHERE username='<talker@chatroom>';`(群的 nickname 就是群名) | |
| 109 | + | |
| 110 | +### 2. 最近 N 条会话历史 | |
| 111 | +```sql | |
| 112 | +SELECT isSend, type, createTime, content FROM message | |
| 113 | +WHERE talker='<talker>' ORDER BY msgId DESC LIMIT 20; -- 再按时间正序排回来 | |
| 114 | +``` | |
| 115 | +解析规则: | |
| 116 | +- `isSend=1` → 我(小火子)发的;`isSend=0` → 对方发的 | |
| 117 | +- 群消息 content 形如 `wxid_xxx:\n正文`,剥前缀取「发送者 wxid + 正文」,每个 wxid 再走身份查询解析显示名 | |
| 118 | +- `type`:`1`=纯文本;`3`=图片;`34`=语音;`43`=视频;`47`=表情;`49`=appmsg(链接/文件/引用/聊天记录);大 type + 内容含「撤回了一条消息 / 拍了拍」= 系统消息(忽略) | |
| 119 | +- 只把**文本**喂给 LLM;非文本给占位符 `[图片]`/`[语音]`/`[链接:标题]`,别把整坨 XML 塞进去 | |
| 120 | + | |
| 121 | +### 3. 引用原文(kind=group_quote_me,或消息含 `<refermsg>`) | |
| 122 | +type=49 且 appmsg `<type>57</type>`,`<refermsg>` 块内: | |
| 123 | +- `<fromusr>` / `<chatusr>` = 被引消息作者 wxid | |
| 124 | +- `<displayname>` = 作者显示名 | |
| 125 | +- `<content>` = 被引原文(群里被引消息也可能带 `wxid:` 前缀,同样剥) | |
| 126 | +把 `<content>` 当作「对方在引用这句话回复你」。(真样例少,遇到先 dump 一条核对字段。) | |
| 127 | + | |
| 128 | +### 组装(喂环节3 的上下文骨架) | |
| 129 | +``` | |
| 130 | +[群] {群名} ← 群聊才有 | |
| 131 | +对方:{显示名}({备注或wxid}) | |
| 132 | +[引用] {被引作者}: {被引原文} ← 有引用才有 | |
| 133 | +最近对话: | |
| 134 | + {时间} {对方显示名}: {文本} | |
| 135 | + {时间} 我: {文本} | |
| 136 | + ... | |
| 137 | +本条:{对方显示名}: {正文} | |
| 138 | +``` | |
| 139 | + | |
| 140 | +### 注意 | |
| 141 | +- **官方号**(username 以 `gh_` 开头)= 公众号推送,不是真人,通常不回(策略层过滤) | |
| 142 | +- 群成员的「群昵称」(per-group) 在 chatroom 表的 displayname,比 rcontact.nickname 更贴切但要多查一次;MVP 先用 rcontact 的名字够用 | |
| 143 | + | |
| 144 | +## 说明 | |
| 145 | +- key 是**确定性**的:uin 不变 → key 不变。没有"独立 key 文件"存在别处,缓存表就是唯一的省算路径。 | |
| 146 | +- app 侧同逻辑源码在仓库 `lifelog-recorder/.../vision/WeChatDb.kt`(供交叉参考)。 | ... | ... |