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 }