From 49114827bc83f8381eb638e8a56f3f0305fc1a1c Mon Sep 17 00:00:00 2001 From: yincong Date: Fri, 10 Jul 2026 09:28:22 +0800 Subject: [PATCH] fix bug --- onebot/main.go | 2 +- onebot/msg.go | 48 +++--- onebot/param.go | 2 + onebot/protobuf_dump_test.go | 246 ++++++++++++++++++++++++++++++ onebot/protobuf_handler.go | 118 ++++++++++---- onebot/script.js | 50 +++--- onebot/utils.go | 33 ++-- onebot/worker.go | 52 ++++--- wechat_version/4_1_11_53_mac.json | 21 +++ 9 files changed, 465 insertions(+), 107 deletions(-) create mode 100644 onebot/protobuf_dump_test.go create mode 100644 wechat_version/4_1_11_53_mac.json 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 ........ +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" +} +