From f9cd86566d1217340503881621ec9d9e84904d92 Mon Sep 17 00:00:00 2001 From: yibo Date: Tue, 7 Jul 2026 16:07:46 +0800 Subject: [PATCH] docs(wechathook): 语音全合成 ✅ 写入 WECHAT_SEND_API — 内联 uploadvoice cgi --- xhs_lsposed_a16_fix/WECHAT_SEND_API.md | 39 +++++++++++++++++++++++++++++++-------- 1 file changed, 31 insertions(+), 8 deletions(-) diff --git a/xhs_lsposed_a16_fix/WECHAT_SEND_API.md b/xhs_lsposed_a16_fix/WECHAT_SEND_API.md index bebc1cf..a039119 100644 --- a/xhs_lsposed_a16_fix/WECHAT_SEND_API.md +++ b/xhs_lsposed_a16_fix/WECHAT_SEND_API.md @@ -9,7 +9,8 @@ | 能力 | 状态 | 广播接口 | |---|---|---| | **文字(中英文)** | ✅ 完全可用,真机验证送达 | `SEND_MSG_INTERNAL --es to --es text <文字>` 或 `--es text_b64 ` | -| **语音(TTS→SILK)** | ✅ 半自动 hot-swap(真机验证投递) | `ARM_VOICE_SWAP --es src_path --ei duration ` + 触发录音 | +| **语音(全合成)** | ✅ 纯 API 合成,真机验证渲染成语音条(无录音/无 hot-swap) | `SEND_VOICE_CGI --es to --es src_path --ei duration ` | +| **语音(hot-swap,旧)** | ✅ 半自动 hot-swap(真机验证投递,全合成打通后可弃用) | `ARM_VOICE_SWAP --es src_path --ei duration ` + 触发录音 | | **图片** | ✅ 完全可用,全图 CDN 上传打通,真机验证全分辨率送达 | `SEND_IMAGE_INTERNAL --es to --es path <路径>` | --- @@ -83,18 +84,40 @@ w11.r0(talker, content, type=1, flag=0, Object=null) # Object-构造器插入 --- -## 2. 语音发送 ✅(半自动 hot-swap) +## 2. 语音发送 ✅(全合成,纯 API) -纯合成(newsendmsg/r0/z2.b)永远卡「发送中」——音频上传锁死在 `jg0.x.w` Kotlin 协程里。 -**绕过方案**:让微信真录一段,在 CDN 上传步偷换 SILK 字节。 +**全合成打通(2026-07-07 真机验证渲染成语音条)**。之前以为纯合成必卡「发送中」是因为找错了分支: +本机语音走的是 **`jg0.x.w` 的内联 cgi 分支**(`/cgi-bin/micromsg-bin/uploadvoice`,SILK 内联在请求 +字段 7),不是 CDN 引擎。这条 cgi 一步同时上传音频+投递消息,且是标准 NetScene,可自己构建+派发。 +```bash +# SILK 须 WeChat 进程可读(用其自身 voice2 文件,或 chown 到 WeChat uid;/sdcard 会 EACCES) +adb shell su -c 'am broadcast -a com.hermes.wechat.SEND_VOICE_CGI \ + --es to filehelper --es src_path <设备上SILK路径> --ei duration 2000' +``` + +### 内部实现(`sendVoiceViaCgi`) ``` -偷换点 = jg0.x.w(self, qi3.d0, w21.w0, int type=4, int durMs, w21.t silk, Continuation) - arg5 w21.t 只有一个 byte[] 字段 = 明文 SILK 音频 -广播 ARM_VOICE_SWAP 装弹 → input swipe 触发录音键长按 → 偷换在 hookVoiceProbe 的 w 分支 +读 SILK → 按 6000 字节分块 → 每块: + cv6 请求 pb: set(0,self) set(1,talker) set(2,offset) set(3,len) set(4,"newSendVoice_"+voiceId) + set(6,dur) set(7,cu5(SILK切片)) set(8,末块?1:0) set(11,msgsource) set(12,4) set(15,createId) + → modelbase.o 13参构造器(reqPb,respPb,url,cmdid=127,0,0,true,0,0,false,0,false,null) # 绕 modelbase.l 字段名不匹配 + → modelbase.i.p(o) → scene = i 的 m1 字段(modelbase.g) + → 派发: i.l()(注册响应消费者+返 pq5.g deferred) + pq5.g.l(30000L)(终端订阅触发 cgi) +真机: VOICECGI-RESULT errType=0 errCode=0 svrMsgId(11)>0 = 服务器接受+分配msgId;filehelper 显示绿色语音条✅ ``` -完整细节见 memory `wechat-voice-send-internals.md` 和 `tts_voice_swap.sh`。 +**关键坑**: +- **派发必须 `i.l()`+订阅**,裸 `z2.b(scene)` 不 wire 回调 → 场景永不完成、dv6 空、svrMsgId=0。 +- **防 GC**:deferred 弱持有 i(CgiLifecycleObserver WeakReference),须静态强引用 i+deferred。 +- **字段名 base≠tinker**:`modelbase.l` 直接反射 `NoSuchFieldError`;用 `modelbase.o` 公开构造器 + 按签名找方法。 +- **SILK 权限**:hook 在 WeChat 进程读文件,`/sdcard` EACCES,root cp 进 app 沙盒被 SELinux 挡;用 voice2 自身文件。 + +### 旧路:半自动 hot-swap(全合成打通后可弃用) +让微信真录一段,在 `jg0.x.w`(arg5 `w21.t` 的 byte[] = 明文 SILK)偷换字节。广播 `ARM_VOICE_SWAP` 装弹 +→ `input swipe` 触发录音键长按 → 偷换在 `hookVoiceProbe` 的 w 分支。见 `tts_voice_swap.sh`。 + +完整逆向记录见 memory `wechat-voice-send-internals.md`。 --- -- libgit2 0.21.4