diff --git a/onebot/main.go b/onebot/main.go
index e116291..a72d50b 100644
--- a/onebot/main.go
+++ b/onebot/main.go
@@ -61,7 +61,7 @@ func initFlag() {
flag.StringVar(&config.FridaGadgetAddr, "gadget_addr", "127.0.0.1:27042", "Gadget 地址: 127.0.0.1:27042 仅当 type 为 gadget 时有效")
flag.StringVar(&config.OnebotToken, "token", "MuseBot", "OneBot Token: MuseBot")
flag.StringVar(&config.ImagePath, "image_path", "", "图片路径: /Users/xxx/Library/Containers/com.tencent.xinWeChat/Data/Documents/xwechat_files/xxx/temp/xxx/2026-01/Img/")
- flag.StringVar(&config.WechatConf, "wechat_conf", "../wechat_version/4_1_10_53_mac.json", "微信配置文件路径: ../wechat_version/4_1_6_12_mac.json")
+ flag.StringVar(&config.WechatConf, "wechat_conf", "../wechat_version/4_1_11_53_mac.json", "微信配置文件路径: ../wechat_version/4_1_6_12_mac.json")
flag.StringVar(&config.ConnType, "conn_type", "http", "连接类型: http | websocket")
flag.IntVar(&config.SendInterval, "send_interval", 1000, "发送间隔: ms")
flag.IntVar(&config.WechatPid, "wechat_pid", 0, "微信进程 PID,不设置则自动查找")
diff --git a/onebot/msg.go b/onebot/msg.go
index 92da9f7..16ba268 100644
--- a/onebot/msg.go
+++ b/onebot/msg.go
@@ -11,7 +11,10 @@ func cleanExpiredDownloads() {
now := time.Now().UnixMilli()
userID2FileMsgMap.Range(func(key, value any) bool {
req := value.(*DownloadRequest)
- if now-req.LastAppendTime > 24*60*60*1000 {
+ req.mu.Lock()
+ expired := now-req.LastAppendTime > 24*60*60*1000
+ req.mu.Unlock()
+ if expired {
userID2FileMsgMap.Delete(key)
}
return true
@@ -26,24 +29,33 @@ func Download(rawMsg []byte) error {
Error("JSON解析失败", "err", err)
return err
}
-
+
Info("下载文件", "file_id", downloadReq.FileID, "media_len", len(downloadReq.Media), "cdn_url", downloadReq.CDNURL)
- if downloadReqInter, ok := userID2FileMsgMap.Load(downloadReq.CDNURL); ok {
- beforeDownloadReq := downloadReqInter.(*DownloadRequest)
- if beforeDownloadReq.FilePath != "" {
- return nil
- }
- if time.Now().UnixMilli()-beforeDownloadReq.LastAppendTime > 10000000 {
- beforeDownloadReq.Media = downloadReq.Media
- } else {
- beforeDownloadReq.Media = append(beforeDownloadReq.Media, downloadReq.Media...)
- }
-
- beforeDownloadReq.LastAppendTime = time.Now().UnixMilli()
- } else {
- downloadReq.LastAppendTime = time.Now().UnixMilli()
- userID2FileMsgMap.Store(downloadReq.CDNURL, downloadReq)
+
+ // LoadOrStore 保证同一 CDNURL 只建一份,消除 Load+Store 之间的竞态。
+ // 首个分片直接作为占位存入,后续分片走 append 分支。
+ actual, loaded := userID2FileMsgMap.LoadOrStore(downloadReq.CDNURL, downloadReq)
+ req := actual.(*DownloadRequest)
+
+ req.mu.Lock()
+ defer req.mu.Unlock()
+
+ if !loaded {
+ // 本次就是首个分片(downloadReq 已被存入),只更新时间戳
+ req.LastAppendTime = time.Now().UnixMilli()
+ return nil
}
-
+
+ if req.FilePath != "" {
+ return nil
+ }
+ if time.Now().UnixMilli()-req.LastAppendTime > 10000000 {
+ // 距上次追加过久,视为新文件,重置数据
+ req.Media = append(req.Media[:0], downloadReq.Media...)
+ } else {
+ req.Media = append(req.Media, downloadReq.Media...)
+ }
+ req.LastAppendTime = time.Now().UnixMilli()
+
return nil
}
diff --git a/onebot/param.go b/onebot/param.go
index f7429a0..9520047 100644
--- a/onebot/param.go
+++ b/onebot/param.go
@@ -211,6 +211,8 @@ type DownloadRequest struct {
CDNURL string `json:"cdn_url"`
LastAppendTime int64 `json:"last_append_time"`
FilePath string `json:"file_path"`
+
+ mu sync.Mutex `json:"-"` // 保护 Media/LastAppendTime/FilePath 的并发读写
}
type ScriptMessage struct {
diff --git a/onebot/protobuf_dump_test.go b/onebot/protobuf_dump_test.go
new file mode 100644
index 0000000..c4018e9
--- /dev/null
+++ b/onebot/protobuf_dump_test.go
@@ -0,0 +1,246 @@
+package main
+
+import (
+ "bytes"
+ "encoding/json"
+ "fmt"
+ "strings"
+ "testing"
+)
+
+// 把你抓到的 hexdump 直接粘到这里(支持地址列 + hex + ASCII 的完整格式)。
+const dumpInput = `
+0000000000000000 08 00 12 AD 10 08 02 12 A7 0D 08 05 12 A2 0D 08 ................
+0000000000000010 9C 0D 12 9C 0D 08 ED EA 92 D5 01 12 15 0A 13 77 ...............w
+0000000000000020 78 69 64 5F 6C 64 66 74 75 68 65 33 36 69 7A 67 xid_ldftuhe36izg
+0000000000000030 31 39 1A 16 0A 14 34 35 38 37 32 30 32 35 33 38 19....4587202538
+0000000000000040 34 40 63 68 61 74 72 6F 6F 6D 20 03 2A A0 0A 0A 4@chatroom .*...
+0000000000000050 9D 0A 3C 3F 78 6D 6C 20 76 65 72 73 69 6F 6E 3D .....<
+0000000000000070 69 6D 67 20 61 65 73 6B 65 79 3D 22 39 61 34 39 img aeskey="9a49
+0000000000000080 36 38 32 32 61 31 62 35 35 30 65 66 34 62 39 65 6822a1b550ef4b9e
+0000000000000090 63 64 31 62 30 36 31 30 34 35 38 66 22 20 65 6E cd1b0610458f" en
+00000000000000A0 63 72 79 76 65 72 3D 22 31 22 20 63 64 6E 74 68 cryver="1" cdnth
+00000000000000B0 75 6D 62 61 65 73 6B 65 79 3D 22 39 61 34 39 36 umbaeskey="9a496
+00000000000000C0 38 32 32 61 31 62 35 35 30 65 66 34 62 39 65 63 822a1b550ef4b9ec
+00000000000000D0 64 31 62 30 36 31 30 34 35 38 66 22 20 63 64 6E d1b0610458f" cdn
+00000000000000E0 74 68 75 6D 62 75 72 6C 3D 22 33 30 35 66 30 32 thumburl="305f02
+00000000000000F0 30 31 30 30 30 34 34 62 33 30 34 39 30 32 30 31 0100044b30490201
+0000000000000100 30 30 30 32 30 34 66 36 35 63 39 63 65 30 30 32 000204f65c9ce002
+0000000000000110 30 33 32 66 35 63 33 39 30 32 30 34 32 33 30 30 032f5c3902042300
+0000000000000120 30 63 37 39 30 32 30 34 36 61 34 66 34 64 63 66 0c7902046a4f4dcf
+0000000000000130 30 34 32 34 33 34 36 32 36 34 36 33 33 33 33 39 0424346264633339
+0000000000000140 33 38 33 32 32 64 33 37 33 34 33 33 33 33 32 64 38322d373433332d
+0000000000000150 33 34 33 36 36 34 36 33 32 64 36 31 33 34 33 34 343664632d613434
+0000000000000160 33 30 32 64 33 33 33 32 36 35 33 36 36 32 33 37 302d333265366237
+0000000000000170 33 30 36 34 33 33 33 37 36 32 33 31 30 32 30 34 3064333762310204
+0000000000000180 30 35 31 35 30 61 30 32 30 32 30 31 30 30 30 34 05150a0202010004
+0000000000000190 30 64 30 30 34 63 35 61 65 31 30 30 30 30 30 30 0d004c5ae1000000
+00000000000001A0 30 30 30 30 30 30 30 30 30 30 30 30 22 20 63 64 000000000000" cd
+00000000000001B0 6E 74 68 75 6D 62 6C 65 6E 67 74 68 3D 22 39 38 nthumblength="98
+00000000000001C0 37 37 22 20 63 64 6E 74 68 75 6D 62 68 65 69 67 77" cdnthumbheig
+00000000000001D0 68 74 3D 22 32 31 30 22 20 63 64 6E 74 68 75 6D ht="210" cdnthum
+00000000000001E0 62 77 69 64 74 68 3D 22 31 31 38 22 20 63 64 6E bwidth="118" cdn
+00000000000001F0 6D 69 64 68 65 69 67 68 74 3D 22 30 22 20 63 64 midheight="0" cd
+0000000000000200 6E 6D 69 64 77 69 64 74 68 3D 22 30 22 20 63 64 nmidwidth="0" cd
+0000000000000210 6E 68 64 68 65 69 67 68 74 3D 22 30 22 20 63 64 nhdheight="0" cd
+0000000000000220 6E 68 64 77 69 64 74 68 3D 22 30 22 20 63 64 6E nhdwidth="0" cdn
+0000000000000230 6D 69 64 69 6D 67 75 72 6C 3D 22 33 30 35 66 30 midimgurl="305f0
+0000000000000240 32 30 31 30 30 30 34 34 62 33 30 34 39 30 32 30 20100044b3049020
+0000000000000250 31 30 30 30 32 30 34 66 36 35 63 39 63 65 30 30 1000204f65c9ce00
+0000000000000260 32 30 33 32 66 35 63 33 39 30 32 30 34 32 33 30 2032f5c390204230
+0000000000000270 30 30 63 37 39 30 32 30 34 36 61 34 66 34 64 63 00c7902046a4f4dc
+0000000000000280 66 30 34 32 34 33 34 36 32 36 34 36 33 33 33 33 f042434626463333
+0000000000000290 39 33 38 33 32 32 64 33 37 33 34 33 33 33 33 32 938322d373433332
+00000000000002A0 64 33 34 33 36 36 34 36 33 32 64 36 31 33 34 33 d343664632d61343
+00000000000002B0 34 33 30 32 64 33 33 33 32 36 35 33 36 36 32 33 4302d33326536623
+00000000000002C0 37 33 30 36 34 33 33 33 37 36 32 33 31 30 32 30 7306433376231020
+00000000000002D0 34 30 35 31 35 30 61 30 32 30 32 30 31 30 30 30 405150a020201000
+00000000000002E0 34 30 64 30 30 34 63 35 61 65 31 30 30 30 30 30 40d004c5ae100000
+00000000000002F0 30 30 30 30 30 30 30 30 30 30 30 30 30 22 20 6C 0000000000000" l
+0000000000000300 65 6E 67 74 68 3D 22 34 37 32 35 31 22 20 6D 64 ength="47251" md
+0000000000000310 35 3D 22 36 31 62 38 32 39 62 65 34 38 62 39 39 5="61b829be48b99
+0000000000000320 65 34 65 36 36 62 65 36 65 36 64 62 61 39 33 34 e4e66be6e6dba934
+0000000000000330 63 33 32 22 20 68 65 76 63 5F 6D 69 64 5F 73 69 c32" hevc_mid_si
+0000000000000340 7A 65 3D 22 34 37 32 35 31 22 20 6F 72 69 67 69 ze="47251" origi
+0000000000000350 6E 73 6F 75 72 63 65 6D 64 35 3D 22 62 63 39 31 nsourcemd5="bc91
+0000000000000360 66 37 62 34 61 39 39 64 31 37 65 36 31 66 36 32 f7b4a99d17e61f62
+0000000000000370 35 37 64 31 32 66 39 32 33 30 36 30 22 3E 0A 09 57d12f923060">..
+0000000000000380 09 3C 73 65 63 48 61 73 68 49 6E 66 6F 42 61 73 .eyJwaGFzaCI6
+00000000000003A0 49 6A 45 77 4F 54 45 32 4D 6A 45 77 4E 54 49 79 IjEwOTE2MjEwNTIy
+00000000000003B0 4D 44 45 77 4E 44 41 69 4C 43 4A 77 5A 48 46 6F MDEwNDAiLCJwZHFo
+00000000000003C0 59 58 4E 6F 49 6A 6F 69 4F 44 51 77 59 7A 55 33 YXNoIjoiODQwYzU3
+00000000000003D0 4D 47 49 33 59 54 4D 77 4D 57 45 7A 5A 57 45 32 MGI3YTMwMWEzZWE2
+00000000000003E0 4E 6A 4E 6C 4F 57 59 32 4E 54 6B 77 5A 6A 56 6D NjNlOWY2NTkwZjVm
+00000000000003F0 4F 44 68 68 4E 6D 45 7A 5A 57 45 33 4D 47 59 34 ODhhNmEzZWE3MGY4
+0000000000000400 4E 47 51 77 4E 57 4D 7A 4D 44 63 77 4D 47 59 34 NGQwNWMzMDcwMGY4
+0000000000000410 4D 44 4D 33 4F 44 4E 6D 5A 6D 5A 6D 5A 43 4A 39 MDM3ODNmZmZmZCJ9
+0000000000000420 3C 2F 73 65 63 48 61 73 68 49 6E 66 6F 42 61 73 ......
+0000000000000440 09 3C 64 75 72 61 74 69 6F 6E 3E 30 3C 2F 64 75 .0....0............0<
+0000000000000490 2F 68 64 73 69 7A 65 3E 0A 09 09 09 3C 68 64 6D /hdsize>............0.
+00000000000004E0 09 09 3C 2F 6C 69 76 65 3E 0A 09 3C 2F 69 6D 67 ..........
.....
+0000000000000540 09 3C 42 69 7A 54 79 70 65 3E 30 3C 2F 42 69 7A .0....0
+0000000000000570 03 38 02 42 02 08 00 48 E4 EF BD D2 06 52 9B 02 .8.B...H.....R..
+0000000000000580 3C 6D 73 67 73 6F 75 72 63 65 3E 0A 09 3C 62 69 ..0.....8b11b3
+00000000000005C0 66 63 32 33 35 34 30 33 62 36 63 65 34 37 32 33 fc235403b6ce4723
+00000000000005D0 62 62 65 62 31 39 62 64 31 35 5F 3C 2F 75 75 69 bbeb19bd15_......
+0000000000000610 0A 09 3C 2F 73 65 63 5F 6D 73 67 5F 6E 6F 64 65 ....N0
+0000000000000630 5F 56 31 5F 6D 74 6F 4A 64 6D 49 35 7C 76 31 5F _V1_mtoJdmI5|v1_
+0000000000000640 6E 6B 4D 6F 79 50 6F 55 3C 2F 73 69 67 6E 61 74 nkMoyPoU..
+0000000000000660 0A 09 09 3C 70 75 62 6C 69 73 68 65 72 2D 69 64 ...
+0000000000000680 0A 09 3C 2F 74 6D 70 5F 6E 6F 64 65 3E 0A 3C 2F ...
+0000000000000690 6D 73 67 73 6F 75 72 63 65 3E 0A 60 FC 96 DB BC msgsource>.....
+00000000000006A0 D3 F9 C6 BB 65 68 F8 B8 AC D4 02 72 02 08 00 78 ....eh.....r...x
+00000000000006B0 00 12 FE 02 08 05 12 F9 02 08 F3 02 12 F3 02 08 ................
+00000000000006C0 8E F9 82 DC 06 12 15 0A 13 77 78 69 64 5F 6C 64 .........wxid_ld
+00000000000006D0 66 74 75 68 65 33 36 69 7A 67 31 39 1A 16 0A 14 ftuhe36izg19....
+00000000000006E0 34 35 38 37 32 30 32 35 33 38 34 40 63 68 61 74 45872025384@chat
+00000000000006F0 72 6F 6F 6D 20 33 2A 9D 01 0A 9A 01 3C 6D 73 67 room 3*.......45872025
+0000000000000720 33 38 34 40 63 68 61 74 72 6F 6F 6D 3C 2F 75 73 384@chatroom.la
+0000000000000740 73 74 4D 65 73 73 61 67 65 3C 2F 6E 61 6D 65 3E stMessage
+0000000000000750 0A 3C 61 72 67 3E 7B 22 6D 65 73 73 61 67 65 53 .{"messageS
+0000000000000760 76 72 49 64 22 3A 30 2C 22 4D 73 67 43 72 65 61 vrId":0,"MsgCrea
+0000000000000770 74 65 54 69 6D 65 22 3A 31 37 38 33 35 39 32 39 teTime":17835929
+0000000000000780 33 32 7D 3C 2F 61 72 67 3E 0A 3C 2F 6F 70 3E 0A 32}..
+0000000000000790 3C 2F 6D 73 67 3E 30 03 38 01 42 02 08 00 48 E5 0.8.B...H.
+00000000000007A0 EF BD D2 06 52 76 3C 6D 73 67 73 6F 75 72 63 65 ....Rv..v1
+00000000000007C0 5F 71 38 43 30 51 43 64 78 3C 2F 73 69 67 6E 61 _q8C0QCdx........<
+0000000000000810 2F 6D 73 67 73 6F 75 72 63 65 3E 0A 60 E2 BB AE /msgsource>....
+0000000000000820 8D D0 B7 A7 ED 1A 68 F9 B8 AC D4 02 72 02 08 00 ......h.....r...
+0000000000000830 78 00 18 00 22 EC 02 08 E6 02 12 E6 02 08 2F 12 x..."........./.
+0000000000000840 08 08 01 10 B6 9F FA 8D 03 12 04 08 65 10 00 12 ............e...
+0000000000000850 08 08 02 10 F9 B8 AC D4 02 12 04 08 66 10 00 12 ............f...
+0000000000000860 04 08 0D 10 00 12 05 08 CC 01 10 00 12 08 08 03 ................
+0000000000000870 10 8E 91 FA 8D 03 12 04 08 67 10 00 12 08 08 0B .........g......
+0000000000000880 10 F2 9D FA 8D 03 12 04 08 6F 10 00 12 04 08 04 .........o......
+0000000000000890 10 00 12 04 08 68 10 00 12 04 08 05 10 00 12 04 .....h..........
+00000000000008A0 08 69 10 00 12 04 08 07 10 00 12 04 08 6B 10 00 .i...........k..
+00000000000008B0 12 04 08 08 10 00 12 04 08 6C 10 00 12 08 08 09 .........l......
+00000000000008C0 10 96 9F FA 8D 03 12 04 08 6D 10 00 12 08 08 16 .........m......
+00000000000008D0 10 A1 9F FA 8D 03 12 04 08 06 10 00 12 04 08 7A ...............z
+00000000000008E0 10 00 12 04 08 6A 10 00 12 04 08 10 10 00 12 04 .....j..........
+00000000000008F0 08 11 10 00 12 04 08 72 10 00 12 08 08 0E 10 CA .......r........
+0000000000000900 ED F9 8D 03 12 04 08 70 10 00 12 06 08 13 10 D9 .......p........
+0000000000000910 EB 02 12 04 08 1A 10 00 12 06 08 1B 10 8B B7 02 ................
+0000000000000920 12 08 08 18 10 E5 EF BD D2 06 12 08 08 17 10 D9 ................
+0000000000000930 EF BD D2 06 12 08 08 19 10 F9 B8 AC D4 02 12 04 ................
+0000000000000940 08 0A 10 00 12 09 08 C9 01 10 E5 EF BD D2 06 12 ................
+0000000000000950 05 08 CD 01 10 00 12 05 08 CA 01 10 00 12 09 08 ................
+0000000000000960 CB 01 10 E3 92 A7 D2 06 12 05 08 CE 01 10 67 12 ..............g.
+0000000000000970 05 08 CF 01 10 00 12 04 08 14 10 00 12 04 08 15 ................
+0000000000000980 10 00 12 09 08 E8 07 10 98 BF BD D2 06 12 09 08 ................
+0000000000000990 E9 07 10 D9 DB BD D2 06 12 09 08 D3 0F 10 92 F4 ................
+00000000000009A0 BC D2 06 28 01 30 DA E0 DA DA 02 38 E5 EF BD D2 ...(.0.....8....
+00000000000009B0 06 42 15 08 11 12 11 0A 0F 08 02 08 1A 08 1B 10 .B..............
+00000000000009C0 BF C0 B4 A7 0A 18 E9 07 ........
+`
+
+// TestProbeProtobufDump 直接测试 HandleProtobufMsg:
+// 把 hexdump 还原成字节 → 按 JS 侧一样封装成 {"data": [byte...]} 的 payload
+// → 调用 HandleProtobufMsg,打印它返回的 WechatMessage JSON 或错误。
+func TestProbeProtobufDump(t *testing.T) {
+ rawBytes := parseDump(t, dumpInput)
+ t.Logf("解析出的字节数: %d", len(rawBytes))
+
+ // 构造 HandleProtobufMsg 期望的 payload:data 是 []interface{}{float64,...}
+ dataArr := make([]interface{}, len(rawBytes))
+ for i, b := range rawBytes {
+ dataArr[i] = float64(b)
+ }
+ payload := map[string]interface{}{"data": dataArr}
+
+ jsonList, err := HandleProtobufMsg(payload)
+ if err != nil {
+ t.Fatalf("HandleProtobufMsg 失败: %v", err)
+ }
+ if len(jsonList) == 0 {
+ t.Fatalf("HandleProtobufMsg 返回空(消息被全部跳过)")
+ }
+
+ fmt.Printf("========== HandleProtobufMsg 返回 %d 条消息 ==========\n", len(jsonList))
+ for idx, jsonData := range jsonList {
+ var pretty bytes.Buffer
+ if err := json.Indent(&pretty, jsonData, "", " "); err != nil {
+ fmt.Printf("--- 消息 %d ---\n%s\n", idx, string(jsonData))
+ } else {
+ fmt.Printf("--- 消息 %d ---\n%s\n", idx, pretty.String())
+ }
+ }
+}
+
+// parseDump 从 hexdump 里抽出真正的字节。
+// 针对 lldb / hexdump -C 的固定列宽格式:
+//
+// 地址(16 hex) + 2空格 + hex列(16字节) + 2空格 + ASCII列
+//
+// 第 0..17 字符是地址列,第 18 起是 hex 列(16 字节 = 48 字符宽,
+// 中缝再占 1 空格)。按固定列宽切片,彻底避开 ASCII 列被误当字节。
+func parseDump(t *testing.T, dump string) []byte {
+ t.Helper()
+ const addrCols = 18 // 16 hex 地址 + 2 空格
+ const hexCols = 49 // 8字节*3 + 中缝1 + 8字节*3 = 49(含末尾空格,Fields 会忽略)
+ var out []byte
+ for _, line := range strings.Split(dump, "\n") {
+ if strings.TrimSpace(line) == "" {
+ continue
+ }
+ if len(line) <= addrCols {
+ continue
+ }
+ end := addrCols + hexCols
+ if end > len(line) {
+ end = len(line)
+ }
+ for _, tk := range strings.Fields(line[addrCols:end]) {
+ if len(tk) == 2 && isHexTok(tk) {
+ var v int
+ fmt.Sscanf(tk, "%02x", &v)
+ out = append(out, byte(v))
+ }
+ }
+ }
+ return out
+}
+
+func isHexTok(s string) bool {
+ for _, c := range s {
+ if !((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F')) {
+ return false
+ }
+ }
+ return len(s) > 0
+}
diff --git a/onebot/protobuf_handler.go b/onebot/protobuf_handler.go
index 4d8d04d..d90e266 100644
--- a/onebot/protobuf_handler.go
+++ b/onebot/protobuf_handler.go
@@ -8,11 +8,12 @@ import (
"time"
wxproto "github.com/yincongcyincong/weixin-macos/onebot/proto"
+ "google.golang.org/protobuf/encoding/protowire"
"google.golang.org/protobuf/proto"
)
func HandleProtobufMsgAndSend(payload map[string]interface{}) {
- jsonData, err := HandleProtobufMsg(payload)
+ jsonList, err := HandleProtobufMsg(payload)
if err != nil {
if isUnsupportedProtobufMessage(err) {
Warn("跳过不匹配的protobuf消息", "err", err)
@@ -21,15 +22,17 @@ func HandleProtobufMsgAndSend(payload map[string]interface{}) {
Error("protobuf消息处理失败", "err", err)
return
}
- if jsonData == nil {
- return
- }
- // jsonData 是 WechatMessage JSON, 直接交给现有的发送流程处理
- if config.ConnType == "http" {
- SendHttpReq(jsonData)
- } else {
- SendWebSocketMsg(jsonData)
+ // 一个数据帧可能打包了多条消息,逐条下发
+ for _, jsonData := range jsonList {
+ if jsonData == nil {
+ continue
+ }
+ if config.ConnType == "http" {
+ SendHttpReq(jsonData)
+ } else {
+ SendWebSocketMsg(jsonData)
+ }
}
}
@@ -45,7 +48,57 @@ func isUnsupportedProtobufMessage(err error) bool {
strings.Contains(message, "no messages found")
}
-func HandleProtobufMsg(payload map[string]interface{}) ([]byte, error) {
+// consumeBytesFields 从一段 protobuf 原始字节里,取出指定字段号、且 wiretype 为
+// bytes(2) 的所有值。用于遍历"重复出现的 singular 字段"(proto.Unmarshal 只会保留
+// 最后一个,这里手动全部取出)。
+func consumeBytesFields(raw []byte, field protowire.Number) [][]byte {
+ var out [][]byte
+ for len(raw) > 0 {
+ num, typ, n := protowire.ConsumeTag(raw)
+ if n < 0 {
+ break
+ }
+ raw = raw[n:]
+ if typ == protowire.BytesType {
+ v, m := protowire.ConsumeBytes(raw)
+ if m < 0 {
+ break
+ }
+ if num == field {
+ out = append(out, v)
+ }
+ raw = raw[m:]
+ continue
+ }
+ m := protowire.ConsumeFieldValue(num, typ, raw)
+ if m < 0 {
+ break
+ }
+ raw = raw[m:]
+ }
+ return out
+}
+
+// parseAllRecvData 从 WxRecvMsg 原始字节里提取所有消息数据。
+// 结构: WxRecvMsg.wrapper(field2) -> wrapper.body(field2, 可重复) -> body.content.data
+// 一个 wrapper 里可能打包多条 body,需要全部取出。
+func parseAllRecvData(rawBytes []byte) []*wxproto.WxRecvMsgData {
+ var result []*wxproto.WxRecvMsgData
+ for _, wrapperRaw := range consumeBytesFields(rawBytes, 2) {
+ for _, bodyRaw := range consumeBytesFields(wrapperRaw, 2) {
+ body := &wxproto.WxRecvMsgBody{}
+ if err := proto.Unmarshal(bodyRaw, body); err != nil {
+ continue
+ }
+ if body.Content != nil && body.Content.Data != nil {
+ result = append(result, body.Content.Data)
+ }
+ }
+ }
+ return result
+}
+
+func HandleProtobufMsg(payload map[string]interface{}) ([][]byte, error) {
dataInter, ok := payload["data"]
if !ok {
return nil, fmt.Errorf("protobuf_msg: missing data field")
@@ -65,17 +118,35 @@ func HandleProtobufMsg(payload map[string]interface{}) ([]byte, error) {
rawBytes[i] = byte(int(num))
}
- msg := &wxproto.WxRecvMsg{}
- err := proto.Unmarshal(rawBytes, msg)
- if err != nil {
- return nil, fmt.Errorf("protobuf unmarshal failed (data_len=%d): %w", len(rawBytes), err)
- }
+ //fmt.Println("[receive protobuf data]", HexDump(rawBytes, 0))
- data := getWxMsgData(msg)
- if data == nil {
+ dataList := parseAllRecvData(rawBytes)
+ if len(dataList) == 0 {
return nil, fmt.Errorf("protobuf_msg: cannot extract message data")
}
+ var jsonList [][]byte
+ for _, data := range dataList {
+ jsonData, err := buildWechatMessageJSON(data)
+ if err != nil {
+ // 单条消息(如系统通知/字段不全)不阻断其他消息
+ Warn("跳过单条protobuf消息", "err", err)
+ continue
+ }
+ if jsonData != nil {
+ jsonList = append(jsonList, jsonData)
+ }
+ }
+
+ if len(jsonList) == 0 {
+ return nil, fmt.Errorf("protobuf_msg: no messages found")
+ }
+
+ return jsonList, nil
+}
+
+// buildWechatMessageJSON 把单条 WxRecvMsgData 转成 WechatMessage JSON。
+func buildWechatMessageJSON(data *wxproto.WxRecvMsgData) ([]byte, error) {
sender := ""
receiver := ""
content := ""
@@ -177,19 +248,6 @@ func HandleProtobufMsg(payload map[string]interface{}) ([]byte, error) {
return json.Marshal(wechatMsg)
}
-func getWxMsgData(msg *wxproto.WxRecvMsg) *wxproto.WxRecvMsgData {
- if msg == nil || msg.Wrapper == nil {
- return nil
- }
- if msg.Wrapper.Body == nil {
- return nil
- }
- if msg.Wrapper.Body.Content == nil {
- return nil
- }
- return msg.Wrapper.Body.Content.Data
-}
-
func getMessagesFromProto(content, sender string, mediaContent []byte) []*Message {
var messages []*Message
diff --git a/onebot/script.js b/onebot/script.js
index 05470f1..d6a3093 100644
--- a/onebot/script.js
+++ b/onebot/script.js
@@ -135,7 +135,6 @@ function generateAESKey() {
}
const MAX_FRIDA_MESSAGE_BYTES = 4 * 1024 * 1024;
-const DOWNLOAD_CHUNK_BYTES = 256 * 1024;
function isReadablePointer(addr) {
try {
@@ -169,7 +168,7 @@ function readUtf8StringIfReadable(addr) {
if (!isReadablePointer(addr)) {
return "";
}
- return addr.readUtf8String(4096);
+ return addr.readUtf8String();
} catch (e) {
return "";
}
@@ -190,23 +189,18 @@ function sendDownloadChunks(dataPtr, dataLen, fileId, cdnUrl) {
if (!cdnUrl || dataLen <= 0) {
return;
}
- const safeFileId = fileId || "";
- for (let offset = 0; offset < dataLen; offset += DOWNLOAD_CHUNK_BYTES) {
- const chunkLen = Math.min(DOWNLOAD_CHUNK_BYTES, dataLen - offset);
- const buffer = readByteArrayIfReadable(dataPtr.add(offset), chunkLen);
- if (!buffer) {
- return;
- }
- const uint8Array = new Uint8Array(buffer);
+ if (dataLen > 0 && dataLen <= 10 * 1024 * 1024) {
+ var buffer = dataPtr.readByteArray(dataLen);
+ var uint8Array = new Uint8Array(buffer);
- send({
- type: "download",
- media: Array.from(uint8Array),
- file_id: safeFileId,
- cdn_url: cdnUrl,
- });
- }
+ send({
+ type: "download",
+ media: Array.from(uint8Array),
+ file_id: fileId,
+ cdn_url: cdnUrl,
+ })
+ }
}
function fillUploadX1AndStart(idAddr, pathAddr, x1Buffer, receiver, md5, filePath, payloadHex) {
@@ -1059,14 +1053,15 @@ function setupDownloadFileDynamic() {
function setReceiver() {
- Interceptor.attach(buf2RespAddr, {
- onEnter: function (args) {
- // 通过 SP+0x140 读取当前 buf2resp 对应的 taskId
- var respTaskId = this.context.sp.add(0x140).readS32();
+ Interceptor.attach(buf2RespAddr, {
+ onEnter: function (args) {
+ // 通过 SP+0x140 读取当前 buf2resp 对应的 taskId
+ var respTaskId = this.context.sp.add(0x140).readS32();
const currentPtr = this.context.x20;
const x2 = this.context.x0.toInt32();
- if (!isReadablePointer(currentPtr)) {
- return;
+ if (!isReadablePointer(currentPtr) || x2 < 4 || x2 > MAX_FRIDA_MESSAGE_BYTES) {
+ console.error("[-] buf2resp: pointer 不可读 或 x2 大小不正确, ptr=" + currentPtr + " x2=" + x2);
+ return;
}
// 判断是否是我们发送的消息的 ack
@@ -1092,14 +1087,7 @@ function setReceiver() {
pendingBuf2RespTaskId = 0;
pendingSendMsgType = "";
- }
-
- if (x2 < 4) {
- return;
- }
- if (x2 > MAX_FRIDA_MESSAGE_BYTES) {
- console.warn("[skip] protobuf_msg length out of range: " + x2);
- return;
+ return
}
const mem = readByteArrayIfReadable(currentPtr, x2);
diff --git a/onebot/utils.go b/onebot/utils.go
index b6e6414..d3944aa 100644
--- a/onebot/utils.go
+++ b/onebot/utils.go
@@ -354,24 +354,39 @@ func SilkToMp3(silkBytes []byte) ([]byte, error) {
return out.Bytes(), nil
}
-func GetFilePath(data []byte, key []byte) (string, error) {
+// GetFilePath 解密 CDN 数据并落盘。
+// - extHint: 消息 XML 里的 fileext(如 "txt"),优先级最高——有就直接用它当扩展名,
+// 不再靠 magic bytes 猜(文本文件没有 magic,猜不出来)。
+// - totalLen: 明文真实长度(消息里的 totallen)。>0 时按它截断,去掉 AES 补齐的尾部字节。
+func GetFilePath(data []byte, key []byte, extHint string, totalLen int) (string, error) {
block, err := aes.NewCipher(key)
if err != nil {
return "", err
}
- if len(data) == 0 || len(data)%block.BlockSize() != 0 {
- return "", fmt.Errorf("invalid encrypted data length: %d, block_size: %d", len(data), block.BlockSize())
+ // 只解密对齐部分,丢弃末尾不足一个块的残余(调用方通常已补齐/截齐)。
+ bs := block.BlockSize()
+ aligned := len(data) - len(data)%bs
+ if aligned == 0 {
+ return "", fmt.Errorf("invalid encrypted data length: %d, block_size: %d", len(data), bs)
}
- decrypted := make([]byte, len(data))
- bs := block.BlockSize()
- for i := 0; i < len(data); i += bs {
+ decrypted := make([]byte, aligned)
+ for i := 0; i < aligned; i += bs {
block.Decrypt(decrypted[i:i+bs], data[i:i+bs])
}
- ext := DetectFileFormat(decrypted)
- if ext == "unknown" {
- return "", fmt.Errorf("无法解析的文件数据")
+ // 按真实长度截断,去掉补齐的尾部字节
+ if totalLen > 0 && totalLen <= len(decrypted) {
+ decrypted = decrypted[:totalLen]
+ }
+
+ // 扩展名:优先用消息里的 fileext,其次靠 magic 检测,都没有则存为 bin(不丢数据)
+ ext := strings.TrimPrefix(strings.ToLower(extHint), ".")
+ if ext == "" {
+ ext = DetectFileFormat(decrypted)
+ if ext == "unknown" {
+ ext = "bin"
+ }
}
return SaveFileToFile(ext, decrypted)
diff --git a/onebot/worker.go b/onebot/worker.go
index 6a2a388..107e859 100644
--- a/onebot/worker.go
+++ b/onebot/worker.go
@@ -10,6 +10,8 @@ import (
"fmt"
"os"
"runtime/debug"
+ "strconv"
+ "strings"
"sync/atomic"
"time"
)
@@ -364,7 +366,7 @@ func HandleMsg(jsonData []byte) ([]byte, error) {
return nil, err
}
- path, err := GetDownloadPath(fileMsg.Image.MidImgURL, fileMsg.Image.AesKey)
+ path, err := GetDownloadPath(fileMsg.Image.MidImgURL, fileMsg.Image.AesKey, "", 0)
if err != nil {
Error("获取文件路径失败", "err", err)
return nil, err
@@ -379,7 +381,8 @@ func HandleMsg(jsonData []byte) ([]byte, error) {
Error("XML解析失败", "err", err)
return nil, err
}
- path, err := GetDownloadPath(fileMsg.AppMsg.AppAttach.CdnAttachURL, fileMsg.AppMsg.AppAttach.AesKey)
+ totalLen, _ := strconv.Atoi(strings.TrimSpace(fileMsg.AppMsg.AppAttach.TotalLen))
+ path, err := GetDownloadPath(fileMsg.AppMsg.AppAttach.CdnAttachURL, fileMsg.AppMsg.AppAttach.AesKey, fileMsg.AppMsg.AppAttach.FileExt, totalLen)
if err != nil {
Error("获取文件路径失败", "err", err)
return nil, err
@@ -393,7 +396,7 @@ func HandleMsg(jsonData []byte) ([]byte, error) {
Error("XML解析失败", "err", err)
return nil, err
}
- path, err := GetDownloadPath(fileMsg.Video.CdnVideoUrl, fileMsg.Video.AesKey)
+ path, err := GetDownloadPath(fileMsg.Video.CdnVideoUrl, fileMsg.Video.AesKey, "mp4", int(fileMsg.Video.Length))
if err != nil {
Error("获取文件路径失败", "err", err)
return nil, err
@@ -435,52 +438,65 @@ func HandleMsg(jsonData []byte) ([]byte, error) {
return json.Marshal(m)
}
-func GetDownloadPath(cdnUrl, aesKeyStr string) (string, error) {
- for i := 0; i < 10; i++ {
+func GetDownloadPath(cdnUrl, aesKeyStr, extHint string, totalLen int) (string, error) {
+ for i := 0; i < 30; i++ {
if downloadMsgInter, ok := userID2FileMsgMap.Load(cdnUrl); ok {
downloadReq := downloadMsgInter.(*DownloadRequest)
+
+ downloadReq.mu.Lock()
+
if downloadReq.FilePath != "" {
- return downloadReq.FilePath, nil
+ fp := downloadReq.FilePath
+ downloadReq.mu.Unlock()
+ return fp, nil
}
// 检查数据是否还在接收中
timeSinceLastAppend := time.Now().UnixMilli() - downloadReq.LastAppendTime
Info("文件等待下载", "url", cdnUrl, "times", i, "last_append_time", timeSinceLastAppend)
- // 如果数据仍在接收中(1秒内有新数据),继续等待
- if timeSinceLastAppend < 1000 && i < 9 {
+ // 如果数据仍在接收中(3秒内有新数据),继续等待
+ if timeSinceLastAppend < 2000 && i < 29 {
+ downloadReq.mu.Unlock()
time.Sleep(2 * time.Second)
continue
}
// 数据接收完成,尝试解密
if len(downloadReq.Media) > 0 {
- if len(downloadReq.Media)%aes.BlockSize != 0 {
- Info("文件数据仍未对齐 AES 块,继续等待", "url", cdnUrl, "times", i, "media_len", len(downloadReq.Media), "block_size", aes.BlockSize)
- if i < 9 {
- time.Sleep(2 * time.Second)
- continue
- }
- userID2FileMsgMap.Delete(cdnUrl)
- return "", fmt.Errorf("文件数据长度不是 AES 块大小倍数: media_len=%d block_size=%d", len(downloadReq.Media), aes.BlockSize)
+ media := downloadReq.Media
+ // AES 块对不齐时,末尾补 0 到整块(微信 CDN 密文尾部可能带
+ // 非整块残余,补齐后按整块解密,避免丢字节)。
+ if rem := len(media) % aes.BlockSize; rem != 0 {
+ pad := aes.BlockSize - rem
+ Info("文件数据未对齐 AES 块,末尾补 0 到整块",
+ "url", cdnUrl, "media_len", len(media), "pad", pad, "block_size", aes.BlockSize)
+ padded := make([]byte, len(media)+pad)
+ copy(padded, media)
+ media = padded
}
aesKey, err := hex.DecodeString(aesKeyStr)
if err != nil {
+ downloadReq.mu.Unlock()
Error("AES key 解码失败", "err", err)
return "", err
}
- filePath, err := GetFilePath(downloadReq.Media, aesKey)
+ filePath, err := GetFilePath(media, aesKey, extHint, totalLen)
if err != nil {
- Error("获取文件路径失败", "err", err, "media_len", len(downloadReq.Media))
+ downloadReq.mu.Unlock()
+ Error("获取文件路径失败", "err", err, "media_len", len(media), "aes_key", aesKeyStr)
userID2FileMsgMap.Delete(cdnUrl)
return "", err
}
downloadReq.FilePath = filePath
downloadReq.Media = nil
+ downloadReq.mu.Unlock()
return filePath, nil
}
+
+ downloadReq.mu.Unlock()
}
time.Sleep(2 * time.Second)
diff --git a/wechat_version/4_1_11_53_mac.json b/wechat_version/4_1_11_53_mac.json
new file mode 100644
index 0000000..ea176e4
--- /dev/null
+++ b/wechat_version/4_1_11_53_mac.json
@@ -0,0 +1,21 @@
+{
+ "sendFuncAddr": "0x5120fd8",
+ "req2bufEnterAddr": "0x3e58e8c",
+ "req2bufExitAddr": "0x3E59DE0",
+ "buf2RespAddr": "0x3e7e670",
+ "blrX8Addr": "0x3e58f0c",
+ "autoBufferWriteFunc": "0x3E7FA8C",
+
+ "uploadImageAddr": "0x529bd7c",
+ "cndOnCompleteAddr": "0x3E1575C",
+ "uploadGetCallbackWrapperAddr": "0x525ab20",
+ "uploadGetCallbackWrapperFuncAddr": "0x3E14FF8",
+ "uploadOnCompleteAddr": "0x525B2D8",
+ "uploadOnCompleteFuncAddr": "0x3E1617C",
+
+ "startDownloadMedia": "0x529c9e4",
+ "downloadImagAddr": "0x53c0004",
+ "downloadFileAddr": "0x535ea98",
+ "downloadVideoAddr": "0x5378a18"
+}
+