Commit f9cd86566d1217340503881621ec9d9e84904d92
1 parent
de7dae33
docs(wechathook): 语音全合成 ✅ 写入 WECHAT_SEND_API — 内联 uploadvoice cgi
真机肉眼确认:SEND_VOICE_CGI 纯合成语音在 filehelper 渲染成绿色语音条(多条 2"/4"/6"/3" 全无红叹号=投递成功)。文档 §2 改写为全合成实现(分块 cv6 → modelbase.o 构造 → i.l()+pq5.g.l() 订阅派发),hot-swap 降级为旧路。TL;DR 新增 SEND_VOICE_CGI 行。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Showing
1 changed file
with
31 additions
and
8 deletions
xhs_lsposed_a16_fix/WECHAT_SEND_API.md
| ... | ... | @@ -9,7 +9,8 @@ |
| 9 | 9 | | 能力 | 状态 | 广播接口 | |
| 10 | 10 | |---|---|---| |
| 11 | 11 | | **文字(中英文)** | ✅ 完全可用,真机验证送达 | `SEND_MSG_INTERNAL --es to <wxid> --es text <文字>` 或 `--es text_b64 <base64>` | |
| 12 | -| **语音(TTS→SILK)** | ✅ 半自动 hot-swap(真机验证投递) | `ARM_VOICE_SWAP --es src_path <SILK> --ei duration <ms>` + 触发录音 | | |
| 12 | +| **语音(全合成)** | ✅ 纯 API 合成,真机验证渲染成语音条(无录音/无 hot-swap) | `SEND_VOICE_CGI --es to <wxid> --es src_path <SILK> --ei duration <ms>` | | |
| 13 | +| **语音(hot-swap,旧)** | ✅ 半自动 hot-swap(真机验证投递,全合成打通后可弃用) | `ARM_VOICE_SWAP --es src_path <SILK> --ei duration <ms>` + 触发录音 | | |
| 13 | 14 | | **图片** | ✅ 完全可用,全图 CDN 上传打通,真机验证全分辨率送达 | `SEND_IMAGE_INTERNAL --es to <wxid> --es path <路径>` | |
| 14 | 15 | |
| 15 | 16 | --- |
| ... | ... | @@ -83,18 +84,40 @@ w11.r0(talker, content, type=1, flag=0, Object=null) # Object-构造器插入 |
| 83 | 84 | |
| 84 | 85 | --- |
| 85 | 86 | |
| 86 | -## 2. 语音发送 ✅(半自动 hot-swap) | |
| 87 | +## 2. 语音发送 ✅(全合成,纯 API) | |
| 87 | 88 | |
| 88 | -纯合成(newsendmsg/r0/z2.b)永远卡「发送中」——音频上传锁死在 `jg0.x.w` Kotlin 协程里。 | |
| 89 | -**绕过方案**:让微信真录一段,在 CDN 上传步偷换 SILK 字节。 | |
| 89 | +**全合成打通(2026-07-07 真机验证渲染成语音条)**。之前以为纯合成必卡「发送中」是因为找错了分支: | |
| 90 | +本机语音走的是 **`jg0.x.w` 的内联 cgi 分支**(`/cgi-bin/micromsg-bin/uploadvoice`,SILK 内联在请求 | |
| 91 | +字段 7),不是 CDN 引擎。这条 cgi 一步同时上传音频+投递消息,且是标准 NetScene,可自己构建+派发。 | |
| 90 | 92 | |
| 93 | +```bash | |
| 94 | +# SILK 须 WeChat 进程可读(用其自身 voice2 文件,或 chown 到 WeChat uid;/sdcard 会 EACCES) | |
| 95 | +adb shell su -c 'am broadcast -a com.hermes.wechat.SEND_VOICE_CGI \ | |
| 96 | + --es to filehelper --es src_path <设备上SILK路径> --ei duration 2000' | |
| 97 | +``` | |
| 98 | + | |
| 99 | +### 内部实现(`sendVoiceViaCgi`) | |
| 91 | 100 | ``` |
| 92 | -偷换点 = jg0.x.w(self, qi3.d0, w21.w0, int type=4, int durMs, w21.t silk, Continuation) | |
| 93 | - arg5 w21.t 只有一个 byte[] 字段 = 明文 SILK 音频 | |
| 94 | -广播 ARM_VOICE_SWAP 装弹 → input swipe 触发录音键长按 → 偷换在 hookVoiceProbe 的 w 分支 | |
| 101 | +读 SILK → 按 6000 字节分块 → 每块: | |
| 102 | + cv6 请求 pb: set(0,self) set(1,talker) set(2,offset) set(3,len) set(4,"newSendVoice_"+voiceId) | |
| 103 | + set(6,dur) set(7,cu5(SILK切片)) set(8,末块?1:0) set(11,msgsource) set(12,4) set(15,createId) | |
| 104 | + → modelbase.o 13参构造器(reqPb,respPb,url,cmdid=127,0,0,true,0,0,false,0,false,null) # 绕 modelbase.l 字段名不匹配 | |
| 105 | + → modelbase.i.p(o) → scene = i 的 m1 字段(modelbase.g) | |
| 106 | + → 派发: i.l()(注册响应消费者+返 pq5.g deferred) + pq5.g.l(30000L)(终端订阅触发 cgi) | |
| 107 | +真机: VOICECGI-RESULT errType=0 errCode=0 svrMsgId(11)>0 = 服务器接受+分配msgId;filehelper 显示绿色语音条✅ | |
| 95 | 108 | ``` |
| 96 | 109 | |
| 97 | -完整细节见 memory `wechat-voice-send-internals.md` 和 `tts_voice_swap.sh`。 | |
| 110 | +**关键坑**: | |
| 111 | +- **派发必须 `i.l()`+订阅**,裸 `z2.b(scene)` 不 wire 回调 → 场景永不完成、dv6 空、svrMsgId=0。 | |
| 112 | +- **防 GC**:deferred 弱持有 i(CgiLifecycleObserver WeakReference),须静态强引用 i+deferred。 | |
| 113 | +- **字段名 base≠tinker**:`modelbase.l` 直接反射 `NoSuchFieldError`;用 `modelbase.o` 公开构造器 + 按签名找方法。 | |
| 114 | +- **SILK 权限**:hook 在 WeChat 进程读文件,`/sdcard` EACCES,root cp 进 app 沙盒被 SELinux 挡;用 voice2 自身文件。 | |
| 115 | + | |
| 116 | +### 旧路:半自动 hot-swap(全合成打通后可弃用) | |
| 117 | +让微信真录一段,在 `jg0.x.w`(arg5 `w21.t` 的 byte[] = 明文 SILK)偷换字节。广播 `ARM_VOICE_SWAP` 装弹 | |
| 118 | +→ `input swipe` 触发录音键长按 → 偷换在 `hookVoiceProbe` 的 w 分支。见 `tts_voice_swap.sh`。 | |
| 119 | + | |
| 120 | +完整逆向记录见 memory `wechat-voice-send-internals.md`。 | |
| 98 | 121 | |
| 99 | 122 | --- |
| 100 | 123 | ... | ... |