Commit ec27d4eb4a85ecd460f54b34040707b950aa7323

Authored by yibo
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>
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`(供交叉参考)。
... ...