diff --git a/onebot/file_builder.go b/onebot/file_builder.go
new file mode 100644
index 0000000..6e617bc
--- /dev/null
+++ b/onebot/file_builder.go
@@ -0,0 +1,258 @@
+package main
+
+import (
+ "encoding/hex"
+ "fmt"
+ "math/rand"
+ "time"
+
+ "google.golang.org/protobuf/proto"
+
+ wxproto "github.com/yincongcyincong/weixin-macos/onebot/proto"
+)
+
+// BuildFileUploadMsgProto 构建 /cgi-bin/micromsg-bin/sendfileuploadmsg 请求的protobuf
+func BuildFileUploadMsgProto(targetId string, fileInfo *FileInfo) (string, error) {
+ // 复用上传时生成的 clientMsgId(去掉 _1 后缀),保证与 upload body 里的 filekey 一致
+ clientMsgId := fileInfo.ClientMsgId
+ if clientMsgId == "" {
+ now := time.Now().Unix()
+ clientMsgId = fmt.Sprintf("%s_%d_%d", targetId, now, rand.Intn(1000))
+ fileInfo.ClientMsgId = clientMsgId
+ }
+
+ version := NextVersion()
+ msgType := uint32(1)
+ field7 := uint32(0)
+ field12 := uint32(0)
+ fileSize := uint32(fileInfo.TotalLen)
+
+ msg := &wxproto.SendFileUploadMsgRequest{
+ BaseRequest: &wxproto.ReplyMsgHeader{
+ Flag: []byte{0x00},
+ SessionId: &globalSessionId,
+ ClientProof: globalClientProof,
+ DeviceId: &globalDeviceId,
+ Platform: proto.String("UnifiedPCMac 26 arm64"),
+ Version: &version,
+ },
+ ToUserName: &targetId,
+ ClientMsgId: &clientMsgId,
+ MsgType: &msgType,
+ FileInfo: &wxproto.FileUploadFileInfo{
+ FileName: &fileInfo.FileName,
+ FileExt: &fileInfo.FileExt,
+ FileMd5: &fileInfo.Md5,
+ FileSize: &fileSize,
+ },
+ Field7: &field7,
+ Field12: &field12,
+ }
+
+ data, err := proto.Marshal(msg)
+ if err != nil {
+ return "", fmt.Errorf("marshal file upload msg proto failed: %w", err)
+ }
+
+ fmt.Printf("[file-upload-proto] final protobuf hex dump:\n%s\n", HexDump(data, 0))
+ return hex.EncodeToString(data), nil
+}
+
+// ParseFileUploadMsgResponse 解析 sendfileuploadmsg 响应,提取 fileUploadToken 和 msgSvrId
+func ParseFileUploadMsgResponse(data []byte) (string, string, error) {
+ resp := &wxproto.SendFileUploadMsgResponse{}
+ err := proto.Unmarshal(data, resp)
+ if err != nil {
+ return "", "", fmt.Errorf("unmarshal file upload response failed: %w", err)
+ }
+
+ if resp.BaseResponse != nil && resp.BaseResponse.Ret != nil && *resp.BaseResponse.Ret != 0 {
+ errMsg := ""
+ if resp.BaseResponse.ErrMsg != nil {
+ errMsg = resp.BaseResponse.ErrMsg.GetMsg()
+ }
+ return "", "", fmt.Errorf("sendfileuploadmsg failed, ret=%d, errMsg=%s", *resp.BaseResponse.Ret, errMsg)
+ }
+
+ token := resp.GetFileUploadToken()
+ msgSvrId := fmt.Sprintf("%d", resp.GetMsgSvrId())
+ return token, msgSvrId, nil
+}
+
+// FileInfo 文件发送消息需要的信息
+type FileInfo struct {
+ FileName string // 文件名 (title)
+ TotalLen int64 // 文件大小
+ AttachId string // 附件ID (来自upload完成回调)
+ FileExt string // 文件扩展名
+ CdnAttachURL string // CDN附件URL
+ AesKey string // AES密钥
+ Md5 string // 文件MD5
+ OverwriteMsgId string // overwrite_newmsgid
+ FileUploadToken string // fileuploadtoken
+ ClientMsgId string // 由 BuildFileUploadMsgProto 生成,BuildFileMsgProto 复用
+}
+
+// BuildFileMsgProto 构建发送文件消息的protobuf并返回hex编码的字符串
+// 复用reply_msg.proto (WxSendReplyMsg),msg_type=6
+func BuildFileMsgProto(sender, receiver string, fileInfo *FileInfo) (string, error) {
+ now := time.Now().Unix()
+
+ // 构建文件appmsg XML
+ appmsgXml := buildFileAppmsgXml(sender, fileInfo)
+
+ // 构建客户端消息ID: 复用 BuildFileUploadMsgProto 的 clientMsgId + 后缀
+ clientMsgId := fileInfo.ClientMsgId + "_xwechat_1"
+
+ // msgsource
+ msgsource := "12"
+
+ // proto2 需要使用指针
+ var (
+ unknown2 = []byte("wx6618f1cfc6c132f8") // appid
+ unknown3 = int32(0)
+ msgType = int32(6) // 文件消息类型
+ flag = int32(1)
+ unknown13 = []byte{}
+ unknown14 = []byte{}
+ unknown15 = []byte{}
+ version = NextVersion()
+ )
+
+ msg := &wxproto.WxSendReplyMsg{
+ Header: &wxproto.ReplyMsgHeader{
+ Flag: []byte{0x00},
+ SessionId: &globalSessionId,
+ ClientProof: globalClientProof,
+ DeviceId: &globalDeviceId,
+ Platform: proto.String("UnifiedPCMac 26 arm64"),
+ Version: &version,
+ },
+ Body: &wxproto.ReplyMsgBody{
+ Sender: &sender,
+ Unknown2: unknown2,
+ Unknown3: &unknown3,
+ Receiver: &receiver,
+ MsgType: &msgType,
+ Content: []byte(appmsgXml),
+ SendTimestamp: proto.Int64(now),
+ ClientMsgId: &clientMsgId,
+ Flag: &flag,
+ Msgsource: []byte(msgsource),
+ Unknown13: unknown13,
+ Unknown14: unknown14,
+ Unknown15: unknown15,
+ },
+ Unknown5: []byte(fileInfo.Md5),
+ Unknown9: proto.Int32(1),
+ Unknown10: proto.Uint64(uint64(rand.Uint32())),
+ Unknown11: proto.Int32(2),
+ }
+
+ data, err := proto.Marshal(msg)
+ if err != nil {
+ return "", fmt.Errorf("marshal file proto failed: %w", err)
+ }
+
+ fmt.Printf("[file-proto] final protobuf hex dump:\n%s\n", HexDump(data, 0))
+
+ return hex.EncodeToString(data), nil
+}
+
+// buildFileAppmsgXml 构建文件消息的appmsg XML
+func buildFileAppmsgXml(selfWxid string, info *FileInfo) string {
+ xml := ``
+ xml += `` + escapeXmlStr(info.FileName) + ``
+ xml += ``
+ xml += ``
+ xml += `6`
+ xml += `0`
+ xml += `0`
+ xml += ``
+ xml += ``
+ xml += ``
+ xml += ``
+ xml += `0`
+ xml += ``
+ xml += ``
+ xml += ``
+ xml += ``
+ xml += ``
+ xml += ``
+ xml += ``
+ xml += ``
+ xml += `` + fmt.Sprintf("%d", info.TotalLen) + ``
+ xml += `` + escapeXmlStr(info.AttachId) + ``
+ xml += ``
+ xml += `` + escapeXmlStr(info.FileExt) + ``
+ xml += `` + escapeXmlStr(info.CdnAttachURL) + ``
+ xml += `` + escapeXmlStr(info.AesKey) + ``
+ xml += `0`
+ xml += `` + escapeXmlStr(info.OverwriteMsgId) + ``
+ xml += `` + escapeXmlStr(info.FileUploadToken) + ``
+ xml += ``
+ xml += ``
+ xml += ``
+ xml += ``
+ xml += ``
+ xml += `` + escapeXmlStr(info.Md5) + ``
+ xml += ``
+ xml += ``
+ xml += `` + escapeXmlStr(selfWxid) + ``
+
+ return xml
+}
+
+// BuildCheckMd5Proto 构建 /cgi-bin/micromsg-bin/checkmd5 请求的protobuf
+func BuildCheckMd5Proto(fileInfo *FileInfo) (string, error) {
+ version := NextVersion()
+ field5 := uint32(0)
+
+ msg := &wxproto.CheckMd5Request{
+ BaseRequest: &wxproto.ReplyMsgHeader{
+ Flag: []byte{0x00},
+ SessionId: &globalSessionId,
+ ClientProof: globalClientProof,
+ DeviceId: &globalDeviceId,
+ Platform: proto.String("UnifiedPCMac 26 arm64"),
+ Version: &version,
+ },
+ FileKey: &fileInfo.CdnAttachURL,
+ FileMd5: &fileInfo.Md5,
+ Field4: []byte{},
+ Field5: &field5,
+ }
+
+ data, err := proto.Marshal(msg)
+ if err != nil {
+ return "", fmt.Errorf("marshal checkmd5 proto failed: %w", err)
+ }
+
+ //fmt.Printf("[checkmd5-proto] final protobuf hex dump:\n%s\n", HexDump(data, 0))
+ return hex.EncodeToString(data), nil
+}
+
+// ParseCheckMd5Response 解析 checkmd5 响应
+// ret=0 表示文件已存在(秒传),ret=102 表示文件不存在需要正常发送,两者都视为成功
+func ParseCheckMd5Response(data []byte) error {
+ resp := &wxproto.CheckMd5Response{}
+ err := proto.Unmarshal(data, resp)
+ if err != nil {
+ return fmt.Errorf("unmarshal checkmd5 response failed: %w", err)
+ }
+
+ if resp.BaseResponse != nil && resp.BaseResponse.Ret != nil {
+ ret := *resp.BaseResponse.Ret
+ // 0=文件已存在(秒传), 102=文件不存在(正常发送), 都可以继续
+ if ret != 0 {
+ errMsg := ""
+ if resp.BaseResponse.ErrMsg != nil {
+ errMsg = resp.BaseResponse.ErrMsg.GetMsg()
+ }
+ return fmt.Errorf("checkmd5 failed, ret=%d, errMsg=%s", ret, errMsg)
+ }
+ Info("checkmd5 ret", "ret", ret)
+ }
+
+ return nil
+}
diff --git a/onebot/go.mod b/onebot/go.mod
index 5244df8..891b82d 100644
--- a/onebot/go.mod
+++ b/onebot/go.mod
@@ -4,6 +4,7 @@ go 1.25.0
require (
github.com/frida/frida-go v1.0.2
+ github.com/google/uuid v1.6.0
github.com/gorilla/websocket v1.5.3
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d
github.com/rs/zerolog v1.35.1
@@ -14,7 +15,6 @@ require (
require (
github.com/dustin/go-humanize v1.0.1 // indirect
- github.com/google/uuid v1.6.0 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.22 // indirect
github.com/ncruces/go-strftime v1.0.0 // indirect
diff --git a/onebot/http.go b/onebot/http.go
index 172d765..0788eab 100644
--- a/onebot/http.go
+++ b/onebot/http.go
@@ -9,6 +9,7 @@ import (
"fmt"
"io"
"net/http"
+ "net/url"
"runtime/debug"
"strings"
"time"
@@ -49,13 +50,21 @@ func sendHandler(w http.ResponseWriter, r *http.Request) {
}
}
- } else if v.Type == "image" || v.Type == "video" {
+ } else if v.Type == "image" || v.Type == "video" || v.Type == "record" || v.Type == "voice" || v.Type == "file" {
+ msgType := v.Type
+ if msgType == "record" || msgType == "voice" {
+ msgType = "voice"
+ }
+ // file: 走 iPad860 风格 uploadappattach 直传(不走 CDN)
+ if msgType == "file" {
+ msgType = "send_file_simple"
+ }
ch := make(chan error, 1)
msg := &SendMsg{
UserId: req.UserID,
GroupID: req.GroupID,
Content: v.Data.File,
- Type: v.Type,
+ Type: msgType,
ResultChan: ch,
}
msgChan <- msg
@@ -212,3 +221,40 @@ func jsonUnescapeString(s string) string {
return result
}
+// getFileExt 从文件名中提取扩展名(不含.)
+func getFileExt(fileName string) string {
+ idx := strings.LastIndex(fileName, ".")
+ if idx == -1 || idx == len(fileName)-1 {
+ return ""
+ }
+ return fileName[idx+1:]
+}
+
+// extractFileName 从路径或URL中提取文件名
+func extractFileName(s string) string {
+ if s == "" {
+ return ""
+ }
+ // 跳过 base64 数据
+ if strings.HasPrefix(s, "base64://") || strings.Contains(s, ";base64,") {
+ return ""
+ }
+ // URL 路径
+ if strings.HasPrefix(s, "http://") || strings.HasPrefix(s, "https://") || strings.HasPrefix(s, "file://") {
+ if u, err := url.Parse(s); err == nil {
+ p := u.Path
+ if idx := strings.LastIndex(p, "/"); idx != -1 {
+ return p[idx+1:]
+ }
+ return p
+ }
+ }
+ // 本地文件路径
+ if strings.Contains(s, "/") || strings.Contains(s, "\\") {
+ if idx := strings.LastIndexAny(s, "/\\"); idx != -1 {
+ return s[idx+1:]
+ }
+ }
+ return s
+}
+
diff --git a/onebot/main.go b/onebot/main.go
index 7f9985b..e116291 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_9_58_mac.json", "微信配置文件路径: ../wechat_version/4_1_6_12_mac.json")
+ 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.ConnType, "conn_type", "http", "连接类型: http | websocket")
flag.IntVar(&config.SendInterval, "send_interval", 1000, "发送间隔: ms")
flag.IntVar(&config.WechatPid, "wechat_pid", 0, "微信进程 PID,不设置则自动查找")
@@ -294,6 +294,39 @@ func loadJs() {
m.ResultChan = ch.(chan error)
}
msgChan <- m
+ case "upload_voice_finish":
+ m := &SendMsg{
+ Type: "send_voice",
+ }
+ targetId := ""
+ if targetIdInter, ok := pMap["target_id"]; ok {
+ targetId = targetIdInter.(string)
+ if strings.Contains(targetId, "wxid_") {
+ m.UserId = targetId
+ } else {
+ m.GroupID = targetId
+ }
+ }
+ if cdnKey, ok := pMap["cdn_key"]; ok {
+ m.CdnKey = cdnKey.(string)
+ }
+ if aesKey, ok := pMap["aes_key"]; ok {
+ m.AesKey = aesKey.(string)
+ }
+ if voiceDuration, ok := pMap["voice_duration"]; ok {
+ if vd, ok := voiceDuration.(float64); ok {
+ m.VoiceDuration = int32(vd)
+ }
+ }
+ if silkDataLen, ok := pMap["silk_data_len"]; ok {
+ if sdl, ok := silkDataLen.(float64); ok {
+ m.SilkDataLen = int32(sdl)
+ }
+ }
+ if ch, ok := pendingResultMap.LoadAndDelete(targetId); ok {
+ m.ResultChan = ch.(chan error)
+ }
+ msgChan <- m
case "download":
err = Download(payloadJson)
if err != nil {
diff --git a/onebot/param.go b/onebot/param.go
index 1aa68f8..f7429a0 100644
--- a/onebot/param.go
+++ b/onebot/param.go
@@ -5,6 +5,7 @@ import (
"fmt"
"math/rand"
"sync"
+ "sync/atomic"
"github.com/frida/frida-go/frida"
)
@@ -27,12 +28,19 @@ var (
config = &Config{}
- userID2NicknameMap sync.Map
- userID2FileMsgMap sync.Map
- videoInfoMap sync.Map // targetId -> *VideoInfo
- buf2RespChan = make(chan *Buf2RespData, 10)
+ userID2NicknameMap sync.Map
+ userID2FileMsgMap sync.Map
+ videoInfoMap sync.Map // targetId -> *VideoInfo
+ buf2RespChan = make(chan *Buf2RespData, 10)
+ debugRespChan = make(chan []byte, 1)
+ appAttachRespChan = make(chan []byte, 1)
)
+// NextVersion 获取当前taskId作为版本号
+func NextVersion() uint32 {
+ return uint32(atomic.LoadInt64(&taskId))
+}
+
type WechatMessage struct {
GroupId string `json:"group_id"`
SelfID string `json:"self_id"`
@@ -73,6 +81,10 @@ type SendMsg struct {
Duration int32
VideoSize int32
+ VoiceDuration int32
+ SilkDataLen int32
+ Unknown13 int32
+
ReferMsgId string
ReferMsgSender string
ReferMsgType int
diff --git a/onebot/proto/check_md5.pb.go b/onebot/proto/check_md5.pb.go
new file mode 100644
index 0000000..f5cab5d
--- /dev/null
+++ b/onebot/proto/check_md5.pb.go
@@ -0,0 +1,213 @@
+// Code generated by protoc-gen-go. DO NOT EDIT.
+// versions:
+// protoc-gen-go v1.36.11
+// protoc v3.5.1
+// source: check_md5.proto
+
+package wxproto
+
+import (
+ protoreflect "google.golang.org/protobuf/reflect/protoreflect"
+ protoimpl "google.golang.org/protobuf/runtime/protoimpl"
+ reflect "reflect"
+ sync "sync"
+ unsafe "unsafe"
+)
+
+const (
+ // Verify that this generated code is sufficiently up-to-date.
+ _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
+ // Verify that runtime/protoimpl is sufficiently up-to-date.
+ _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
+)
+
+// /cgi-bin/micromsg-bin/checkmd5 请求
+type CheckMd5Request struct {
+ state protoimpl.MessageState `protogen:"open.v1"`
+ BaseRequest *ReplyMsgHeader `protobuf:"bytes,1,opt,name=baseRequest" json:"baseRequest,omitempty"`
+ FileKey *string `protobuf:"bytes,2,opt,name=fileKey" json:"fileKey,omitempty"` // CDN file key (attachid)
+ FileMd5 *string `protobuf:"bytes,3,opt,name=fileMd5" json:"fileMd5,omitempty"` // 文件MD5 (32位hex字符串)
+ Field4 []byte `protobuf:"bytes,4,opt,name=field4" json:"field4,omitempty"` // 空
+ Field5 *uint32 `protobuf:"varint,5,opt,name=field5" json:"field5,omitempty"` // 0
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
+}
+
+func (x *CheckMd5Request) Reset() {
+ *x = CheckMd5Request{}
+ mi := &file_check_md5_proto_msgTypes[0]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *CheckMd5Request) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*CheckMd5Request) ProtoMessage() {}
+
+func (x *CheckMd5Request) ProtoReflect() protoreflect.Message {
+ mi := &file_check_md5_proto_msgTypes[0]
+ if x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use CheckMd5Request.ProtoReflect.Descriptor instead.
+func (*CheckMd5Request) Descriptor() ([]byte, []int) {
+ return file_check_md5_proto_rawDescGZIP(), []int{0}
+}
+
+func (x *CheckMd5Request) GetBaseRequest() *ReplyMsgHeader {
+ if x != nil {
+ return x.BaseRequest
+ }
+ return nil
+}
+
+func (x *CheckMd5Request) GetFileKey() string {
+ if x != nil && x.FileKey != nil {
+ return *x.FileKey
+ }
+ return ""
+}
+
+func (x *CheckMd5Request) GetFileMd5() string {
+ if x != nil && x.FileMd5 != nil {
+ return *x.FileMd5
+ }
+ return ""
+}
+
+func (x *CheckMd5Request) GetField4() []byte {
+ if x != nil {
+ return x.Field4
+ }
+ return nil
+}
+
+func (x *CheckMd5Request) GetField5() uint32 {
+ if x != nil && x.Field5 != nil {
+ return *x.Field5
+ }
+ return 0
+}
+
+// /cgi-bin/micromsg-bin/checkmd5 响应
+type CheckMd5Response struct {
+ state protoimpl.MessageState `protogen:"open.v1"`
+ BaseResponse *BaseResponse `protobuf:"bytes,1,opt,name=baseResponse" json:"baseResponse,omitempty"`
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
+}
+
+func (x *CheckMd5Response) Reset() {
+ *x = CheckMd5Response{}
+ mi := &file_check_md5_proto_msgTypes[1]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *CheckMd5Response) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*CheckMd5Response) ProtoMessage() {}
+
+func (x *CheckMd5Response) ProtoReflect() protoreflect.Message {
+ mi := &file_check_md5_proto_msgTypes[1]
+ if x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use CheckMd5Response.ProtoReflect.Descriptor instead.
+func (*CheckMd5Response) Descriptor() ([]byte, []int) {
+ return file_check_md5_proto_rawDescGZIP(), []int{1}
+}
+
+func (x *CheckMd5Response) GetBaseResponse() *BaseResponse {
+ if x != nil {
+ return x.BaseResponse
+ }
+ return nil
+}
+
+var File_check_md5_proto protoreflect.FileDescriptor
+
+const file_check_md5_proto_rawDesc = "" +
+ "\n" +
+ "\x0fcheck_md5.proto\x12\awxproto\x1a\rapp_msg.proto\x1a\x11common_resp.proto\"\xb0\x01\n" +
+ "\x0fCheckMd5Request\x129\n" +
+ "\vbaseRequest\x18\x01 \x01(\v2\x17.wxproto.ReplyMsgHeaderR\vbaseRequest\x12\x18\n" +
+ "\afileKey\x18\x02 \x01(\tR\afileKey\x12\x18\n" +
+ "\afileMd5\x18\x03 \x01(\tR\afileMd5\x12\x16\n" +
+ "\x06field4\x18\x04 \x01(\fR\x06field4\x12\x16\n" +
+ "\x06field5\x18\x05 \x01(\rR\x06field5\"M\n" +
+ "\x10CheckMd5Response\x129\n" +
+ "\fbaseResponse\x18\x01 \x01(\v2\x15.wxproto.BaseResponseR\fbaseResponseB>Z wxproto.ReplyMsgHeader
+ 3, // 1: wxproto.CheckMd5Response.baseResponse:type_name -> wxproto.BaseResponse
+ 2, // [2:2] is the sub-list for method output_type
+ 2, // [2:2] is the sub-list for method input_type
+ 2, // [2:2] is the sub-list for extension type_name
+ 2, // [2:2] is the sub-list for extension extendee
+ 0, // [0:2] is the sub-list for field type_name
+}
+
+func init() { file_check_md5_proto_init() }
+func file_check_md5_proto_init() {
+ if File_check_md5_proto != nil {
+ return
+ }
+ file_app_msg_proto_init()
+ file_common_resp_proto_init()
+ type x struct{}
+ out := protoimpl.TypeBuilder{
+ File: protoimpl.DescBuilder{
+ GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
+ RawDescriptor: unsafe.Slice(unsafe.StringData(file_check_md5_proto_rawDesc), len(file_check_md5_proto_rawDesc)),
+ NumEnums: 0,
+ NumMessages: 2,
+ NumExtensions: 0,
+ NumServices: 0,
+ },
+ GoTypes: file_check_md5_proto_goTypes,
+ DependencyIndexes: file_check_md5_proto_depIdxs,
+ MessageInfos: file_check_md5_proto_msgTypes,
+ }.Build()
+ File_check_md5_proto = out.File
+ file_check_md5_proto_goTypes = nil
+ file_check_md5_proto_depIdxs = nil
+}
diff --git a/onebot/proto/file_msg.pb.go b/onebot/proto/file_msg.pb.go
new file mode 100644
index 0000000..a64dda8
--- /dev/null
+++ b/onebot/proto/file_msg.pb.go
@@ -0,0 +1,257 @@
+// Code generated by protoc-gen-go. DO NOT EDIT.
+// versions:
+// protoc-gen-go v1.36.11
+// protoc v3.5.1
+// source: file_msg.proto
+
+package wxproto
+
+import (
+ protoreflect "google.golang.org/protobuf/reflect/protoreflect"
+ protoimpl "google.golang.org/protobuf/runtime/protoimpl"
+ reflect "reflect"
+ sync "sync"
+ unsafe "unsafe"
+)
+
+const (
+ // Verify that this generated code is sufficiently up-to-date.
+ _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
+ // Verify that runtime/protoimpl is sufficiently up-to-date.
+ _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
+)
+
+// /cgi-bin/micromsg-bin/sendfileuploadmsg 请求
+type SendFileUploadMsgRequest struct {
+ state protoimpl.MessageState `protogen:"open.v1"`
+ BaseRequest *ReplyMsgHeader `protobuf:"bytes,1,opt,name=baseRequest" json:"baseRequest,omitempty"`
+ ToUserName *string `protobuf:"bytes,2,opt,name=toUserName" json:"toUserName,omitempty"`
+ ClientMsgId *string `protobuf:"bytes,3,opt,name=clientMsgId" json:"clientMsgId,omitempty"`
+ MsgType *uint32 `protobuf:"varint,4,opt,name=msgType" json:"msgType,omitempty"`
+ FileInfo *FileUploadFileInfo `protobuf:"bytes,5,opt,name=fileInfo" json:"fileInfo,omitempty"`
+ Field7 *uint32 `protobuf:"varint,7,opt,name=field7" json:"field7,omitempty"`
+ Field12 *uint32 `protobuf:"varint,12,opt,name=field12" json:"field12,omitempty"`
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
+}
+
+func (x *SendFileUploadMsgRequest) Reset() {
+ *x = SendFileUploadMsgRequest{}
+ mi := &file_file_msg_proto_msgTypes[0]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *SendFileUploadMsgRequest) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*SendFileUploadMsgRequest) ProtoMessage() {}
+
+func (x *SendFileUploadMsgRequest) ProtoReflect() protoreflect.Message {
+ mi := &file_file_msg_proto_msgTypes[0]
+ if x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use SendFileUploadMsgRequest.ProtoReflect.Descriptor instead.
+func (*SendFileUploadMsgRequest) Descriptor() ([]byte, []int) {
+ return file_file_msg_proto_rawDescGZIP(), []int{0}
+}
+
+func (x *SendFileUploadMsgRequest) GetBaseRequest() *ReplyMsgHeader {
+ if x != nil {
+ return x.BaseRequest
+ }
+ return nil
+}
+
+func (x *SendFileUploadMsgRequest) GetToUserName() string {
+ if x != nil && x.ToUserName != nil {
+ return *x.ToUserName
+ }
+ return ""
+}
+
+func (x *SendFileUploadMsgRequest) GetClientMsgId() string {
+ if x != nil && x.ClientMsgId != nil {
+ return *x.ClientMsgId
+ }
+ return ""
+}
+
+func (x *SendFileUploadMsgRequest) GetMsgType() uint32 {
+ if x != nil && x.MsgType != nil {
+ return *x.MsgType
+ }
+ return 0
+}
+
+func (x *SendFileUploadMsgRequest) GetFileInfo() *FileUploadFileInfo {
+ if x != nil {
+ return x.FileInfo
+ }
+ return nil
+}
+
+func (x *SendFileUploadMsgRequest) GetField7() uint32 {
+ if x != nil && x.Field7 != nil {
+ return *x.Field7
+ }
+ return 0
+}
+
+func (x *SendFileUploadMsgRequest) GetField12() uint32 {
+ if x != nil && x.Field12 != nil {
+ return *x.Field12
+ }
+ return 0
+}
+
+type FileUploadFileInfo struct {
+ state protoimpl.MessageState `protogen:"open.v1"`
+ FileName *string `protobuf:"bytes,1,opt,name=fileName" json:"fileName,omitempty"`
+ FileExt *string `protobuf:"bytes,2,opt,name=fileExt" json:"fileExt,omitempty"`
+ FileMd5 *string `protobuf:"bytes,3,opt,name=fileMd5" json:"fileMd5,omitempty"`
+ FileSize *uint32 `protobuf:"varint,4,opt,name=fileSize" json:"fileSize,omitempty"`
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
+}
+
+func (x *FileUploadFileInfo) Reset() {
+ *x = FileUploadFileInfo{}
+ mi := &file_file_msg_proto_msgTypes[1]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *FileUploadFileInfo) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*FileUploadFileInfo) ProtoMessage() {}
+
+func (x *FileUploadFileInfo) ProtoReflect() protoreflect.Message {
+ mi := &file_file_msg_proto_msgTypes[1]
+ if x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use FileUploadFileInfo.ProtoReflect.Descriptor instead.
+func (*FileUploadFileInfo) Descriptor() ([]byte, []int) {
+ return file_file_msg_proto_rawDescGZIP(), []int{1}
+}
+
+func (x *FileUploadFileInfo) GetFileName() string {
+ if x != nil && x.FileName != nil {
+ return *x.FileName
+ }
+ return ""
+}
+
+func (x *FileUploadFileInfo) GetFileExt() string {
+ if x != nil && x.FileExt != nil {
+ return *x.FileExt
+ }
+ return ""
+}
+
+func (x *FileUploadFileInfo) GetFileMd5() string {
+ if x != nil && x.FileMd5 != nil {
+ return *x.FileMd5
+ }
+ return ""
+}
+
+func (x *FileUploadFileInfo) GetFileSize() uint32 {
+ if x != nil && x.FileSize != nil {
+ return *x.FileSize
+ }
+ return 0
+}
+
+var File_file_msg_proto protoreflect.FileDescriptor
+
+const file_file_msg_proto_rawDesc = "" +
+ "\n" +
+ "\x0efile_msg.proto\x12\awxproto\x1a\rapp_msg.proto\"\x9c\x02\n" +
+ "\x18SendFileUploadMsgRequest\x129\n" +
+ "\vbaseRequest\x18\x01 \x01(\v2\x17.wxproto.ReplyMsgHeaderR\vbaseRequest\x12\x1e\n" +
+ "\n" +
+ "toUserName\x18\x02 \x01(\tR\n" +
+ "toUserName\x12 \n" +
+ "\vclientMsgId\x18\x03 \x01(\tR\vclientMsgId\x12\x18\n" +
+ "\amsgType\x18\x04 \x01(\rR\amsgType\x127\n" +
+ "\bfileInfo\x18\x05 \x01(\v2\x1b.wxproto.FileUploadFileInfoR\bfileInfo\x12\x16\n" +
+ "\x06field7\x18\a \x01(\rR\x06field7\x12\x18\n" +
+ "\afield12\x18\f \x01(\rR\afield12\"\x80\x01\n" +
+ "\x12FileUploadFileInfo\x12\x1a\n" +
+ "\bfileName\x18\x01 \x01(\tR\bfileName\x12\x18\n" +
+ "\afileExt\x18\x02 \x01(\tR\afileExt\x12\x18\n" +
+ "\afileMd5\x18\x03 \x01(\tR\afileMd5\x12\x1a\n" +
+ "\bfileSize\x18\x04 \x01(\rR\bfileSizeB>Z wxproto.ReplyMsgHeader
+ 1, // 1: wxproto.SendFileUploadMsgRequest.fileInfo:type_name -> wxproto.FileUploadFileInfo
+ 2, // [2:2] is the sub-list for method output_type
+ 2, // [2:2] is the sub-list for method input_type
+ 2, // [2:2] is the sub-list for extension type_name
+ 2, // [2:2] is the sub-list for extension extendee
+ 0, // [0:2] is the sub-list for field type_name
+}
+
+func init() { file_file_msg_proto_init() }
+func file_file_msg_proto_init() {
+ if File_file_msg_proto != nil {
+ return
+ }
+ file_app_msg_proto_init()
+ type x struct{}
+ out := protoimpl.TypeBuilder{
+ File: protoimpl.DescBuilder{
+ GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
+ RawDescriptor: unsafe.Slice(unsafe.StringData(file_file_msg_proto_rawDesc), len(file_file_msg_proto_rawDesc)),
+ NumEnums: 0,
+ NumMessages: 2,
+ NumExtensions: 0,
+ NumServices: 0,
+ },
+ GoTypes: file_file_msg_proto_goTypes,
+ DependencyIndexes: file_file_msg_proto_depIdxs,
+ MessageInfos: file_file_msg_proto_msgTypes,
+ }.Build()
+ File_file_msg_proto = out.File
+ file_file_msg_proto_goTypes = nil
+ file_file_msg_proto_depIdxs = nil
+}
diff --git a/onebot/proto/send_app_msg.pb.go b/onebot/proto/send_app_msg.pb.go
new file mode 100644
index 0000000..b1c3715
--- /dev/null
+++ b/onebot/proto/send_app_msg.pb.go
@@ -0,0 +1,495 @@
+// Code generated by protoc-gen-go. DO NOT EDIT.
+// versions:
+// protoc-gen-go v1.36.11
+// protoc v3.5.1
+// source: send_app_msg.proto
+
+package wxproto
+
+import (
+ protoreflect "google.golang.org/protobuf/reflect/protoreflect"
+ protoimpl "google.golang.org/protobuf/runtime/protoimpl"
+ reflect "reflect"
+ sync "sync"
+ unsafe "unsafe"
+)
+
+const (
+ // Verify that this generated code is sufficiently up-to-date.
+ _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
+ // Verify that runtime/protoimpl is sufficiently up-to-date.
+ _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
+)
+
+// /cgi-bin/micromsg-bin/sendappmsg 请求
+// 字段号严格对齐 wechat7016 SendAppMsgRequest,
+// 但 baseRequest 使用本项目 macOS 风格的 ReplyMsgHeader。
+type SendAppMsgReq struct {
+ state protoimpl.MessageState `protogen:"open.v1"`
+ BaseRequest *ReplyMsgHeader `protobuf:"bytes,1,opt,name=baseRequest" json:"baseRequest,omitempty"`
+ Msg *AppMsgBody `protobuf:"bytes,2,opt,name=msg" json:"msg,omitempty"`
+ CommentUrl *string `protobuf:"bytes,3,opt,name=commentUrl" json:"commentUrl,omitempty"`
+ ReqTime *uint32 `protobuf:"varint,4,opt,name=reqTime" json:"reqTime,omitempty"`
+ Md5 *string `protobuf:"bytes,5,opt,name=md5" json:"md5,omitempty"`
+ FileType *uint32 `protobuf:"varint,6,opt,name=fileType" json:"fileType,omitempty"`
+ Signature *string `protobuf:"bytes,7,opt,name=signature" json:"signature,omitempty"`
+ FromSence *string `protobuf:"bytes,8,opt,name=fromSence" json:"fromSence,omitempty"`
+ HitMd5 *uint32 `protobuf:"varint,9,opt,name=hitMd5" json:"hitMd5,omitempty"`
+ Crc32 *uint32 `protobuf:"varint,10,opt,name=crc32" json:"crc32,omitempty"`
+ MsgForwardType *uint32 `protobuf:"varint,11,opt,name=msgForwardType" json:"msgForwardType,omitempty"`
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
+}
+
+func (x *SendAppMsgReq) Reset() {
+ *x = SendAppMsgReq{}
+ mi := &file_send_app_msg_proto_msgTypes[0]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *SendAppMsgReq) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*SendAppMsgReq) ProtoMessage() {}
+
+func (x *SendAppMsgReq) ProtoReflect() protoreflect.Message {
+ mi := &file_send_app_msg_proto_msgTypes[0]
+ if x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use SendAppMsgReq.ProtoReflect.Descriptor instead.
+func (*SendAppMsgReq) Descriptor() ([]byte, []int) {
+ return file_send_app_msg_proto_rawDescGZIP(), []int{0}
+}
+
+func (x *SendAppMsgReq) GetBaseRequest() *ReplyMsgHeader {
+ if x != nil {
+ return x.BaseRequest
+ }
+ return nil
+}
+
+func (x *SendAppMsgReq) GetMsg() *AppMsgBody {
+ if x != nil {
+ return x.Msg
+ }
+ return nil
+}
+
+func (x *SendAppMsgReq) GetCommentUrl() string {
+ if x != nil && x.CommentUrl != nil {
+ return *x.CommentUrl
+ }
+ return ""
+}
+
+func (x *SendAppMsgReq) GetReqTime() uint32 {
+ if x != nil && x.ReqTime != nil {
+ return *x.ReqTime
+ }
+ return 0
+}
+
+func (x *SendAppMsgReq) GetMd5() string {
+ if x != nil && x.Md5 != nil {
+ return *x.Md5
+ }
+ return ""
+}
+
+func (x *SendAppMsgReq) GetFileType() uint32 {
+ if x != nil && x.FileType != nil {
+ return *x.FileType
+ }
+ return 0
+}
+
+func (x *SendAppMsgReq) GetSignature() string {
+ if x != nil && x.Signature != nil {
+ return *x.Signature
+ }
+ return ""
+}
+
+func (x *SendAppMsgReq) GetFromSence() string {
+ if x != nil && x.FromSence != nil {
+ return *x.FromSence
+ }
+ return ""
+}
+
+func (x *SendAppMsgReq) GetHitMd5() uint32 {
+ if x != nil && x.HitMd5 != nil {
+ return *x.HitMd5
+ }
+ return 0
+}
+
+func (x *SendAppMsgReq) GetCrc32() uint32 {
+ if x != nil && x.Crc32 != nil {
+ return *x.Crc32
+ }
+ return 0
+}
+
+func (x *SendAppMsgReq) GetMsgForwardType() uint32 {
+ if x != nil && x.MsgForwardType != nil {
+ return *x.MsgForwardType
+ }
+ return 0
+}
+
+// AppMsg 消息体 (wechat7016 AppMsg)
+type AppMsgBody struct {
+ state protoimpl.MessageState `protogen:"open.v1"`
+ FromUserName *string `protobuf:"bytes,1,opt,name=fromUserName" json:"fromUserName,omitempty"`
+ AppId *string `protobuf:"bytes,2,opt,name=appId" json:"appId,omitempty"`
+ SdkVersion *uint32 `protobuf:"varint,3,opt,name=sdkVersion" json:"sdkVersion,omitempty"`
+ ToUserName *string `protobuf:"bytes,4,opt,name=toUserName" json:"toUserName,omitempty"`
+ Type *uint32 `protobuf:"varint,5,opt,name=type" json:"type,omitempty"` // 6 = 文件
+ Content *string `protobuf:"bytes,6,opt,name=content" json:"content,omitempty"` // appmsg XML
+ CreateTime *int64 `protobuf:"varint,7,opt,name=createTime" json:"createTime,omitempty"`
+ ClientMsgId *string `protobuf:"bytes,8,opt,name=clientMsgId" json:"clientMsgId,omitempty"`
+ MsgSource *string `protobuf:"bytes,12,opt,name=msgSource" json:"msgSource,omitempty"`
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
+}
+
+func (x *AppMsgBody) Reset() {
+ *x = AppMsgBody{}
+ mi := &file_send_app_msg_proto_msgTypes[1]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *AppMsgBody) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*AppMsgBody) ProtoMessage() {}
+
+func (x *AppMsgBody) ProtoReflect() protoreflect.Message {
+ mi := &file_send_app_msg_proto_msgTypes[1]
+ if x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use AppMsgBody.ProtoReflect.Descriptor instead.
+func (*AppMsgBody) Descriptor() ([]byte, []int) {
+ return file_send_app_msg_proto_rawDescGZIP(), []int{1}
+}
+
+func (x *AppMsgBody) GetFromUserName() string {
+ if x != nil && x.FromUserName != nil {
+ return *x.FromUserName
+ }
+ return ""
+}
+
+func (x *AppMsgBody) GetAppId() string {
+ if x != nil && x.AppId != nil {
+ return *x.AppId
+ }
+ return ""
+}
+
+func (x *AppMsgBody) GetSdkVersion() uint32 {
+ if x != nil && x.SdkVersion != nil {
+ return *x.SdkVersion
+ }
+ return 0
+}
+
+func (x *AppMsgBody) GetToUserName() string {
+ if x != nil && x.ToUserName != nil {
+ return *x.ToUserName
+ }
+ return ""
+}
+
+func (x *AppMsgBody) GetType() uint32 {
+ if x != nil && x.Type != nil {
+ return *x.Type
+ }
+ return 0
+}
+
+func (x *AppMsgBody) GetContent() string {
+ if x != nil && x.Content != nil {
+ return *x.Content
+ }
+ return ""
+}
+
+func (x *AppMsgBody) GetCreateTime() int64 {
+ if x != nil && x.CreateTime != nil {
+ return *x.CreateTime
+ }
+ return 0
+}
+
+func (x *AppMsgBody) GetClientMsgId() string {
+ if x != nil && x.ClientMsgId != nil {
+ return *x.ClientMsgId
+ }
+ return ""
+}
+
+func (x *AppMsgBody) GetMsgSource() string {
+ if x != nil && x.MsgSource != nil {
+ return *x.MsgSource
+ }
+ return ""
+}
+
+// /cgi-bin/micromsg-bin/sendappmsg 响应
+type SendAppMsgResp struct {
+ state protoimpl.MessageState `protogen:"open.v1"`
+ BaseResponse *BaseResponse `protobuf:"bytes,1,opt,name=baseResponse" json:"baseResponse,omitempty"`
+ AppId *string `protobuf:"bytes,2,opt,name=appId" json:"appId,omitempty"`
+ FromUserName *string `protobuf:"bytes,3,opt,name=fromUserName" json:"fromUserName,omitempty"`
+ ToUserName *string `protobuf:"bytes,4,opt,name=toUserName" json:"toUserName,omitempty"`
+ MsgId *int32 `protobuf:"varint,5,opt,name=msgId" json:"msgId,omitempty"`
+ ClientMsgId *string `protobuf:"bytes,6,opt,name=clientMsgId" json:"clientMsgId,omitempty"`
+ CreateTime *int32 `protobuf:"varint,7,opt,name=createTime" json:"createTime,omitempty"`
+ Type *int32 `protobuf:"varint,8,opt,name=type" json:"type,omitempty"`
+ NewMsgId *int64 `protobuf:"varint,9,opt,name=newMsgId" json:"newMsgId,omitempty"`
+ Aeskey *string `protobuf:"bytes,10,opt,name=aeskey" json:"aeskey,omitempty"`
+ MsgSource *string `protobuf:"bytes,11,opt,name=msgSource" json:"msgSource,omitempty"`
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
+}
+
+func (x *SendAppMsgResp) Reset() {
+ *x = SendAppMsgResp{}
+ mi := &file_send_app_msg_proto_msgTypes[2]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *SendAppMsgResp) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*SendAppMsgResp) ProtoMessage() {}
+
+func (x *SendAppMsgResp) ProtoReflect() protoreflect.Message {
+ mi := &file_send_app_msg_proto_msgTypes[2]
+ if x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use SendAppMsgResp.ProtoReflect.Descriptor instead.
+func (*SendAppMsgResp) Descriptor() ([]byte, []int) {
+ return file_send_app_msg_proto_rawDescGZIP(), []int{2}
+}
+
+func (x *SendAppMsgResp) GetBaseResponse() *BaseResponse {
+ if x != nil {
+ return x.BaseResponse
+ }
+ return nil
+}
+
+func (x *SendAppMsgResp) GetAppId() string {
+ if x != nil && x.AppId != nil {
+ return *x.AppId
+ }
+ return ""
+}
+
+func (x *SendAppMsgResp) GetFromUserName() string {
+ if x != nil && x.FromUserName != nil {
+ return *x.FromUserName
+ }
+ return ""
+}
+
+func (x *SendAppMsgResp) GetToUserName() string {
+ if x != nil && x.ToUserName != nil {
+ return *x.ToUserName
+ }
+ return ""
+}
+
+func (x *SendAppMsgResp) GetMsgId() int32 {
+ if x != nil && x.MsgId != nil {
+ return *x.MsgId
+ }
+ return 0
+}
+
+func (x *SendAppMsgResp) GetClientMsgId() string {
+ if x != nil && x.ClientMsgId != nil {
+ return *x.ClientMsgId
+ }
+ return ""
+}
+
+func (x *SendAppMsgResp) GetCreateTime() int32 {
+ if x != nil && x.CreateTime != nil {
+ return *x.CreateTime
+ }
+ return 0
+}
+
+func (x *SendAppMsgResp) GetType() int32 {
+ if x != nil && x.Type != nil {
+ return *x.Type
+ }
+ return 0
+}
+
+func (x *SendAppMsgResp) GetNewMsgId() int64 {
+ if x != nil && x.NewMsgId != nil {
+ return *x.NewMsgId
+ }
+ return 0
+}
+
+func (x *SendAppMsgResp) GetAeskey() string {
+ if x != nil && x.Aeskey != nil {
+ return *x.Aeskey
+ }
+ return ""
+}
+
+func (x *SendAppMsgResp) GetMsgSource() string {
+ if x != nil && x.MsgSource != nil {
+ return *x.MsgSource
+ }
+ return ""
+}
+
+var File_send_app_msg_proto protoreflect.FileDescriptor
+
+const file_send_app_msg_proto_rawDesc = "" +
+ "\n" +
+ "\x12send_app_msg.proto\x12\awxproto\x1a\rapp_msg.proto\x1a\x11common_resp.proto\"\xeb\x02\n" +
+ "\rSendAppMsgReq\x129\n" +
+ "\vbaseRequest\x18\x01 \x01(\v2\x17.wxproto.ReplyMsgHeaderR\vbaseRequest\x12%\n" +
+ "\x03msg\x18\x02 \x01(\v2\x13.wxproto.AppMsgBodyR\x03msg\x12\x1e\n" +
+ "\n" +
+ "commentUrl\x18\x03 \x01(\tR\n" +
+ "commentUrl\x12\x18\n" +
+ "\areqTime\x18\x04 \x01(\rR\areqTime\x12\x10\n" +
+ "\x03md5\x18\x05 \x01(\tR\x03md5\x12\x1a\n" +
+ "\bfileType\x18\x06 \x01(\rR\bfileType\x12\x1c\n" +
+ "\tsignature\x18\a \x01(\tR\tsignature\x12\x1c\n" +
+ "\tfromSence\x18\b \x01(\tR\tfromSence\x12\x16\n" +
+ "\x06hitMd5\x18\t \x01(\rR\x06hitMd5\x12\x14\n" +
+ "\x05crc32\x18\n" +
+ " \x01(\rR\x05crc32\x12&\n" +
+ "\x0emsgForwardType\x18\v \x01(\rR\x0emsgForwardType\"\x94\x02\n" +
+ "\n" +
+ "AppMsgBody\x12\"\n" +
+ "\ffromUserName\x18\x01 \x01(\tR\ffromUserName\x12\x14\n" +
+ "\x05appId\x18\x02 \x01(\tR\x05appId\x12\x1e\n" +
+ "\n" +
+ "sdkVersion\x18\x03 \x01(\rR\n" +
+ "sdkVersion\x12\x1e\n" +
+ "\n" +
+ "toUserName\x18\x04 \x01(\tR\n" +
+ "toUserName\x12\x12\n" +
+ "\x04type\x18\x05 \x01(\rR\x04type\x12\x18\n" +
+ "\acontent\x18\x06 \x01(\tR\acontent\x12\x1e\n" +
+ "\n" +
+ "createTime\x18\a \x01(\x03R\n" +
+ "createTime\x12 \n" +
+ "\vclientMsgId\x18\b \x01(\tR\vclientMsgId\x12\x1c\n" +
+ "\tmsgSource\x18\f \x01(\tR\tmsgSource\"\xe3\x02\n" +
+ "\x0eSendAppMsgResp\x129\n" +
+ "\fbaseResponse\x18\x01 \x01(\v2\x15.wxproto.BaseResponseR\fbaseResponse\x12\x14\n" +
+ "\x05appId\x18\x02 \x01(\tR\x05appId\x12\"\n" +
+ "\ffromUserName\x18\x03 \x01(\tR\ffromUserName\x12\x1e\n" +
+ "\n" +
+ "toUserName\x18\x04 \x01(\tR\n" +
+ "toUserName\x12\x14\n" +
+ "\x05msgId\x18\x05 \x01(\x05R\x05msgId\x12 \n" +
+ "\vclientMsgId\x18\x06 \x01(\tR\vclientMsgId\x12\x1e\n" +
+ "\n" +
+ "createTime\x18\a \x01(\x05R\n" +
+ "createTime\x12\x12\n" +
+ "\x04type\x18\b \x01(\x05R\x04type\x12\x1a\n" +
+ "\bnewMsgId\x18\t \x01(\x03R\bnewMsgId\x12\x16\n" +
+ "\x06aeskey\x18\n" +
+ " \x01(\tR\x06aeskey\x12\x1c\n" +
+ "\tmsgSource\x18\v \x01(\tR\tmsgSourceB>Z wxproto.ReplyMsgHeader
+ 1, // 1: wxproto.SendAppMsgReq.msg:type_name -> wxproto.AppMsgBody
+ 4, // 2: wxproto.SendAppMsgResp.baseResponse:type_name -> wxproto.BaseResponse
+ 3, // [3:3] is the sub-list for method output_type
+ 3, // [3:3] is the sub-list for method input_type
+ 3, // [3:3] is the sub-list for extension type_name
+ 3, // [3:3] is the sub-list for extension extendee
+ 0, // [0:3] is the sub-list for field type_name
+}
+
+func init() { file_send_app_msg_proto_init() }
+func file_send_app_msg_proto_init() {
+ if File_send_app_msg_proto != nil {
+ return
+ }
+ file_app_msg_proto_init()
+ file_common_resp_proto_init()
+ type x struct{}
+ out := protoimpl.TypeBuilder{
+ File: protoimpl.DescBuilder{
+ GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
+ RawDescriptor: unsafe.Slice(unsafe.StringData(file_send_app_msg_proto_rawDesc), len(file_send_app_msg_proto_rawDesc)),
+ NumEnums: 0,
+ NumMessages: 3,
+ NumExtensions: 0,
+ NumServices: 0,
+ },
+ GoTypes: file_send_app_msg_proto_goTypes,
+ DependencyIndexes: file_send_app_msg_proto_depIdxs,
+ MessageInfos: file_send_app_msg_proto_msgTypes,
+ }.Build()
+ File_send_app_msg_proto = out.File
+ file_send_app_msg_proto_goTypes = nil
+ file_send_app_msg_proto_depIdxs = nil
+}
diff --git a/onebot/proto/upload_app_attach.pb.go b/onebot/proto/upload_app_attach.pb.go
new file mode 100644
index 0000000..29b2c7d
--- /dev/null
+++ b/onebot/proto/upload_app_attach.pb.go
@@ -0,0 +1,404 @@
+// Code generated by protoc-gen-go. DO NOT EDIT.
+// versions:
+// protoc-gen-go v1.36.11
+// protoc v3.5.1
+// source: upload_app_attach.proto
+
+package wxproto
+
+import (
+ protoreflect "google.golang.org/protobuf/reflect/protoreflect"
+ protoimpl "google.golang.org/protobuf/runtime/protoimpl"
+ reflect "reflect"
+ sync "sync"
+ unsafe "unsafe"
+)
+
+const (
+ // Verify that this generated code is sufficiently up-to-date.
+ _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
+ // Verify that runtime/protoimpl is sufficiently up-to-date.
+ _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
+)
+
+// 通用二进制缓冲(iPad860 SKBuiltinBufferT 对齐)
+type SKBuiltinBufferT struct {
+ state protoimpl.MessageState `protogen:"open.v1"`
+ ILen *uint32 `protobuf:"varint,1,opt,name=iLen" json:"iLen,omitempty"`
+ Buffer []byte `protobuf:"bytes,2,opt,name=buffer" json:"buffer,omitempty"`
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
+}
+
+func (x *SKBuiltinBufferT) Reset() {
+ *x = SKBuiltinBufferT{}
+ mi := &file_upload_app_attach_proto_msgTypes[0]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *SKBuiltinBufferT) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*SKBuiltinBufferT) ProtoMessage() {}
+
+func (x *SKBuiltinBufferT) ProtoReflect() protoreflect.Message {
+ mi := &file_upload_app_attach_proto_msgTypes[0]
+ if x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use SKBuiltinBufferT.ProtoReflect.Descriptor instead.
+func (*SKBuiltinBufferT) Descriptor() ([]byte, []int) {
+ return file_upload_app_attach_proto_rawDescGZIP(), []int{0}
+}
+
+func (x *SKBuiltinBufferT) GetILen() uint32 {
+ if x != nil && x.ILen != nil {
+ return *x.ILen
+ }
+ return 0
+}
+
+func (x *SKBuiltinBufferT) GetBuffer() []byte {
+ if x != nil {
+ return x.Buffer
+ }
+ return nil
+}
+
+// /cgi-bin/micromsg-bin/uploadappattach 请求
+// 参考 we-chat-ipad860 wechat.proto UploadAppAttachRequest,
+// 但 baseRequest 使用本项目 macOS 风格的 ReplyMsgHeader。
+type UploadAppAttachRequest struct {
+ state protoimpl.MessageState `protogen:"open.v1"`
+ BaseRequest *ReplyMsgHeader `protobuf:"bytes,1,opt,name=baseRequest" json:"baseRequest,omitempty"`
+ AppId *string `protobuf:"bytes,2,opt,name=appId" json:"appId,omitempty"`
+ SdkVersion *uint32 `protobuf:"varint,3,opt,name=sdkVersion" json:"sdkVersion,omitempty"`
+ ClientAppDataId *string `protobuf:"bytes,4,opt,name=clientAppDataId" json:"clientAppDataId,omitempty"` // {receiver}_{ts}_UploadFile
+ UserName *string `protobuf:"bytes,5,opt,name=userName" json:"userName,omitempty"` // 接收方 wxid
+ TotalLen *uint32 `protobuf:"varint,6,opt,name=totalLen" json:"totalLen,omitempty"` // 文件总长度
+ StartPos *uint32 `protobuf:"varint,7,opt,name=startPos" json:"startPos,omitempty"` // 本分片偏移
+ DataLen *uint32 `protobuf:"varint,8,opt,name=dataLen" json:"dataLen,omitempty"` // 本分片长度
+ Data *SKBuiltinBufferT `protobuf:"bytes,9,opt,name=data" json:"data,omitempty"` // 本分片原始字节
+ Type *uint32 `protobuf:"varint,10,opt,name=type" json:"type,omitempty"` // 6 = 文件
+ Md5 *string `protobuf:"bytes,11,opt,name=md5" json:"md5,omitempty"` // 文件 MD5
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
+}
+
+func (x *UploadAppAttachRequest) Reset() {
+ *x = UploadAppAttachRequest{}
+ mi := &file_upload_app_attach_proto_msgTypes[1]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *UploadAppAttachRequest) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*UploadAppAttachRequest) ProtoMessage() {}
+
+func (x *UploadAppAttachRequest) ProtoReflect() protoreflect.Message {
+ mi := &file_upload_app_attach_proto_msgTypes[1]
+ if x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use UploadAppAttachRequest.ProtoReflect.Descriptor instead.
+func (*UploadAppAttachRequest) Descriptor() ([]byte, []int) {
+ return file_upload_app_attach_proto_rawDescGZIP(), []int{1}
+}
+
+func (x *UploadAppAttachRequest) GetBaseRequest() *ReplyMsgHeader {
+ if x != nil {
+ return x.BaseRequest
+ }
+ return nil
+}
+
+func (x *UploadAppAttachRequest) GetAppId() string {
+ if x != nil && x.AppId != nil {
+ return *x.AppId
+ }
+ return ""
+}
+
+func (x *UploadAppAttachRequest) GetSdkVersion() uint32 {
+ if x != nil && x.SdkVersion != nil {
+ return *x.SdkVersion
+ }
+ return 0
+}
+
+func (x *UploadAppAttachRequest) GetClientAppDataId() string {
+ if x != nil && x.ClientAppDataId != nil {
+ return *x.ClientAppDataId
+ }
+ return ""
+}
+
+func (x *UploadAppAttachRequest) GetUserName() string {
+ if x != nil && x.UserName != nil {
+ return *x.UserName
+ }
+ return ""
+}
+
+func (x *UploadAppAttachRequest) GetTotalLen() uint32 {
+ if x != nil && x.TotalLen != nil {
+ return *x.TotalLen
+ }
+ return 0
+}
+
+func (x *UploadAppAttachRequest) GetStartPos() uint32 {
+ if x != nil && x.StartPos != nil {
+ return *x.StartPos
+ }
+ return 0
+}
+
+func (x *UploadAppAttachRequest) GetDataLen() uint32 {
+ if x != nil && x.DataLen != nil {
+ return *x.DataLen
+ }
+ return 0
+}
+
+func (x *UploadAppAttachRequest) GetData() *SKBuiltinBufferT {
+ if x != nil {
+ return x.Data
+ }
+ return nil
+}
+
+func (x *UploadAppAttachRequest) GetType() uint32 {
+ if x != nil && x.Type != nil {
+ return *x.Type
+ }
+ return 0
+}
+
+func (x *UploadAppAttachRequest) GetMd5() string {
+ if x != nil && x.Md5 != nil {
+ return *x.Md5
+ }
+ return ""
+}
+
+// /cgi-bin/micromsg-bin/uploadappattach 响应
+type UploadAppAttachResponse struct {
+ state protoimpl.MessageState `protogen:"open.v1"`
+ BaseResponse *BaseResponse `protobuf:"bytes,1,opt,name=baseResponse" json:"baseResponse,omitempty"`
+ AppId *string `protobuf:"bytes,2,opt,name=appId" json:"appId,omitempty"`
+ MediaId *string `protobuf:"bytes,3,opt,name=mediaId" json:"mediaId,omitempty"` // attachid
+ ClientAppDataId *string `protobuf:"bytes,4,opt,name=clientAppDataId" json:"clientAppDataId,omitempty"`
+ UserName *string `protobuf:"bytes,5,opt,name=userName" json:"userName,omitempty"`
+ TotalLen *uint32 `protobuf:"varint,6,opt,name=totalLen" json:"totalLen,omitempty"`
+ StartPos *uint32 `protobuf:"varint,7,opt,name=startPos" json:"startPos,omitempty"`
+ DataLen *uint32 `protobuf:"varint,8,opt,name=dataLen" json:"dataLen,omitempty"`
+ CreateTime *uint64 `protobuf:"varint,9,opt,name=createTime" json:"createTime,omitempty"`
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
+}
+
+func (x *UploadAppAttachResponse) Reset() {
+ *x = UploadAppAttachResponse{}
+ mi := &file_upload_app_attach_proto_msgTypes[2]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *UploadAppAttachResponse) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*UploadAppAttachResponse) ProtoMessage() {}
+
+func (x *UploadAppAttachResponse) ProtoReflect() protoreflect.Message {
+ mi := &file_upload_app_attach_proto_msgTypes[2]
+ if x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use UploadAppAttachResponse.ProtoReflect.Descriptor instead.
+func (*UploadAppAttachResponse) Descriptor() ([]byte, []int) {
+ return file_upload_app_attach_proto_rawDescGZIP(), []int{2}
+}
+
+func (x *UploadAppAttachResponse) GetBaseResponse() *BaseResponse {
+ if x != nil {
+ return x.BaseResponse
+ }
+ return nil
+}
+
+func (x *UploadAppAttachResponse) GetAppId() string {
+ if x != nil && x.AppId != nil {
+ return *x.AppId
+ }
+ return ""
+}
+
+func (x *UploadAppAttachResponse) GetMediaId() string {
+ if x != nil && x.MediaId != nil {
+ return *x.MediaId
+ }
+ return ""
+}
+
+func (x *UploadAppAttachResponse) GetClientAppDataId() string {
+ if x != nil && x.ClientAppDataId != nil {
+ return *x.ClientAppDataId
+ }
+ return ""
+}
+
+func (x *UploadAppAttachResponse) GetUserName() string {
+ if x != nil && x.UserName != nil {
+ return *x.UserName
+ }
+ return ""
+}
+
+func (x *UploadAppAttachResponse) GetTotalLen() uint32 {
+ if x != nil && x.TotalLen != nil {
+ return *x.TotalLen
+ }
+ return 0
+}
+
+func (x *UploadAppAttachResponse) GetStartPos() uint32 {
+ if x != nil && x.StartPos != nil {
+ return *x.StartPos
+ }
+ return 0
+}
+
+func (x *UploadAppAttachResponse) GetDataLen() uint32 {
+ if x != nil && x.DataLen != nil {
+ return *x.DataLen
+ }
+ return 0
+}
+
+func (x *UploadAppAttachResponse) GetCreateTime() uint64 {
+ if x != nil && x.CreateTime != nil {
+ return *x.CreateTime
+ }
+ return 0
+}
+
+var File_upload_app_attach_proto protoreflect.FileDescriptor
+
+const file_upload_app_attach_proto_rawDesc = "" +
+ "\n" +
+ "\x17upload_app_attach.proto\x12\awxproto\x1a\rapp_msg.proto\x1a\x11common_resp.proto\">\n" +
+ "\x10SKBuiltinBufferT\x12\x12\n" +
+ "\x04iLen\x18\x01 \x01(\rR\x04iLen\x12\x16\n" +
+ "\x06buffer\x18\x02 \x01(\fR\x06buffer\"\xf6\x02\n" +
+ "\x16UploadAppAttachRequest\x129\n" +
+ "\vbaseRequest\x18\x01 \x01(\v2\x17.wxproto.ReplyMsgHeaderR\vbaseRequest\x12\x14\n" +
+ "\x05appId\x18\x02 \x01(\tR\x05appId\x12\x1e\n" +
+ "\n" +
+ "sdkVersion\x18\x03 \x01(\rR\n" +
+ "sdkVersion\x12(\n" +
+ "\x0fclientAppDataId\x18\x04 \x01(\tR\x0fclientAppDataId\x12\x1a\n" +
+ "\buserName\x18\x05 \x01(\tR\buserName\x12\x1a\n" +
+ "\btotalLen\x18\x06 \x01(\rR\btotalLen\x12\x1a\n" +
+ "\bstartPos\x18\a \x01(\rR\bstartPos\x12\x18\n" +
+ "\adataLen\x18\b \x01(\rR\adataLen\x12-\n" +
+ "\x04data\x18\t \x01(\v2\x19.wxproto.SKBuiltinBufferTR\x04data\x12\x12\n" +
+ "\x04type\x18\n" +
+ " \x01(\rR\x04type\x12\x10\n" +
+ "\x03md5\x18\v \x01(\tR\x03md5\"\xbc\x02\n" +
+ "\x17UploadAppAttachResponse\x129\n" +
+ "\fbaseResponse\x18\x01 \x01(\v2\x15.wxproto.BaseResponseR\fbaseResponse\x12\x14\n" +
+ "\x05appId\x18\x02 \x01(\tR\x05appId\x12\x18\n" +
+ "\amediaId\x18\x03 \x01(\tR\amediaId\x12(\n" +
+ "\x0fclientAppDataId\x18\x04 \x01(\tR\x0fclientAppDataId\x12\x1a\n" +
+ "\buserName\x18\x05 \x01(\tR\buserName\x12\x1a\n" +
+ "\btotalLen\x18\x06 \x01(\rR\btotalLen\x12\x1a\n" +
+ "\bstartPos\x18\a \x01(\rR\bstartPos\x12\x18\n" +
+ "\adataLen\x18\b \x01(\rR\adataLen\x12\x1e\n" +
+ "\n" +
+ "createTime\x18\t \x01(\x04R\n" +
+ "createTimeB>Z wxproto.ReplyMsgHeader
+ 0, // 1: wxproto.UploadAppAttachRequest.data:type_name -> wxproto.SKBuiltinBufferT
+ 4, // 2: wxproto.UploadAppAttachResponse.baseResponse:type_name -> wxproto.BaseResponse
+ 3, // [3:3] is the sub-list for method output_type
+ 3, // [3:3] is the sub-list for method input_type
+ 3, // [3:3] is the sub-list for extension type_name
+ 3, // [3:3] is the sub-list for extension extendee
+ 0, // [0:3] is the sub-list for field type_name
+}
+
+func init() { file_upload_app_attach_proto_init() }
+func file_upload_app_attach_proto_init() {
+ if File_upload_app_attach_proto != nil {
+ return
+ }
+ file_app_msg_proto_init()
+ file_common_resp_proto_init()
+ type x struct{}
+ out := protoimpl.TypeBuilder{
+ File: protoimpl.DescBuilder{
+ GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
+ RawDescriptor: unsafe.Slice(unsafe.StringData(file_upload_app_attach_proto_rawDesc), len(file_upload_app_attach_proto_rawDesc)),
+ NumEnums: 0,
+ NumMessages: 3,
+ NumExtensions: 0,
+ NumServices: 0,
+ },
+ GoTypes: file_upload_app_attach_proto_goTypes,
+ DependencyIndexes: file_upload_app_attach_proto_depIdxs,
+ MessageInfos: file_upload_app_attach_proto_msgTypes,
+ }.Build()
+ File_upload_app_attach_proto = out.File
+ file_upload_app_attach_proto_goTypes = nil
+ file_upload_app_attach_proto_depIdxs = nil
+}
diff --git a/onebot/proto/voice_msg.pb.go b/onebot/proto/voice_msg.pb.go
new file mode 100644
index 0000000..7c39961
--- /dev/null
+++ b/onebot/proto/voice_msg.pb.go
@@ -0,0 +1,388 @@
+// Code generated by protoc-gen-go. DO NOT EDIT.
+// versions:
+// protoc-gen-go v1.36.11
+// protoc v3.5.1
+// source: voice_msg.proto
+
+package wxproto
+
+import (
+ protoreflect "google.golang.org/protobuf/reflect/protoreflect"
+ protoimpl "google.golang.org/protobuf/runtime/protoimpl"
+ reflect "reflect"
+ sync "sync"
+ unsafe "unsafe"
+)
+
+const (
+ // Verify that this generated code is sufficiently up-to-date.
+ _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
+ // Verify that runtime/protoimpl is sufficiently up-to-date.
+ _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
+)
+
+// 发送语音消息的protobuf结构
+type WxSendVoiceMsg struct {
+ state protoimpl.MessageState `protogen:"open.v1"`
+ FromUser string `protobuf:"bytes,1,opt,name=from_user,json=fromUser,proto3" json:"from_user,omitempty"`
+ ToUser string `protobuf:"bytes,2,opt,name=to_user,json=toUser,proto3" json:"to_user,omitempty"`
+ Unknown3 int32 `protobuf:"varint,3,opt,name=unknown3,proto3" json:"unknown3,omitempty"`
+ Unknown4 int32 `protobuf:"varint,4,opt,name=unknown4,proto3" json:"unknown4,omitempty"`
+ ClientMsgId string `protobuf:"bytes,5,opt,name=client_msg_id,json=clientMsgId,proto3" json:"client_msg_id,omitempty"`
+ // field 6 skipped
+ Duration int32 `protobuf:"varint,7,opt,name=duration,proto3" json:"duration,omitempty"`
+ // field 8 skipped
+ Unknown9 int32 `protobuf:"varint,9,opt,name=unknown9,proto3" json:"unknown9,omitempty"`
+ Header *VoiceMsgHeader `protobuf:"bytes,10,opt,name=header,proto3" json:"header,omitempty"`
+ Unknown11 int32 `protobuf:"varint,11,opt,name=unknown11,proto3" json:"unknown11,omitempty"`
+ // field 12 skipped
+ Unknown13 int32 `protobuf:"varint,13,opt,name=unknown13,proto3" json:"unknown13,omitempty"`
+ // field 14 skipped
+ Unknown15 int32 `protobuf:"varint,15,opt,name=unknown15,proto3" json:"unknown15,omitempty"`
+ Unknown16 int32 `protobuf:"varint,16,opt,name=unknown16,proto3" json:"unknown16,omitempty"`
+ Unknown17 int64 `protobuf:"varint,17,opt,name=unknown17,proto3" json:"unknown17,omitempty"`
+ // fields 18-19 skipped
+ AesKey []byte `protobuf:"bytes,20,opt,name=aes_key,json=aesKey,proto3" json:"aes_key,omitempty"`
+ CdnKey []byte `protobuf:"bytes,21,opt,name=cdn_key,json=cdnKey,proto3" json:"cdn_key,omitempty"`
+ // fields 22-23 skipped
+ Unknown24 []byte `protobuf:"bytes,24,opt,name=unknown24,proto3" json:"unknown24,omitempty"`
+ Unknown25 int32 `protobuf:"varint,25,opt,name=unknown25,proto3" json:"unknown25,omitempty"`
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
+}
+
+func (x *WxSendVoiceMsg) Reset() {
+ *x = WxSendVoiceMsg{}
+ mi := &file_voice_msg_proto_msgTypes[0]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *WxSendVoiceMsg) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*WxSendVoiceMsg) ProtoMessage() {}
+
+func (x *WxSendVoiceMsg) ProtoReflect() protoreflect.Message {
+ mi := &file_voice_msg_proto_msgTypes[0]
+ if x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use WxSendVoiceMsg.ProtoReflect.Descriptor instead.
+func (*WxSendVoiceMsg) Descriptor() ([]byte, []int) {
+ return file_voice_msg_proto_rawDescGZIP(), []int{0}
+}
+
+func (x *WxSendVoiceMsg) GetFromUser() string {
+ if x != nil {
+ return x.FromUser
+ }
+ return ""
+}
+
+func (x *WxSendVoiceMsg) GetToUser() string {
+ if x != nil {
+ return x.ToUser
+ }
+ return ""
+}
+
+func (x *WxSendVoiceMsg) GetUnknown3() int32 {
+ if x != nil {
+ return x.Unknown3
+ }
+ return 0
+}
+
+func (x *WxSendVoiceMsg) GetUnknown4() int32 {
+ if x != nil {
+ return x.Unknown4
+ }
+ return 0
+}
+
+func (x *WxSendVoiceMsg) GetClientMsgId() string {
+ if x != nil {
+ return x.ClientMsgId
+ }
+ return ""
+}
+
+func (x *WxSendVoiceMsg) GetDuration() int32 {
+ if x != nil {
+ return x.Duration
+ }
+ return 0
+}
+
+func (x *WxSendVoiceMsg) GetUnknown9() int32 {
+ if x != nil {
+ return x.Unknown9
+ }
+ return 0
+}
+
+func (x *WxSendVoiceMsg) GetHeader() *VoiceMsgHeader {
+ if x != nil {
+ return x.Header
+ }
+ return nil
+}
+
+func (x *WxSendVoiceMsg) GetUnknown11() int32 {
+ if x != nil {
+ return x.Unknown11
+ }
+ return 0
+}
+
+func (x *WxSendVoiceMsg) GetUnknown13() int32 {
+ if x != nil {
+ return x.Unknown13
+ }
+ return 0
+}
+
+func (x *WxSendVoiceMsg) GetUnknown15() int32 {
+ if x != nil {
+ return x.Unknown15
+ }
+ return 0
+}
+
+func (x *WxSendVoiceMsg) GetUnknown16() int32 {
+ if x != nil {
+ return x.Unknown16
+ }
+ return 0
+}
+
+func (x *WxSendVoiceMsg) GetUnknown17() int64 {
+ if x != nil {
+ return x.Unknown17
+ }
+ return 0
+}
+
+func (x *WxSendVoiceMsg) GetAesKey() []byte {
+ if x != nil {
+ return x.AesKey
+ }
+ return nil
+}
+
+func (x *WxSendVoiceMsg) GetCdnKey() []byte {
+ if x != nil {
+ return x.CdnKey
+ }
+ return nil
+}
+
+func (x *WxSendVoiceMsg) GetUnknown24() []byte {
+ if x != nil {
+ return x.Unknown24
+ }
+ return nil
+}
+
+func (x *WxSendVoiceMsg) GetUnknown25() int32 {
+ if x != nil {
+ return x.Unknown25
+ }
+ return 0
+}
+
+type VoiceMsgHeader struct {
+ state protoimpl.MessageState `protogen:"open.v1"`
+ Flag []byte `protobuf:"bytes,1,opt,name=flag,proto3" json:"flag,omitempty"`
+ SessionId int64 `protobuf:"varint,2,opt,name=session_id,json=sessionId,proto3" json:"session_id,omitempty"`
+ ClientProof []byte `protobuf:"bytes,3,opt,name=client_proof,json=clientProof,proto3" json:"client_proof,omitempty"`
+ DeviceId int64 `protobuf:"varint,4,opt,name=device_id,json=deviceId,proto3" json:"device_id,omitempty"`
+ Platform []byte `protobuf:"bytes,5,opt,name=platform,proto3" json:"platform,omitempty"`
+ Version int32 `protobuf:"varint,6,opt,name=version,proto3" json:"version,omitempty"`
+ // field 7 skipped
+ Unknown8 int32 `protobuf:"varint,8,opt,name=unknown8,proto3" json:"unknown8,omitempty"`
+ // fields 9-14 skipped
+ Unknown15 int32 `protobuf:"varint,15,opt,name=unknown15,proto3" json:"unknown15,omitempty"`
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
+}
+
+func (x *VoiceMsgHeader) Reset() {
+ *x = VoiceMsgHeader{}
+ mi := &file_voice_msg_proto_msgTypes[1]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *VoiceMsgHeader) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*VoiceMsgHeader) ProtoMessage() {}
+
+func (x *VoiceMsgHeader) ProtoReflect() protoreflect.Message {
+ mi := &file_voice_msg_proto_msgTypes[1]
+ if x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use VoiceMsgHeader.ProtoReflect.Descriptor instead.
+func (*VoiceMsgHeader) Descriptor() ([]byte, []int) {
+ return file_voice_msg_proto_rawDescGZIP(), []int{1}
+}
+
+func (x *VoiceMsgHeader) GetFlag() []byte {
+ if x != nil {
+ return x.Flag
+ }
+ return nil
+}
+
+func (x *VoiceMsgHeader) GetSessionId() int64 {
+ if x != nil {
+ return x.SessionId
+ }
+ return 0
+}
+
+func (x *VoiceMsgHeader) GetClientProof() []byte {
+ if x != nil {
+ return x.ClientProof
+ }
+ return nil
+}
+
+func (x *VoiceMsgHeader) GetDeviceId() int64 {
+ if x != nil {
+ return x.DeviceId
+ }
+ return 0
+}
+
+func (x *VoiceMsgHeader) GetPlatform() []byte {
+ if x != nil {
+ return x.Platform
+ }
+ return nil
+}
+
+func (x *VoiceMsgHeader) GetVersion() int32 {
+ if x != nil {
+ return x.Version
+ }
+ return 0
+}
+
+func (x *VoiceMsgHeader) GetUnknown8() int32 {
+ if x != nil {
+ return x.Unknown8
+ }
+ return 0
+}
+
+func (x *VoiceMsgHeader) GetUnknown15() int32 {
+ if x != nil {
+ return x.Unknown15
+ }
+ return 0
+}
+
+var File_voice_msg_proto protoreflect.FileDescriptor
+
+const file_voice_msg_proto_rawDesc = "" +
+ "\n" +
+ "\x0fvoice_msg.proto\x12\awxproto\"\x8f\x04\n" +
+ "\x0eWxSendVoiceMsg\x12\x1b\n" +
+ "\tfrom_user\x18\x01 \x01(\tR\bfromUser\x12\x17\n" +
+ "\ato_user\x18\x02 \x01(\tR\x06toUser\x12\x1a\n" +
+ "\bunknown3\x18\x03 \x01(\x05R\bunknown3\x12\x1a\n" +
+ "\bunknown4\x18\x04 \x01(\x05R\bunknown4\x12\"\n" +
+ "\rclient_msg_id\x18\x05 \x01(\tR\vclientMsgId\x12\x1a\n" +
+ "\bduration\x18\a \x01(\x05R\bduration\x12\x1a\n" +
+ "\bunknown9\x18\t \x01(\x05R\bunknown9\x12/\n" +
+ "\x06header\x18\n" +
+ " \x01(\v2\x17.wxproto.VoiceMsgHeaderR\x06header\x12\x1c\n" +
+ "\tunknown11\x18\v \x01(\x05R\tunknown11\x12\x1c\n" +
+ "\tunknown13\x18\r \x01(\x05R\tunknown13\x12\x1c\n" +
+ "\tunknown15\x18\x0f \x01(\x05R\tunknown15\x12\x1c\n" +
+ "\tunknown16\x18\x10 \x01(\x05R\tunknown16\x12\x1c\n" +
+ "\tunknown17\x18\x11 \x01(\x03R\tunknown17\x12\x17\n" +
+ "\aaes_key\x18\x14 \x01(\fR\x06aesKey\x12\x17\n" +
+ "\acdn_key\x18\x15 \x01(\fR\x06cdnKey\x12\x1c\n" +
+ "\tunknown24\x18\x18 \x01(\fR\tunknown24\x12\x1c\n" +
+ "\tunknown25\x18\x19 \x01(\x05R\tunknown25\"\xf3\x01\n" +
+ "\x0eVoiceMsgHeader\x12\x12\n" +
+ "\x04flag\x18\x01 \x01(\fR\x04flag\x12\x1d\n" +
+ "\n" +
+ "session_id\x18\x02 \x01(\x03R\tsessionId\x12!\n" +
+ "\fclient_proof\x18\x03 \x01(\fR\vclientProof\x12\x1b\n" +
+ "\tdevice_id\x18\x04 \x01(\x03R\bdeviceId\x12\x1a\n" +
+ "\bplatform\x18\x05 \x01(\fR\bplatform\x12\x18\n" +
+ "\aversion\x18\x06 \x01(\x05R\aversion\x12\x1a\n" +
+ "\bunknown8\x18\b \x01(\x05R\bunknown8\x12\x1c\n" +
+ "\tunknown15\x18\x0f \x01(\x05R\tunknown15B>Z wxproto.VoiceMsgHeader
+ 1, // [1:1] is the sub-list for method output_type
+ 1, // [1:1] is the sub-list for method input_type
+ 1, // [1:1] is the sub-list for extension type_name
+ 1, // [1:1] is the sub-list for extension extendee
+ 0, // [0:1] is the sub-list for field type_name
+}
+
+func init() { file_voice_msg_proto_init() }
+func file_voice_msg_proto_init() {
+ if File_voice_msg_proto != nil {
+ return
+ }
+ type x struct{}
+ out := protoimpl.TypeBuilder{
+ File: protoimpl.DescBuilder{
+ GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
+ RawDescriptor: unsafe.Slice(unsafe.StringData(file_voice_msg_proto_rawDesc), len(file_voice_msg_proto_rawDesc)),
+ NumEnums: 0,
+ NumMessages: 2,
+ NumExtensions: 0,
+ NumServices: 0,
+ },
+ GoTypes: file_voice_msg_proto_goTypes,
+ DependencyIndexes: file_voice_msg_proto_depIdxs,
+ MessageInfos: file_voice_msg_proto_msgTypes,
+ }.Build()
+ File_voice_msg_proto = out.File
+ file_voice_msg_proto_goTypes = nil
+ file_voice_msg_proto_depIdxs = nil
+}
diff --git a/onebot/script.js b/onebot/script.js
index 51c1af0..05470f1 100644
--- a/onebot/script.js
+++ b/onebot/script.js
@@ -93,6 +93,7 @@ function initAddresses() {
setImmediate(setupSendTextMessageDynamic);
setImmediate(setupSendFileMessageDynamic);
setImmediate(setupSendFileUploadMessageDynamic);
+ setImmediate(setupSendAppAttachMessageDynamic);
setImmediate(attachBlrX8Hook);
setImmediate(AttachSendFunc);
setImmediate(attachReq2buf);
@@ -332,6 +333,8 @@ var replyProtoHexGlobal = "";
// 文件消息protobuf全局变量 (从Go直接传入hex编码)
var fileProtoHexGlobal = "";
var fileUploadProtoHexGlobal = "";
+// uploadappattach protobuf全局变量 (从Go直接传入hex编码)
+var appAttachProtoHexGlobal = "";
// 文件消息全局变量
var fileCgiAddr = ptr(0);
@@ -345,6 +348,11 @@ var fileUploadCgiAddr = ptr(0);
var sendFileUploadMessageAddr = ptr(0);
var fileUploadMessageAddr = ptr(0);
+// uploadappattach 全局变量
+var appAttachCgiAddr = ptr(0);
+var sendAppAttachMessageAddr = ptr(0);
+var appAttachMessageAddr = ptr(0);
+
// 回复消息全局变量
var replyMessageCallbackFunc;
var replyCgiAddr = ptr(0);
@@ -362,7 +370,7 @@ function setupSendTextMessageDynamic() {
textCgiAddr = Memory.alloc(128);
sendTextMessageAddr = Memory.alloc(256);
textMessageAddr = Memory.alloc(256);
- textProtoDataAddr = Memory.alloc(4096);
+ textProtoDataAddr = Memory.alloc(64 * 1024); // 支持 50KB 分片(uploadappattach)的 protobuf
// A. 写入字符串内容
patchString(textCgiAddr, "/cgi-bin/micromsg-bin/newsendmsg");
@@ -452,6 +460,35 @@ function triggerSendFileUploadMessage(taskId, sender, receiver, protoHex, payloa
return triggerSendMediaMessage(taskId, sender, receiver, protoHex, payloadHex, "fileupload");
}
+// -------------------------uploadappattach分区-------------------------
+function setupSendAppAttachMessageDynamic() {
+ appAttachCgiAddr = Memory.alloc(128);
+ sendAppAttachMessageAddr = Memory.alloc(256);
+ appAttachMessageAddr = Memory.alloc(256);
+
+ patchString(appAttachCgiAddr, "/cgi-bin/micromsg-bin/uploadappattach");
+
+ sendAppAttachMessageAddr.add(0x00).writeU64(0);
+ sendAppAttachMessageAddr.add(0x08).writeU64(0);
+ sendAppAttachMessageAddr.add(0x10).writeU64(0);
+ sendAppAttachMessageAddr.add(0x18).writeU64(1);
+ sendAppAttachMessageAddr.add(0x20).writeU32(taskIdGlobal);
+ sendAppAttachMessageAddr.add(0x28).writePointer(appAttachMessageAddr);
+
+ appAttachMessageAddr.add(0x00).writePointer(fakeVtable);
+ appAttachMessageAddr.add(0x08).writeU32(taskIdGlobal);
+ appAttachMessageAddr.add(0x0c).writeU32(0x6e);
+ appAttachMessageAddr.add(0x10).writeU64(0x3);
+ appAttachMessageAddr.add(0x18).writePointer(appAttachCgiAddr);
+ appAttachMessageAddr.add(0x20).writeU64(0x25);
+ appAttachMessageAddr.add(0x28).writeU64(uint64("0x8000000000000030"));
+ appAttachMessageAddr.add(0x30).writeU64(uint64("0x0000000001010100"));
+}
+
+function triggerUploadAppAttach(taskId, sender, receiver, protoHex, payloadHex) {
+ return triggerSendMediaMessage(taskId, sender, receiver, protoHex, payloadHex, "appattach");
+}
+
// -------------------------发送文件消息分区-------------------------
@@ -502,6 +539,8 @@ function attachBlrX8Hook() {
protoHex = fileProtoHexGlobal;
} else if (sendMsgType === "fileupload") {
protoHex = fileUploadProtoHexGlobal;
+ } else if (sendMsgType === "appattach") {
+ protoHex = appAttachProtoHexGlobal;
} else if (sendMsgType === "voice") {
protoHex = voiceProtoHexGlobal;
}
@@ -587,6 +626,10 @@ function attachReq2buf() {
insertMsgAddr.writePointer(sendFileUploadMessageAddr);
console.log("[+] 发送fileUploadMsg成功! Req2Buf 已将 X24+0x60 指向新地址: " + sendFileUploadMessageAddr +
"[+] Req2Buf 写入后内存预览: " + insertMsgAddr);
+ } else if (sendMsgType === "appattach") {
+ insertMsgAddr.writePointer(sendAppAttachMessageAddr);
+ console.log("[+] 发送uploadAppAttach成功! Req2Buf 已将 X24+0x60 指向新地址: " + sendAppAttachMessageAddr +
+ "[+] Req2Buf 写入后内存预览: " + insertMsgAddr);
}
}
});
@@ -723,6 +766,7 @@ function triggerSendMediaMessage(taskId, sender, receiver, protoHex, payloadHex,
"voice": { messageAddr: voiceMessageAddr, sendMessageAddr: sendVoiceMessageAddr, cgiAddr: voiceCgiAddr, protoHexSetter: function(h) { voiceProtoHexGlobal = h; } },
"file": { messageAddr: fileMessageAddr, sendMessageAddr: sendFileMessageAddr, cgiAddr: fileCgiAddr, protoHexSetter: function(h) { fileProtoHexGlobal = h; } },
"fileupload": { messageAddr: fileUploadMessageAddr, sendMessageAddr: sendFileUploadMessageAddr, cgiAddr: fileUploadCgiAddr, protoHexSetter: function(h) { fileUploadProtoHexGlobal = h; } },
+ "appattach": { messageAddr: appAttachMessageAddr, sendMessageAddr: sendAppAttachMessageAddr, cgiAddr: appAttachCgiAddr, protoHexSetter: function(h) { appAttachProtoHexGlobal = h; } },
};
var info = msgAddrInfo[msgType];
@@ -998,6 +1042,7 @@ rpc.exports = {
triggerSendFileMessage: triggerSendFileMessage,
triggerSendFileUploadMessage: triggerSendFileUploadMessage,
triggerUploadFile: triggerUploadFile,
+ triggerUploadAppAttach: triggerUploadAppAttach,
};
// -------------------------发送图片消息分区-------------------------
diff --git a/onebot/text_builder.go b/onebot/text_builder.go
index 5dc1500..5043ecf 100644
--- a/onebot/text_builder.go
+++ b/onebot/text_builder.go
@@ -141,6 +141,11 @@ func BuildSendPayload(taskId int64, msgType string) string {
payloadData[16] = 0x10
payloadData[28] = 0x27
payloadData[92] = 0x6E
+ case "appattach":
+ payloadData[0] = 0x6E
+ payloadData[16] = 0x10
+ payloadData[28] = 0x25
+ payloadData[92] = 0x6E
case "voice":
payloadData[0] = 0x6E
payloadData[16] = 0x10
diff --git a/onebot/upload_app_builder.go b/onebot/upload_app_builder.go
new file mode 100644
index 0000000..16a744f
--- /dev/null
+++ b/onebot/upload_app_builder.go
@@ -0,0 +1,194 @@
+package main
+
+import (
+ "crypto/md5"
+ "encoding/hex"
+ "fmt"
+ "os"
+ "time"
+
+ "google.golang.org/protobuf/proto"
+
+ wxproto "github.com/yincongcyincong/weixin-macos/onebot/proto"
+)
+
+// uploadAppAttachChunkSize 每个分片大小 (参考 we-chat-ipad860 的 50000 字节)
+const uploadAppAttachChunkSize = 50000
+
+// BuildUploadAppAttachChunks 读取文件并按分片构造 uploadappattach 请求的 protobuf hex 列表。
+// 返回: 每个分片的 protobuf hex、填充好基础信息的 FileInfo、错误。
+// 参考 we-chat-ipad860 models/Tools/UploadApp.go 的 SendUploadAppAttach。
+func BuildUploadAppAttachChunks(receiver, filePath string) ([]string, *FileInfo, error) {
+ fileData, err := os.ReadFile(filePath)
+ if err != nil {
+ return nil, nil, fmt.Errorf("读取文件失败: %w", err)
+ }
+
+ total := len(fileData)
+ if total == 0 {
+ return nil, nil, fmt.Errorf("文件为空: %s", filePath)
+ }
+
+ sum := md5.Sum(fileData)
+ fileMd5 := hex.EncodeToString(sum[:])
+
+ // clientAppDataId: {receiver}_{ts}_UploadFile (对齐 iPad860)
+ clientAppDataId := fmt.Sprintf("%s_%d_UploadFile", receiver, time.Now().Unix())
+
+ fileInfo := &FileInfo{
+ FileName: extractFileName(filePath),
+ TotalLen: int64(total),
+ FileExt: getFileExt(filePath),
+ Md5: fileMd5,
+ ClientMsgId: clientAppDataId,
+ }
+
+ var hexChunks []string
+ for startPos := 0; startPos < total; startPos += uploadAppAttachChunkSize {
+ end := startPos + uploadAppAttachChunkSize
+ if end > total {
+ end = total
+ }
+ chunk := fileData[startPos:end]
+
+ hexStr, err := buildUploadAppAttachChunk(receiver, clientAppDataId, fileMd5,
+ uint32(total), uint32(startPos), chunk)
+ if err != nil {
+ return nil, nil, err
+ }
+ hexChunks = append(hexChunks, hexStr)
+ }
+
+ return hexChunks, fileInfo, nil
+}
+
+// buildUploadAppAttachChunk 构造单个分片的 UploadAppAttachRequest protobuf hex。
+func buildUploadAppAttachChunk(receiver, clientAppDataId, fileMd5 string,
+ totalLen, startPos uint32, chunk []byte) (string, error) {
+
+ version := NextVersion()
+ appId := ""
+ sdkVersion := uint32(0)
+ dataLen := uint32(len(chunk))
+ fileType := uint32(6) // 6 = 文件
+
+ req := &wxproto.UploadAppAttachRequest{
+ BaseRequest: &wxproto.ReplyMsgHeader{
+ Flag: []byte{0x00},
+ SessionId: &globalSessionId,
+ ClientProof: globalClientProof,
+ DeviceId: &globalDeviceId,
+ Platform: proto.String("UnifiedPCMac 26 arm64"),
+ Version: &version,
+ },
+ AppId: &appId,
+ SdkVersion: &sdkVersion,
+ ClientAppDataId: &clientAppDataId,
+ UserName: &receiver,
+ TotalLen: &totalLen,
+ StartPos: &startPos,
+ DataLen: &dataLen,
+ Data: &wxproto.SKBuiltinBufferT{
+ ILen: &dataLen,
+ Buffer: chunk,
+ },
+ Type: &fileType,
+ Md5: &fileMd5,
+ }
+
+ data, err := proto.Marshal(req)
+ if err != nil {
+ return "", fmt.Errorf("marshal uploadappattach chunk failed: %w", err)
+ }
+
+ //fmt.Printf("[uploadappattach-proto] startPos=%d dataLen=%d totalLen=%d len=%d\n%s\n",
+ // startPos, dataLen, totalLen, len(data), HexDump(data, 0))
+
+ return hex.EncodeToString(data), nil
+}
+
+// ParseUploadAppAttachResponse 解析 uploadappattach 响应,提取 mediaId(attachid)。
+func ParseUploadAppAttachResponse(data []byte) (string, error) {
+ // fmt.Printf("[uploadappattach-resp] len=%d\n%s\n", len(data), HexDump(data, 0))
+
+ resp := &wxproto.UploadAppAttachResponse{}
+ if err := proto.Unmarshal(data, resp); err != nil {
+ return "", fmt.Errorf("unmarshal uploadappattach response failed: %w", err)
+ }
+
+ if resp.BaseResponse != nil && resp.BaseResponse.Ret != nil && *resp.BaseResponse.Ret != 0 {
+ errMsg := ""
+ if resp.BaseResponse.ErrMsg != nil {
+ errMsg = resp.BaseResponse.ErrMsg.GetMsg()
+ }
+ return "", fmt.Errorf("uploadappattach failed, ret=%d, errMsg=%s",
+ *resp.BaseResponse.Ret, errMsg)
+ }
+
+ return resp.GetMediaId(), nil
+}
+
+// BuildSimpleFileMsgProto 构建发送文件消息的 protobuf(uploadappattach 直传后用)。
+// 严格按 wechat7016 WXSendMsgFile / SendAppMsgRequest 格式:
+// - 顶层 SendAppMsgReq{baseRequest, msg=AppMsgBody{...}}
+// - AppMsgBody 只填 fromUserName/toUserName/type=6/content(XML)/clientMsgId/createTime
+// - appmsg XML 极简:只有 title/type/appattach(totallen/attachid/fileext),不含 cdnattachurl
+func BuildSimpleFileMsgProto(sender, receiver string, fileInfo *FileInfo) (string, error) {
+ now := time.Now().Unix()
+
+ // wechat7016 WXSendMsgFile 的极简 XML(attachid 用完整 mediaId,不加 cdnattachurl)
+ xml := `` + "\n"
+ xml += ``
+ xml += `` + escapeXmlStr(fileInfo.FileName) + ``
+ xml += ``
+ xml += ``
+ xml += `6`
+ xml += ``
+ xml += ``
+ xml += ``
+ xml += ``
+ xml += `` + fmt.Sprintf("%d", fileInfo.TotalLen) + ``
+ xml += `` + escapeXmlStr(fileInfo.AttachId) + ``
+ xml += `` + escapeXmlStr(fileInfo.FileExt) + ``
+ xml += ``
+ xml += ``
+ xml += ``
+
+ version := NextVersion()
+ appId := ""
+ sdkVersion := uint32(0)
+ msgType := uint32(6) // 6 = 文件
+ clientMsgId := fmt.Sprintf("%d", now)
+ msgSource := "12"
+
+ req := &wxproto.SendAppMsgReq{
+ BaseRequest: &wxproto.ReplyMsgHeader{
+ Flag: []byte{0x00},
+ SessionId: &globalSessionId,
+ ClientProof: globalClientProof,
+ DeviceId: &globalDeviceId,
+ Platform: proto.String("UnifiedPCMac 26 arm64"),
+ Version: &version,
+ },
+ Msg: &wxproto.AppMsgBody{
+ FromUserName: &sender,
+ AppId: &appId,
+ SdkVersion: &sdkVersion,
+ ToUserName: &receiver,
+ Type: &msgType,
+ Content: &xml,
+ CreateTime: proto.Int64(now),
+ ClientMsgId: &clientMsgId,
+ MsgSource: &msgSource,
+ },
+ }
+
+ data, err := proto.Marshal(req)
+ if err != nil {
+ return "", fmt.Errorf("marshal SendAppMsgReq failed: %w", err)
+ }
+
+ // fmt.Printf("[simple-file-proto] xml=%s\n[simple-file-proto] hex dump:\n%s\n", xml, HexDump(data, 0))
+
+ return hex.EncodeToString(data), nil
+}
diff --git a/onebot/upload_builder.go b/onebot/upload_builder.go
index 7aae39d..7f373b1 100644
--- a/onebot/upload_builder.go
+++ b/onebot/upload_builder.go
@@ -119,3 +119,117 @@ func BuildUploadPayload(uploadType string) string {
return hex.EncodeToString(payload)
}
+
+// BuildVoiceUploadPayload 构建语音上传的payload模板,返回hex编码字符串
+// 基于WeChat实际内存dump校正
+// JS侧会在固定偏移写入运行时指针: 0x00, 0x08, 0x48(voiceIdAddr), 0x50(voiceId长度), 0x58(voiceId容量), 0x68(receiver), 0x100(audioData), 0x108(audioLen), 0x110(audioCap)
+func BuildVoiceUploadPayload() string {
+ payload := []byte{
+ // 0x00 (被uploadFunc1Addr覆盖)
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ // 0x08 (被uploadFunc2Addr覆盖)
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ // 0x10
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ // 0x18
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ // 0x20
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ // 0x28
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ // 0x30
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ // 0x38
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ // 0x40
+ 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00,
+ // 0x48 (被voiceIdAddr覆盖)
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ // 0x50 voiceId length (JS实时写入)
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ // 0x58 voiceId capacity (JS实时写入)
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80,
+ // 0x60
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ // 0x68 (被receiver覆盖, inline string区域)
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ // 0x70
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ // 0x78 [0x7F]=receiver length
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x13,
+ // 0x80
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ // 0x88
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ // 0x90
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ // 0x98 [0x9C]=0x0F voice type
+ 0x01, 0xAA, 0xAA, 0xAA, 0x0F, 0x00, 0x00, 0x00,
+ // 0xA0
+ 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00,
+ // 0xA8
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ // 0xB0
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ // 0xB8
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ // 0xC0
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ // 0xC8
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ // 0xD0
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ // 0xD8 音频参数
+ 0x10, 0x0E, 0x00, 0x00, 0x08, 0x07, 0x00, 0x00,
+ // 0xE0 文件大小(-1=未知)
+ 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
+ // 0xE8
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ // 0xF0
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ // 0xF8
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ // 0x100 (被voiceAudioDataAddr覆盖 - 音频数据指针)
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ // 0x108 音频数据长度 (JS写入)
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ // 0x110 音频数据容量 (JS写入, 带high bit)
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80,
+ // 0x118
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ // 0x120
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ // 0x128
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ // 0x130
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ // 0x138
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ // 0x140
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ // 0x148
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ // 0x150
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ // 0x158
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ // 0x160
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ // 0x168
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ // 0x170 [0x174]=0x01
+ 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00,
+ // 0x178
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ // 0x180
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ // 0x188
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ // 0x190
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ // 0x198
+ 0x00, 0xAA, 0xAA, 0xAA, 0x01, 0x00, 0x00, 0x00,
+ }
+
+ return hex.EncodeToString(payload)
+}
diff --git a/onebot/utils.go b/onebot/utils.go
index c30b129..b6e6414 100644
--- a/onebot/utils.go
+++ b/onebot/utils.go
@@ -61,6 +61,199 @@ func SaveBase64Image(base64Data string) (string, string, error) {
return targetPath, md5Str, nil
}
+// SaveVoiceFile 解码base64音频数据并保存为文件(不追加salt,保持二进制完整性)
+// 返回原始字节、文件路径、错误
+func SaveVoiceFile(base64Data string) ([]byte, string, error) {
+ rawContents := base64Data
+ if strings.HasPrefix(base64Data, "base64://") {
+ rawContents = strings.TrimPrefix(base64Data, "base64://")
+ } else if idx := strings.Index(base64Data, ","); idx != -1 {
+ rawContents = base64Data[idx+1:]
+ }
+
+ data, err := base64.StdEncoding.DecodeString(rawContents)
+ if err != nil {
+ return nil, "", fmt.Errorf("base64 decode failed: %v", err)
+ }
+
+ r := rand.New(rand.NewSource(time.Now().UnixNano()))
+ randomNumber := r.Intn(1000)
+ timestamp := time.Now().Unix()
+ ext := DetectFileFormat(data)
+ fileName := fmt.Sprintf("%d_%d.%s", randomNumber, timestamp, ext)
+ targetPath := config.ImagePath + fileName
+ dir := filepath.Dir(targetPath)
+ if err := os.MkdirAll(dir, 0755); err != nil {
+ return nil, "", fmt.Errorf("create directory failed: %v", err)
+ }
+
+ err = os.WriteFile(targetPath, data, 0666)
+ if err != nil {
+ return nil, "", fmt.Errorf("write file failed: %v", err)
+ }
+ os.Chmod(targetPath, 0666)
+
+ return data, targetPath, nil
+}
+
+// SaveBase64File 解码 base64 数据并以指定扩展名保存文件,返回文件路径和 MD5
+func SaveBase64File(base64Data string, ext string) (string, string, error) {
+ rawContents := base64Data
+ if strings.HasPrefix(base64Data, "base64://") {
+ rawContents = strings.TrimPrefix(base64Data, "base64://")
+ } else if idx := strings.Index(base64Data, ","); idx != -1 {
+ rawContents = base64Data[idx+1:]
+ }
+
+ data, err := base64.StdEncoding.DecodeString(rawContents)
+ if err != nil {
+ return "", "", fmt.Errorf("base64 decode failed: %v", err)
+ }
+
+ // 如果没有传入扩展名,尝试自动检测
+ if ext == "" {
+ ext = DetectFileFormat(data)
+ if ext == "unknown" {
+ // fallback: 用 MIME 类型推断
+ mimeType := http.DetectContentType(data)
+ ext = mimeToExt(mimeType)
+ }
+ }
+
+ r := rand.New(rand.NewSource(time.Now().UnixNano()))
+ randomNumber := r.Intn(1000)
+ timestamp := time.Now().Unix()
+ fileName := fmt.Sprintf("%d_%d.%s", randomNumber, timestamp, ext)
+ targetPath := config.ImagePath + fileName
+ dir := filepath.Dir(targetPath)
+ if err := os.MkdirAll(dir, 0755); err != nil {
+ return "", "", fmt.Errorf("create directory failed: %v", err)
+ }
+
+ err = os.WriteFile(targetPath, data, 0666)
+ if err != nil {
+ return "", "", fmt.Errorf("write file failed: %v", err)
+ }
+ os.Chmod(targetPath, 0666)
+
+ md5Str, err := GetFileMD5(targetPath)
+ if err != nil {
+ return "", "", fmt.Errorf("get file md5 failed: %v", err)
+ }
+
+ return targetPath, md5Str, nil
+}
+
+// mimeToExt 将 MIME 类型转换为文件扩展名
+func mimeToExt(mimeType string) string {
+ if idx := strings.Index(mimeType, ";"); idx != -1 {
+ mimeType = strings.TrimSpace(mimeType[:idx])
+ }
+ switch mimeType {
+ case "text/plain":
+ return "txt"
+ case "text/html":
+ return "html"
+ case "text/xml", "application/xml":
+ return "xml"
+ case "application/json":
+ return "json"
+ case "application/pdf":
+ return "pdf"
+ case "application/zip":
+ return "zip"
+ case "application/gzip":
+ return "gz"
+ case "image/jpeg":
+ return "jpg"
+ case "image/png":
+ return "png"
+ case "image/gif":
+ return "gif"
+ case "image/webp":
+ return "webp"
+ case "video/mp4":
+ return "mp4"
+ case "audio/mpeg":
+ return "mp3"
+ default:
+ return "bin"
+ }
+}
+
+// ConvertToSilk 将音频数据(任意格式)通过ffmpeg转为PCM,再编码为SILK格式
+// 微信要求格式: \x02#!SILK_V3 开头 (tencent silk)
+// 如果输入已经是该格式,则直接返回
+// 返回: silkData, 时长(毫秒), error
+func ConvertToSilk(audioData []byte) ([]byte, int32, error) {
+ // 已经是tencent SILK格式 (\x02#!SILK_V3),直接返回,时长未知设为0
+ if len(audioData) > 10 && audioData[0] == 0x02 && bytes.HasPrefix(audioData[1:], []byte("#!SILK_V3")) {
+ return audioData, 0, nil
+ }
+
+ // 先用ffmpeg将输入音频转为PCM (s16le, 16000Hz, mono)
+ cmd := exec.Command("ffmpeg",
+ "-i", "pipe:0",
+ "-f", "s16le",
+ "-ar", "16000",
+ "-ac", "1",
+ "pipe:1",
+ )
+ cmd.Stdin = bytes.NewReader(audioData)
+
+ var out bytes.Buffer
+ cmd.Stdout = &out
+ var stderr bytes.Buffer
+ cmd.Stderr = &stderr
+
+ if err := cmd.Run(); err != nil {
+ return nil, 0, fmt.Errorf("ffmpeg to pcm error: %v, details: %s", err, stderr.String())
+ }
+
+ pcmBytes := out.Bytes()
+ // 时长(ms) = pcm字节数 * 1000 / (采样率 * 通道数 * 每样本字节数)
+ durationMs := int32(int64(len(pcmBytes)) * 1000 / (16000 * 2))
+
+ // 尝试使用外部silk-encoder(和微信兼容性更好)
+ silkData, err := encodeSilkExternal(pcmBytes)
+ if err != nil {
+ // fallback: 使用go-silk库
+ silkData, err = silk.EncodePcmBuffToSilk(pcmBytes, 16000, 16000, true)
+ if err != nil {
+ return nil, 0, fmt.Errorf("encode silk error: %v", err)
+ }
+ }
+
+ return silkData, durationMs, nil
+}
+
+// encodeSilkExternal 使用外部pilk(Python)工具编码pcm->silk(和微信兼容)
+func encodeSilkExternal(pcmBytes []byte) ([]byte, error) {
+ tmpPcm, err := os.CreateTemp("", "voice_*.pcm")
+ if err != nil {
+ return nil, err
+ }
+ defer os.Remove(tmpPcm.Name())
+
+ if _, err := tmpPcm.Write(pcmBytes); err != nil {
+ tmpPcm.Close()
+ return nil, err
+ }
+ tmpPcm.Close()
+
+ tmpSilk := tmpPcm.Name() + ".silk"
+ defer os.Remove(tmpSilk)
+
+ pyScript := fmt.Sprintf(`import pilk; pilk.encode("%s", "%s", pcm_rate=16000, tencent=True)`, tmpPcm.Name(), tmpSilk)
+ cmd := exec.Command("python3", "-c", pyScript)
+ var stderr bytes.Buffer
+ cmd.Stderr = &stderr
+ if err := cmd.Run(); err != nil {
+ return nil, fmt.Errorf("pilk encode failed: %v, %s", err, stderr.String())
+ }
+ return os.ReadFile(tmpSilk)
+}
+
// GetVideoDuration 使用ffprobe获取视频时长(秒)
func GetVideoDuration(filePath string) (int32, error) {
cmd := exec.Command("ffprobe",
diff --git a/onebot/voice_builder.go b/onebot/voice_builder.go
new file mode 100644
index 0000000..184d669
--- /dev/null
+++ b/onebot/voice_builder.go
@@ -0,0 +1,67 @@
+package main
+
+import (
+ "encoding/hex"
+ "fmt"
+ "time"
+
+ "github.com/google/uuid"
+ wxproto "github.com/yincongcyincong/weixin-macos/onebot/proto"
+ "google.golang.org/protobuf/proto"
+)
+
+// BuildVoiceMsgProto 构建发送语音消息的protobuf并返回hex编码的字符串
+func BuildVoiceMsgProto(sender, targetId, cdnKey, aesKey string, voiceDuration int32, silkDataLen int32, unknown13 int32) (string, error) {
+ // client_msg_id: UUID格式
+ clientMsgId := uuid.New().String()
+
+ msg := &wxproto.WxSendVoiceMsg{
+ FromUser: sender,
+ ToUser: targetId,
+ Unknown3: 0,
+ Unknown4: silkDataLen,
+ ClientMsgId: clientMsgId,
+ Duration: voiceDuration,
+ Unknown9: 1,
+ Header: &wxproto.VoiceMsgHeader{
+ Flag: []byte{0x00},
+ SessionId: int64(globalSessionId),
+ ClientProof: globalClientProof,
+ DeviceId: int64(globalDeviceId),
+ Platform: []byte("UnifiedPCMac 26 arm64"),
+ Version: int32(NextVersion()),
+ Unknown8: 4,
+ Unknown15: 0,
+ },
+ Unknown11: 0,
+ Unknown13: 4,
+ Unknown15: 0,
+ Unknown16: 0,
+ Unknown17: time.Now().Unix(),
+ CdnKey: []byte(cdnKey),
+ AesKey: []byte(aesKey),
+ Unknown24: []byte{},
+ Unknown25: 0,
+ }
+
+ data, err := proto.Marshal(msg)
+ if err != nil {
+ return "", fmt.Errorf("marshal voice proto failed: %w", err)
+ }
+
+ // 手动追加proto3不会序列化的0值字段
+ // field 3 (unknown3=0)
+ data = appendZeroVarintField(data, 3)
+ // field 11 (unknown11=0)
+ data = appendZeroVarintField(data, 11)
+ // field 15 (unknown15=0)
+ data = appendZeroVarintField(data, 15)
+ // field 16 (unknown16=0)
+ data = appendZeroVarintField(data, 16)
+ // field 25 (unknown25=0)
+ data = appendZeroVarintField(data, 25)
+
+ //fmt.Printf("[voice-proto] final protobuf hex dump:\n%s\n", HexDump(data, 0))
+
+ return hex.EncodeToString(data), nil
+}
diff --git a/onebot/worker.go b/onebot/worker.go
index 65b6b95..6a2a388 100644
--- a/onebot/worker.go
+++ b/onebot/worker.go
@@ -195,6 +195,126 @@ func SendWechatMsg(m *SendMsg) {
sendErr = errors.New("send reply failed")
return
}
+ case "voice":
+ // 直接base64解码,不追加salt(音频二进制不能被修改)
+ rawAudio, targetPath, err := SaveVoiceFile(m.Content)
+ if err != nil {
+ Error("保存语音文件失败", "err", err)
+ sendErr = err
+ return
+ }
+
+ // 转换为SILK格式
+ silkData, voiceDurationMs, err := ConvertToSilk(rawAudio)
+ if err != nil {
+ Error("转换SILK格式失败", "err", err)
+ sendErr = err
+ return
+ }
+
+ audioHex := hex.EncodeToString(silkData)
+
+ uploadPayloadHex := BuildVoiceUploadPayload()
+ result := fridaScript.ExportsCall("triggerUploadVoice", targetId, targetPath, uploadPayloadHex, audioHex, voiceDurationMs)
+ Info("📩 上传语音任务执行结果", "result", result, "target_id", targetId, "path", targetPath, "silk_len", len(silkData), "duration_ms", voiceDurationMs)
+ if result != "0" {
+ Error("上传语音失败", "target_id", targetId, "result", result)
+ sendErr = errors.New("upload voice failed")
+ return
+ }
+ if m.ResultChan != nil {
+ pendingResultMap.Store(targetId, m.ResultChan)
+ m.ResultChan = nil
+ }
+ return
+ case "send_voice":
+ protoHex, err := BuildVoiceMsgProto(myWechatId, targetId, m.CdnKey, m.AesKey, m.VoiceDuration, m.SilkDataLen, m.Unknown13)
+ if err != nil {
+ Error("构建语音protobuf失败", "err", err)
+ sendErr = err
+ return
+ }
+ payloadHex := BuildSendPayload(currTaskId, "voice")
+ result := fridaScript.ExportsCall("triggerSendVoiceMessage", currTaskId, myWechatId, targetId, protoHex, payloadHex)
+ Info("📩 发送语音任务执行结果", "result", result, "task_id", currTaskId, "wechat_id", myWechatId, "target_id", targetId, "unknown13", m.Unknown13)
+ if result != "1" {
+ Error("发送语音失败", "task_id", currTaskId, "target_id", targetId, "result", result)
+ sendErr = errors.New("send voice failed")
+ return
+ }
+ case "send_file_simple":
+ // iPad860 风格: uploadappattach 分片直传 → sendappmsg,不走 CDN。
+ // 文件名/扩展名由内容自动识别 + 时间戳随机生成(SaveBase64File 内部完成)。
+ targetPath, _, err := SaveBase64File(m.Content, "")
+ if err != nil {
+ Error("保存文件失败", "err", err)
+ sendErr = err
+ return
+ }
+
+ chunks, fileInfo, err := BuildUploadAppAttachChunks(targetId, targetPath)
+ if err != nil {
+ Error("构建uploadappattach分片失败", "err", err)
+ sendErr = err
+ return
+ }
+ Info("📩 开始uploadappattach直传", "target_id", targetId, "chunks", len(chunks),
+ "file_name", fileInfo.FileName, "file_ext", fileInfo.FileExt,
+ "total_len", fileInfo.TotalLen, "md5", fileInfo.Md5)
+
+ var attachId string
+ for i, chunkHex := range chunks {
+ chunkTaskId := atomic.AddInt64(&taskId, 1)
+ payloadHex := BuildSendPayload(chunkTaskId, "appattach")
+ result := fridaScript.ExportsCall("triggerUploadAppAttach", chunkTaskId, myWechatId, targetId, chunkHex, payloadHex)
+ if result != "1" {
+ Error("uploadappattach分片发送失败", "chunk", i, "result", result)
+ sendErr = errors.New("upload app attach chunk failed")
+ return
+ }
+
+ select {
+ case <-ctx.Done():
+ Error("等待uploadappattach响应超时", "chunk", i)
+ sendErr = errors.New("upload app attach timeout")
+ return
+ case data := <-appAttachRespChan:
+ id, perr := ParseUploadAppAttachResponse(data)
+ if perr != nil {
+ Error("解析uploadappattach响应失败", "chunk", i, "err", perr)
+ sendErr = perr
+ return
+ }
+ if id != "" {
+ attachId = id
+ }
+ Info("📩 uploadappattach分片完成", "chunk", i, "attach_id", id)
+ }
+ }
+
+ if attachId == "" {
+ Error("uploadappattach未返回attachId", "target_id", targetId)
+ sendErr = errors.New("upload app attach no attachId")
+ return
+ }
+ fileInfo.AttachId = attachId
+
+ // sendappmsg (type=6),精简版 appmsg,cdnattachurl 也填 attachId
+ currTaskId = atomic.AddInt64(&taskId, 1)
+ protoHex, err := BuildSimpleFileMsgProto(myWechatId, targetId, fileInfo)
+ if err != nil {
+ Error("构建文件protobuf失败", "err", err)
+ sendErr = err
+ return
+ }
+ payloadHex := BuildSendPayload(currTaskId, "file")
+ result := fridaScript.ExportsCall("triggerSendFileMessage", currTaskId, myWechatId, targetId, protoHex, payloadHex)
+ Info("📩 发送文件消息(simple)执行结果", "result", result, "task_id", currTaskId, "target_id", targetId)
+ if result != "1" {
+ Error("发送文件失败(simple)", "task_id", currTaskId, "target_id", targetId, "result", result)
+ sendErr = errors.New("send file failed")
+ return
+ }
}
select {
@@ -383,6 +503,13 @@ func HandleBuf2Resp(msgType string, data []byte) {
return
}
+ switch msgType {
+ case "appattach":
+ Info("收到uploadappattach响应", "data_len", len(data))
+ appAttachRespChan <- data
+ return
+ }
+
ret, errMsg, err := ParseSendMsgResponse(data)
if err != nil {
Info("buf2resp响应无法提取错误码,视为成功", "msg_type", msgType, "err", err)