package main import ( "context" "crypto/aes" "encoding/hex" "encoding/json" "encoding/xml" "errors" "fmt" "os" "runtime/debug" "sync/atomic" "time" ) func SendWorker() { defer func() { if err := recover(); err != nil { Error("SendWorker panic", "err", err, "stack", string(debug.Stack())) go SendWorker() } }() for { select { case m, ok := <-msgChan: if !ok { Fatal("发送通道关闭") return } SendWechatMsg(m) } } } func SendWechatMsg(m *SendMsg) { var sendErr error defer func() { if m.ResultChan != nil { m.ResultChan <- sendErr } }() time.Sleep(time.Duration(config.SendInterval) * time.Millisecond) currTaskId := atomic.AddInt64(&taskId, 1) Info("📩 收到任务", "task_id", currTaskId, "type", m.Type) ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second) defer cancel() targetId := m.UserId if m.GroupID != "" { targetId = m.GroupID } if targetId == "" { Error("目标为空", "task_id", currTaskId, "target_id", targetId) sendErr = errors.New("target is empty") return } switch m.Type { case "text": protoHex, err := BuildTextMsgProto(targetId, m.Content, m.AtUser) if err != nil { Error("构建文本protobuf失败", "err", err) sendErr = err return } payloadHex := BuildSendPayload(currTaskId, "text") result := fridaScript.ExportsCall("triggerSendTextMessage", currTaskId, targetId, m.Content, m.AtUser, protoHex, payloadHex) Info("📩 发送文本任务执行结果", "result", result, "task_id", currTaskId, "target_id", targetId, "at_user", m.AtUser) if result != "1" { Error("发送文本失败", "task_id", currTaskId, "target_id", targetId, "result", result) sendErr = errors.New("send text failed") return } case "image": targetPath, md5Str, err := SaveBase64Image(m.Content) if err != nil { Error("保存图片失败", "err", err) sendErr = err return } uploadPayloadHex := BuildUploadPayload("img") result := fridaScript.ExportsCall("triggerUploadImg", targetId, md5Str, targetPath, uploadPayloadHex) Info("📩 上传图片任务执行结果", "result", result, "target_id", targetId, "md5", md5Str, "path", targetPath) if result != "0" { Error("上传图片失败", "target_id", targetId, "md5", md5Str, "result", result) sendErr = errors.New("upload image failed") return } if m.ResultChan != nil { pendingResultMap.Store(targetId, m.ResultChan) m.ResultChan = nil // 不让 defer 发送结果 } return case "send_image": protoHex, err := BuildImgMsgProto(myWechatId, targetId, m.CdnKey, m.AesKey, m.Md5Key) if err != nil { Error("构建图片protobuf失败", "err", err) sendErr = err return } payloadHex := BuildSendPayload(currTaskId, "img") result := fridaScript.ExportsCall("triggerSendImgMessage", currTaskId, myWechatId, targetId, protoHex, payloadHex) Info("📩 发送图片任务执行结果", "result", result, "task_id", currTaskId, "wechat_id", myWechatId, "target_id", targetId) if result != "1" { Error("发送图片失败", "task_id", currTaskId, "target_id", targetId, "result", result) sendErr = errors.New("send image failed") return } case "video": targetPath, md5Str, err := SaveBase64Image(m.Content) if err != nil { Error("保存图片失败", "err", err) sendErr = err return } // 获取视频时长和文件大小 info := &VideoInfo{} duration, err := GetVideoDuration(targetPath) if err != nil { Error("获取视频时长失败", "err", err) } else { info.Duration = duration } if fi, err := os.Stat(targetPath); err == nil { info.VideoSize = int32(fi.Size()) } videoInfoMap.Store(targetId, info) uploadPayloadHex := BuildUploadPayload("video") result := fridaScript.ExportsCall("triggerUploadVideo", targetId, md5Str, targetPath, uploadPayloadHex) Info("📩 上传视频任务执行结果", "result", result, "target_id", targetId, "md5", md5Str, "path", targetPath, "duration", info.Duration, "size", info.VideoSize) if result != "0" { Error("上传视频失败", "target_id", targetId, "md5", md5Str, "result", result) sendErr = errors.New("upload video failed") return } if m.ResultChan != nil { pendingResultMap.Store(targetId, m.ResultChan) m.ResultChan = nil } return case "send_video": var duration, videoSize int32 if info, ok := videoInfoMap.LoadAndDelete(targetId); ok { vi := info.(*VideoInfo) duration = vi.Duration videoSize = vi.VideoSize } protoHex, err := BuildVideoMsgProto(myWechatId, targetId, m.CdnKey, m.AesKey, m.Md5Key, m.VideoId, duration, videoSize) if err != nil { Error("构建视频protobuf失败", "err", err) sendErr = err return } payloadHex := BuildSendPayload(currTaskId, "video") result := fridaScript.ExportsCall("triggerSendVideoMessage", currTaskId, myWechatId, targetId, protoHex, payloadHex) Info("📩 发送视频任务执行结果", "result", result, "task_id", currTaskId, "wechat_id", myWechatId, "target_id", targetId, "duration", duration, "size", videoSize) if result != "1" { Error("发送视频失败", "task_id", currTaskId, "target_id", targetId, "result", result) sendErr = errors.New("send video failed") return } case "download": result := fridaScript.ExportsCall("triggerDownload", targetId, m.FIleCdnUrl, m.AesKey, m.FilePath, m.FileType) Info("📩 下载任务执行结果", "result", result, "task_id", currTaskId, "wechat_id", myWechatId, "target_id", targetId) case "reply": replyInfo := &ReplyInfo{ Content: m.Content, MsgId: m.ReferMsgId, MsgSender: m.ReferMsgSender, MsgType: m.ReferMsgType, CreateTime: m.ReferCreateTime, Msgsource: m.ReferMsgsource, DisplayName: m.ReferDisplayName, MsgContent: m.ReferContent, } protoHex, err := BuildReplyMsgProto(myWechatId, targetId, replyInfo) if err != nil { Error("构建回复protobuf失败", "err", err) sendErr = err return } payloadHex := BuildSendPayload(currTaskId, "reply") result := fridaScript.ExportsCall("triggerSendReplyMessage", currTaskId, myWechatId, targetId, protoHex, payloadHex) Info("📩 发送回复任务执行结果", "result", result, "task_id", currTaskId, "wechat_id", myWechatId, "target_id", targetId) if result != "1" { Error("发送回复失败", "task_id", currTaskId, "target_id", targetId, "result", result) sendErr = errors.New("send reply failed") return } } select { case <-ctx.Done(): Error("任务执行超时!", "taskId", currTaskId) sendErr = errors.New("send timeout") case resp := <-buf2RespChan: if resp.Err != nil { Error("收到buf2resp失败信号", "taskId", currTaskId, "msg_type", resp.MsgType, "err", resp.Err) sendErr = resp.Err return } Info("收到buf2resp完成信号,任务完成", "taskId", currTaskId, "msg_type", resp.MsgType, "data_len", len(resp.Data)) } } func HandleMsg(jsonData []byte) ([]byte, error) { m := new(WechatMessage) err := json.Unmarshal(jsonData, m) if err != nil { Error("解析消息失败", "err", err) return nil, err } if myWechatId == "" && m.SelfID != "" { myWechatId = m.SelfID } if m.GroupId != "" { userID2NicknameMap.Store(m.GroupId+"_"+m.UserID, m.Sender.Nickname) } for _, msg := range m.Message { switch msg.Type { case "record": path, err := SaveAudioFile(msg.Data.Media) if err != nil { Error("保存音频失败", "err", err) return nil, err } msg.Data.URL = "file://" + path msg.Data.Media = nil case "image": var fileMsg FileMsg err = xml.Unmarshal([]byte(msg.Data.Text), &fileMsg) if err != nil { Error("XML解析失败", "err", err) return nil, err } path, err := GetDownloadPath(fileMsg.Image.MidImgURL, fileMsg.Image.AesKey) if err != nil { Error("获取文件路径失败", "err", err) return nil, err } msg.Data.URL = "file://" + path case "file": var fileMsg FileMsg err = xml.Unmarshal([]byte(msg.Data.Text), &fileMsg) if err != nil { Error("XML解析失败", "err", err) return nil, err } path, err := GetDownloadPath(fileMsg.AppMsg.AppAttach.CdnAttachURL, fileMsg.AppMsg.AppAttach.AesKey) if err != nil { Error("获取文件路径失败", "err", err) return nil, err } msg.Data.URL = "file://" + path case "video": var fileMsg FileMsg err = xml.Unmarshal([]byte(msg.Data.Text), &fileMsg) if err != nil { Error("XML解析失败", "err", err) return nil, err } path, err := GetDownloadPath(fileMsg.Video.CdnVideoUrl, fileMsg.Video.AesKey) if err != nil { Error("获取文件路径失败", "err", err) return nil, err } msg.Data.URL = "file://" + path case "face": var fileMsg FileMsg err = xml.Unmarshal([]byte(msg.Data.Text), &fileMsg) if err != nil { Error("XML解析失败", "err", err) return nil, err } // 优先cdnurl,为空则用thumburl,再为空则用externurl emojiUrl := fileMsg.Emoji.CdnUrl if emojiUrl == "" { emojiUrl = fileMsg.Emoji.ThumbUrl } if emojiUrl == "" { emojiUrl = fileMsg.Emoji.ExternUrl } data, err := DownloadFile(emojiUrl) if err != nil { Error("下载表情失败", "err", err) return nil, err } path, err := DetectAndSaveImage(data) if err != nil { Error("保存表情失败", "err", err) return nil, err } msg.Data.URL = "file://" + path } } return json.Marshal(m) } func GetDownloadPath(cdnUrl, aesKeyStr string) (string, error) { for i := 0; i < 10; i++ { if downloadMsgInter, ok := userID2FileMsgMap.Load(cdnUrl); ok { downloadReq := downloadMsgInter.(*DownloadRequest) if downloadReq.FilePath != "" { return downloadReq.FilePath, nil } // 检查数据是否还在接收中 timeSinceLastAppend := time.Now().UnixMilli() - downloadReq.LastAppendTime Info("文件等待下载", "url", cdnUrl, "times", i, "last_append_time", timeSinceLastAppend) // 如果数据仍在接收中(1秒内有新数据),继续等待 if timeSinceLastAppend < 1000 && i < 9 { time.Sleep(2 * time.Second) continue } // 数据接收完成,尝试解密 if len(downloadReq.Media) > 0 { if len(downloadReq.Media)%aes.BlockSize != 0 { Info("文件数据仍未对齐 AES 块,继续等待", "url", cdnUrl, "times", i, "media_len", len(downloadReq.Media), "block_size", aes.BlockSize) if i < 9 { time.Sleep(2 * time.Second) continue } userID2FileMsgMap.Delete(cdnUrl) return "", fmt.Errorf("文件数据长度不是 AES 块大小倍数: media_len=%d block_size=%d", len(downloadReq.Media), aes.BlockSize) } aesKey, err := hex.DecodeString(aesKeyStr) if err != nil { Error("AES key 解码失败", "err", err) return "", err } filePath, err := GetFilePath(downloadReq.Media, aesKey) if err != nil { Error("获取文件路径失败", "err", err, "media_len", len(downloadReq.Media)) userID2FileMsgMap.Delete(cdnUrl) return "", err } downloadReq.FilePath = filePath downloadReq.Media = nil return filePath, nil } } time.Sleep(2 * time.Second) } return "", errors.New("文件下载超时或数据为空") } // HandleBuf2Resp 处理所有消息类型的buf2resp响应 func HandleBuf2Resp(msgType string, data []byte) { Info("收到buf2resp响应", "msg_type", msgType, "data_len", len(data)) if len(data) == 0 { Error("buf2resp响应数据为空", "msg_type", msgType) buf2RespChan <- &Buf2RespData{ MsgType: msgType, Data: data, Err: errors.New("response data is empty"), } return } ret, errMsg, err := ParseSendMsgResponse(data) if err != nil { Info("buf2resp响应无法提取错误码,视为成功", "msg_type", msgType, "err", err) } // 判断错误码是否为0 if ret != 0 { Error("buf2resp响应错误", "msg_type", msgType, "ret", ret, "errMsg", errMsg) buf2RespChan <- &Buf2RespData{ MsgType: msgType, Data: data, Err: fmt.Errorf("response error, ret=%d, errMsg=%s", ret, errMsg), } return } Info("buf2resp响应成功", "msg_type", msgType) buf2RespChan <- &Buf2RespData{ MsgType: msgType, Data: data, } }