Commit f9cd86566d1217340503881621ec9d9e84904d92

Authored by yibo
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>
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  
... ...