package main import ( "bytes" "encoding/json" "fmt" "io" "io/ioutil" "math/rand" "mime/multipart" "net/http" "os" "os/exec" "path/filepath" "regexp" "strconv" "strings" "time" "qiweimanager/config" ) // TransformData 根据模板转换数据 // 将原始数据按照eventdata目录下对应type的模板进行转换 // 如果找不到模板,则直接返回原始数据 func TransformData(responseData map[string]interface{}, iClientId uint32) ([]byte, error) { // 获取可执行文件路径 exePath, err := os.Executable() if err != nil { globalLogger.Error("[错误] 获取可执行文件路径失败: %v", err) // 如果获取路径失败,创建一个新的对象只包含data字段,避免重复 resultData := map[string]interface{}{} if data, ok := responseData["data"]; ok { resultData["data"] = data } else { resultData = responseData } jsonBytes, err := json.Marshal(resultData) if err == nil { globalLogger.Info("[辅助程序] 已将原始数据转换为JSON格式: %s", string(jsonBytes)) } return jsonBytes, err } // 获取可执行文件目录 exeDir := filepath.Dir(exePath) // 获取type值 typeValue, ok := responseData["type"] if !ok { // 打印responseData的结构,帮助调试 responseDataStr, _ := json.MarshalIndent(responseData, "", " ") globalLogger.Debug("[错误] 数据中未找到type字段,responseData结构: %s", string(responseDataStr)) // 检查是否有rawData字段,尝试从中提取type if rawData, rawOk := responseData["rawData"].(string); rawOk { globalLogger.Info("[辅助程序] 尝试从rawData中提取type字段") // 清理rawData中的null字节,避免JSON解析失败 cleanedRawData := strings.Replace(rawData, "\x00", "", -1) var rawDataObj map[string]interface{} if err := json.Unmarshal([]byte(cleanedRawData), &rawDataObj); err == nil { if rawTypeValue, rawTypeOk := rawDataObj["type"]; rawTypeOk { globalLogger.Info("[辅助程序] 成功从rawData中提取type字段: %v", rawTypeValue) typeValue = rawTypeValue ok = true // 同时提取rawData中的data字段并添加到responseData中 if rawDataField, rawDataOk := rawDataObj["data"]; rawDataOk { responseData["data"] = rawDataField globalLogger.Info("[辅助程序] 成功从rawData中提取data字段并添加到responseData") } } else { globalLogger.Error("[错误] rawData中也未找到type字段") } } else { globalLogger.Error("[错误] 解析rawData失败: %v", err) globalLogger.Info("[辅助程序] 清理后的rawData内容: %s", cleanedRawData) } } // 如果仍然没有type字段,创建一个新的对象只包含data字段,避免重复 if !ok { resultData := map[string]interface{}{} if data, ok := responseData["data"]; ok { resultData["data"] = data } else { resultData = responseData } jsonBytes, err := json.Marshal(resultData) if err == nil { globalLogger.Info("[辅助程序] 已将原始数据转换为JSON格式: %s", string(jsonBytes)) } return jsonBytes, err } } else { globalLogger.Info("[辅助程序] 成功获取type字段: %v", typeValue) } // 构建模板文件路径 typeStr := fmt.Sprintf("%v", typeValue) templatePath := filepath.Join(exeDir, "eventdata", typeStr+".json") // 检查模板文件是否存在 _, err = os.Stat(templatePath) if os.IsNotExist(err) { globalLogger.Warn("[警告] 未找到模板文件: %s,直接发送原始数据", templatePath) // 如果模板文件不存在,创建一个新的对象只包含data字段,避免重复 resultData := map[string]interface{}{} if data, ok := responseData["data"]; ok { resultData["data"] = data } else { resultData = responseData } jsonBytes, err := json.Marshal(resultData) if err == nil { globalLogger.Info("[辅助程序] 已将原始数据转换为JSON格式: %s", string(jsonBytes)) } return jsonBytes, err } if err != nil { globalLogger.Error("[错误] 检查模板文件是否存在失败: %v", err) // 如果检查失败,创建一个新的对象只包含data字段,避免重复 resultData := map[string]interface{}{} if data, ok := responseData["data"]; ok { resultData["data"] = data } else { resultData = responseData } jsonBytes, err := json.Marshal(resultData) if err == nil { globalLogger.Info("[辅助程序] 已将原始数据转换为JSON格式: %s", string(jsonBytes)) } return jsonBytes, err } // 读取模板文件内容 templateContent, err := ioutil.ReadFile(templatePath) if err != nil { globalLogger.Error("[错误] 读取模板文件失败: %v", err) // 如果读取失败,创建一个新的对象只包含data字段,避免重复 resultData := map[string]interface{}{} if data, ok := responseData["data"]; ok { resultData["data"] = data } else { resultData = responseData } jsonBytes, err := json.Marshal(resultData) if err == nil { globalLogger.Info("[辅助程序] 已将原始数据转换为JSON格式: %s", string(jsonBytes)) } return jsonBytes, err } // 尝试解析模板为单个JSON对象 var templateData map[string]interface{} err = json.Unmarshal(templateContent, &templateData) if err != nil { globalLogger.Info("[辅助程序] 尝试使用标准JSON解析失败,尝试解析非标准格式: %v", err) // 如果标准解析失败,尝试解析非标准格式(两个连续的JSON对象) // 寻找第二个JSON对象的开始位置 contentStr := string(templateContent) firstObjEnd := -1 braceCount := 0 inString := false for i, char := range contentStr { // 处理字符串中的特殊情况 if char == '"' { inString = !inString } if inString { continue } // 统计花括号数量 if char == '{' { braceCount++ } else if char == '}' { braceCount-- // 当花括号数量回到0时,第一个JSON对象结束 if braceCount == 0 { firstObjEnd = i + 1 break } } } // 如果找到了第一个JSON对象的结束位置,尝试解析第二个JSON对象 if firstObjEnd > 0 && firstObjEnd < len(contentStr) { secondObjContent := strings.TrimSpace(contentStr[firstObjEnd:]) if len(secondObjContent) > 0 && secondObjContent[0] == '{' { globalLogger.Info("[辅助程序] 找到第二个JSON对象,尝试解析") err = json.Unmarshal([]byte(secondObjContent), &templateData) if err != nil { globalLogger.Error("[错误] 解析第二个JSON对象失败: %v", err) // 如果解析失败,创建一个新的对象只包含data字段,避免重复 resultData := map[string]interface{}{} if data, ok := responseData["data"]; ok { resultData["data"] = data } else { resultData = responseData } jsonBytes, err := json.Marshal(resultData) if err == nil { globalLogger.Info("[辅助程序] 已将原始数据转换为JSON格式: %s", string(jsonBytes)) } return jsonBytes, err } } else { globalLogger.Error("[错误] 未找到有效的第二个JSON对象") // 如果未找到第二个JSON对象,创建一个新的对象只包含data字段,避免重复 resultData := map[string]interface{}{} if data, ok := responseData["data"]; ok { resultData["data"] = data } else { resultData = responseData } jsonBytes, err := json.Marshal(resultData) if err == nil { globalLogger.Info("[辅助程序] 已将原始数据转换为JSON格式: %s", string(jsonBytes)) } return jsonBytes, err } } else { globalLogger.Error("[错误] 无法解析模板文件格式") // 如果无法解析,创建一个新的对象只包含data字段,避免重复 resultData := map[string]interface{}{} if data, ok := responseData["data"]; ok { resultData["data"] = data } else { resultData = responseData } jsonBytes, err := json.Marshal(resultData) if err == nil { globalLogger.Info("[辅助程序] 已将原始数据转换为JSON格式: %s", string(jsonBytes)) } return jsonBytes, err } } // 递归替换模板中的占位符 replacePlaceholders(templateData, responseData) // 赋值robotId - 从globalClientMap获取用户ID dataField, ok := templateData["data"].(map[string]interface{}) if ok { // 从globalClientMap获取对应的用户ID userID := runtimeRobotID(iClientId) if userID != "" && !strings.HasPrefix(userID, "client:") { dataField["robotId"] = userID globalLogger.Info("[辅助程序] 已从globalClientMap获取用户ID并赋值给robotId: clientId=%d -> user_id=%s", iClientId, userID) } else { // 如果globalClientMap中没有对应的用户ID,使用clientId作为默认值 dataField["robotId"] = fmt.Sprintf("client:%d", iClientId) globalLogger.Warn("[辅助程序] globalClientMap中未找到用户ID,使用clientId作为默认值: clientId=%d", iClientId) } } else { globalLogger.Warn("[警告] 模板中未找到data字段,无法赋值robotId") } // 赋值deviceCode - 从全局配置获取deviceCode appConfig := config.GetGlobalConfig() if appConfig != nil && appConfig.CallbackConfig.DeviceCode != "" { // 检查templateData是否已存在deviceCode字段 if _, exists := templateData["deviceCode"]; exists { templateData["deviceCode"] = appConfig.CallbackConfig.DeviceCode } else { // 如果不存在,添加此字段 templateData["deviceCode"] = appConfig.CallbackConfig.DeviceCode } globalLogger.Info("[辅助程序] 已从全局配置获取deviceCode并赋值: deviceCode=%s", appConfig.CallbackConfig.DeviceCode) } else { globalLogger.Warn("[辅助程序] 全局配置不存在或deviceCode为空,不赋值deviceCode") } // 序列化转换后的数据 jsonBytes, err := json.Marshal(templateData) if err != nil { globalLogger.Error("[错误] 序列化转换后的数据失败: %v", err) // 如果序列化失败,创建一个新的对象只包含data字段,避免重复 resultData := map[string]interface{}{} if data, ok := responseData["data"]; ok { resultData["data"] = data } else { resultData = responseData } jsonBytes, err := json.Marshal(resultData) if err == nil { globalLogger.Info("[辅助程序] 已将原始数据转换为JSON格式: %s", string(jsonBytes)) } return jsonBytes, err } globalLogger.Info("[辅助程序] 数据已成功转换为模板格式") globalLogger.Info("[辅助程序] 转换后的数据: %s", string(jsonBytes)) // 处理媒体消息事件(图片、视频、语音等) // 调用媒体消息处理函数,并获取需要更新的字段 updatedFields := handleMediaMessageEvents(jsonBytes, iClientId) //globalLogger.Info("[辅助程序] 替换后的数据: %s", string(jsonBytes)) // 如果有需要更新的字段,则更新原始JSON数据 if len(updatedFields) > 0 { var eventData map[string]interface{} if err := json.Unmarshal(jsonBytes, &eventData); err == nil { // 更新字段 for fieldPath, newValue := range updatedFields { updateNestedField(eventData, fieldPath, newValue) } // 重新序列化为JSON var err error jsonBytes, err = json.Marshal(eventData) if err != nil { globalLogger.Error("更新JSON数据失败: %v", err) } } globalLogger.Info("[辅助程序] 替换后的数据: %s", string(jsonBytes)) } return jsonBytes, nil } // handleMediaMessageEvents 处理各类媒体消息事件(图片、视频、语音等) // 返回需要更新的字段映射,键为字段路径,值为新值 func handleMediaMessageEvents(jsonBytes []byte, clientID uint32) map[string]interface{} { updatedFields := make(map[string]interface{}) var eventData map[string]interface{} if err := json.Unmarshal(jsonBytes, &eventData); err != nil { globalLogger.Error("[辅助程序] 解析媒体消息事件失败: %v", err) return updatedFields } // 根据event类型分发到不同的处理函数 if event, ok := eventData["event"]; ok { switch event { case "20003": // 图片消息事件 fileName, fieldPath := handleImageMessageEvent(eventData) if fileName != "" && fieldPath != "" { updatedFields[fieldPath] = fileName } case "20004": // 视频消息事件 fileName, fieldPath := handleVideoMessageEvent(eventData) if fileName != "" && fieldPath != "" { updatedFields[fieldPath] = fileName } case "20012": // 语音消息事件 fileName, fieldPath := handleVoiceMessageEvent(eventData, clientID) if fileName != "" && fieldPath != "" { updatedFields[fieldPath] = fileName } case "20005": // 文件消息事件 fileName, fieldPath := handleFileMessageEvent(eventData) if fileName != "" && fieldPath != "" { updatedFields[fieldPath] = fileName } /* case "20006": // 表情消息事件 fileName, fieldPath := handleEmojiMessageEvent(eventData) if fileName != "" && fieldPath != "" { updatedFields[fieldPath] = fileName } */ case "20014": // 图文消息事件 fileName, fieldPath := handleNewsMessageEvent(eventData) if fileName != "" && fieldPath != "" { updatedFields[fieldPath] = fileName } } } return updatedFields } // handleNewsMessageEvent 处理图文消息事件(event=20014) // 返回上传后的文件路径和需要更新的字段路径 func handleNewsMessageEvent(eventData map[string]interface{}) (string, string) { globalLogger.Info("[辅助程序] 检测到图文消息事件") // 获取data字段 if data, ok := eventData["data"].(map[string]interface{}); ok { var firstImage map[string]interface{} var fileNameFieldPath string var cdnData map[string]interface{} var cdnDataOk bool // 检查imageList是否为数组 if imageListArray, arrayOk := data["imageList"].([]interface{}); arrayOk && len(imageListArray) > 0 { firstImage = imageListArray[0].(map[string]interface{}) cdnData, cdnDataOk = firstImage["cdn"].(map[string]interface{}) fileNameFieldPath = "data.imageList.0.cdn.file_name" } else if imageListObj, objOk := data["imageList"].(map[string]interface{}); objOk { // 如果imageList是对象,尝试获取键"0"对应的值 if firstImageObj, exists := imageListObj["0"]; exists { firstImage = firstImageObj.(map[string]interface{}) cdnData, cdnDataOk = firstImage["cdn"].(map[string]interface{}) fileNameFieldPath = "data.imageList.0.cdn.file_name" } } if cdnDataOk { // 准备下载所需参数 aesKey, _ := cdnData["aes_key"].(string) fileId, fileIdOk := cdnData["file_id"].(string) fileSize, fileSizeOk := cdnData["size"].(float64) originalFileName, _ := cdnData["file_name"].(string) if fileIdOk && fileSizeOk { // 获取文件扩展名 fileExt := ".jpg" // 默认扩展名 if originalFileName != "" { ext := filepath.Ext(originalFileName) if ext != "" { fileExt = ext } } // 生成保存路径 savePath := generateSavePath("news", fmt.Sprintf("news_%s", fileId), fileExt) if savePath != "" { globalLogger.Info("[辅助程序] 图文图片保存路径: %s", savePath) // 图文消息通常使用fileType=1(图片类型) if DownloadFileByFileId(aesKey, fileId, savePath, int(fileSize), 2) { // 下载成功,尝试上传 uploadedPath := uploadMediaFile(savePath, "news") if uploadedPath != "" { // 获取保存路径中的文件名 //newFileName := filepath.Base(savePath) newFileName := uploadedPath globalLogger.Info("[辅助程序] 更新file_name为保存路径中的文件名: %s", newFileName) // 直接在eventData中更新file_name字段 updateNestedField(eventData, fileNameFieldPath, newFileName) // 返回newFileName和fileNameFieldPath,确保更新被包含在updatedFields中 return newFileName, fileNameFieldPath } } } } else { globalLogger.Error("[辅助程序] 缺少必要的图文文件参数") } } else { globalLogger.Error("[辅助程序] 无法获取图文消息的cdn数据") } } else { globalLogger.Error("[辅助程序] 无法获取图文消息的data字段") } return "", "" } // handleEmojiMessageEvent 处理表情消息事件(event=20006) // 返回上传后的文件路径和需要更新的字段路径 func handleEmojiMessageEvent(eventData map[string]interface{}) (string, string) { globalLogger.Info("[辅助程序] 检测到表情消息事件") // 获取data字段 if data, ok := eventData["data"].(map[string]interface{}); ok { // 获取cdnType和cdnData cdnType, cdnTypeOk := data["cdnType"].(float64) cdnData, cdnDataOk := data["cdnData"].(map[string]interface{}) if cdnTypeOk && cdnDataOk { // 准备下载所需参数 url, _ := data["url"].(string) aesKey, _ := cdnData["aes_key"].(string) auth_key, _ := cdnData["auth_key"].(string) fileId, _ := cdnData["fileId"].(string) size, _ := cdnData["size"].(float64) var savePath string var fileNameFieldPath string // 根据cdnType处理 if cdnType == 1 || url != "" { // 生成保存路径 - 表情通常是图片格式 if url != "" { savePath = generateSavePath("emojis", filepath.Base(url), ".jpg") } else if auth_key != "" { savePath = generateSavePath("emojis", fmt.Sprintf("emoji_%s", auth_key), ".jpg") } if savePath != "" { globalLogger.Info("[辅助程序] 表情保存路径: %s", savePath) if url != "" { // 使用DownloadMediaFile下载表情文件 if DownloadMediaFile(url, "", aesKey, int(size), savePath) { // 下载成功,尝试上传 uploadedPath := uploadMediaFile(savePath, "emoji") if uploadedPath != "" { // 从保存路径中提取新的文件名 //newFileName := filepath.Base(savePath) newFileName := uploadedPath globalLogger.Info("[辅助程序] 表情新文件名: %s", newFileName) fileNameFieldPath = "data.cdnData.file_name" // 更新file_name字段 updateNestedField(eventData, fileNameFieldPath, newFileName) // 返回新文件名和字段路径 return newFileName, fileNameFieldPath } } } else if fileId != "" { // 使用DownloadFileByFileId下载 if DownloadFileByFileId(aesKey, fileId, savePath, int(size), 1) { // 下载成功,尝试上传 uploadedPath := uploadMediaFile(savePath, "emoji") if uploadedPath != "" { // 从保存路径中提取新的文件名 //newFileName := filepath.Base(savePath) newFileName := uploadedPath globalLogger.Info("[辅助程序] 表情新文件名: %s", newFileName) fileNameFieldPath = "data.cdnData.file_name" // 更新file_name字段 updateNestedField(eventData, fileNameFieldPath, newFileName) // 返回新文件名和字段路径 return newFileName, fileNameFieldPath } } } } } else { // 处理cdnType=2的情况 savePath = handleCdnType2Download(cdnData, "emojis") if savePath != "" { // 下载成功,尝试上传 uploadedPath := uploadMediaFile(savePath, "emoji") if uploadedPath != "" { // 从保存路径中提取新的文件名 //newFileName := filepath.Base(savePath) newFileName := uploadedPath globalLogger.Info("[辅助程序] 表情新文件名: %s", newFileName) fileNameFieldPath = "data.cdnData.file_name" // 更新file_name字段 updateNestedField(eventData, fileNameFieldPath, newFileName) // 返回新文件名和字段路径 return newFileName, fileNameFieldPath } } } } } // 如果没有找到合适的cdnType或cdnData,返回空字符串 return "", "" } // handleFileMessageEvent 处理文件消息事件(event=20005) // 返回上传后的文件路径和需要更新的字段路径 func handleFileMessageEvent(eventData map[string]interface{}) (string, string) { globalLogger.Info("[辅助程序] 检测到文件消息事件") // 获取data字段 if data, ok := eventData["data"].(map[string]interface{}); ok { // 获取cdnType和cdnData cdnType, cdnTypeOk := data["cdnType"].(float64) cdnData, cdnDataOk := data["cdnData"].(map[string]interface{}) if cdnTypeOk && cdnDataOk { // 根据cdnType处理 if cdnType == 1 { // 准备下载所需参数 url, _ := cdnData["url"].(string) authKey, _ := cdnData["auth_key"].(string) aesKey, _ := cdnData["aes_key"].(string) size, _ := cdnData["size"].(float64) originalFileName, _ := cdnData["file_name"].(string) // 获取文件扩展名 fileExt := ".bin" // 默认扩展名 if originalFileName != "" { ext := filepath.Ext(originalFileName) if ext != "" { fileExt = ext } } // 生成保存路径 savePath := generateSavePath("files", filepath.Base(url), fileExt) if savePath != "" { globalLogger.Info("[辅助程序] 文件保存路径: %s", savePath) if DownloadMediaFile(url, authKey, aesKey, int(size), savePath) { // 下载成功,尝试上传 uploadedPath := uploadMediaFile(savePath, "file") if uploadedPath != "" { // 从保存路径中提取新的文件名 //newFileName := filepath.Base(savePath) newFileName := uploadedPath globalLogger.Info("[辅助程序] 文件新文件名: %s", newFileName) fileNameFieldPath := "data.cdnData.file_name" // 更新file_name字段 updateNestedField(eventData, fileNameFieldPath, newFileName) // 返回新文件名和字段路径 return newFileName, fileNameFieldPath } } } } else { // 原有逻辑:通过file_id下载文件 // 准备下载所需参数 aesKey, _ := cdnData["aes_key"].(string) fileId, fileIdOk := cdnData["file_id"].(string) fileSize, fileSizeOk := cdnData["size"].(float64) originalFileName, _ := cdnData["file_name"].(string) if fileIdOk && fileSizeOk { // 获取文件扩展名 fileExt := ".bin" // 默认扩展名 if originalFileName != "" { ext := filepath.Ext(originalFileName) if ext != "" { fileExt = ext } } // 生成保存路径 savePath := generateSavePath("files", fmt.Sprintf("file_%s", fileId), fileExt) if savePath != "" { globalLogger.Info("[辅助程序] 文件保存路径: %s", savePath) // 文件消息通常使用fileType=6 if DownloadFileByFileId(aesKey, fileId, savePath, int(fileSize), 5) { // 下载成功,尝试上传 uploadedPath := uploadMediaFile(savePath, "file") if uploadedPath != "" { // 获取保存路径中的文件名 //newFileName := filepath.Base(savePath) newFileName := uploadedPath globalLogger.Info("[辅助程序] 更新file_name为保存路径中的文件名: %s", newFileName) // 文件消息的字段路径为data.cdnData.file_name fileNameFieldPath := "data.cdnData.file_name" // 直接在eventData中更新file_name字段 updateNestedField(eventData, fileNameFieldPath, newFileName) // 返回newFileName和fileNameFieldPath,确保更新被包含在updatedFields中 return newFileName, fileNameFieldPath } } } } else { globalLogger.Error("[辅助程序] 缺少必要的文件参数") } } } else { globalLogger.Error("[辅助程序] 未找到文件消息的CDN数据") } } // 如果没有找到合适的cdn数据,返回空字符串 return "", "" } // handleImageMessageEvent 处理图片消息事件(event=20003) // 返回上传后的文件路径和需要更新的字段路径 func handleImageMessageEvent(eventData map[string]interface{}) (string, string) { globalLogger.Info("[辅助程序] 检测到图片消息事件") // 获取data字段 if data, ok := eventData["data"].(map[string]interface{}); ok { // 获取cdnType和cdnData cdnType, cdnTypeOk := data["cdnType"].(float64) cdnData, cdnDataOk := data["cdnData"].(map[string]interface{}) if cdnTypeOk && cdnDataOk { // 准备下载所需参数 url, _ := cdnData["url"].(string) authKey, _ := cdnData["auth_key"].(string) aesKey, _ := cdnData["aes_key"].(string) size, _ := cdnData["size"].(float64) var savePath string var fileNameFieldPath string // 根据cdnType处理 if cdnType == 1 { // 生成保存路径 savePath = generateSavePath("images", filepath.Base(url), ".jpg") if savePath != "" { globalLogger.Info("[辅助程序] 图片保存路径: %s", savePath) if DownloadMediaFile(url, authKey, aesKey, int(size), savePath) { // 下载成功,尝试上传 uploadedPath := uploadMediaFile(savePath, "image") if uploadedPath != "" { // 从保存路径中提取新的文件名 //newFileName := filepath.Base(savePath) newFileName := uploadedPath globalLogger.Info("[辅助程序] 图片新文件名: %s", newFileName) fileNameFieldPath = "data.cdnData.file_name" // 更新file_name字段 updateNestedField(eventData, fileNameFieldPath, newFileName) // 返回新文件名和字段路径 return newFileName, fileNameFieldPath } } } } else { savePath = handleCdnType2Download(cdnData, "images") if savePath != "" { // 下载成功,尝试上传 // 增加2秒延迟 //time.Sleep(2 * time.Second) uploadedPath := uploadMediaFile(savePath, "image") if uploadedPath != "" { // 从保存路径中提取新的文件名 //newFileName := filepath.Base(savePath) newFileName := uploadedPath globalLogger.Info("[辅助程序] 图片新文件名: %s", newFileName) fileNameFieldPath = "data.cdnData.file_name" // 更新file_name字段 updateNestedField(eventData, fileNameFieldPath, newFileName) // 返回新文件名和字段路径 return newFileName, fileNameFieldPath } } } } } // 如果没有找到合适的cdnType或cdnData,返回空字符串 return "", "" } // handleVideoMessageEvent 处理视频消息事件(event=20004) // 返回上传后的文件路径和需要更新的字段路径 func handleVideoMessageEvent(eventData map[string]interface{}) (string, string) { globalLogger.Info("[辅助程序] 检测到视频消息事件") // 获取data字段 if data, ok := eventData["data"].(map[string]interface{}); ok { // 获取cdnType和cdnData cdnType, cdnTypeOk := data["cdnType"].(float64) cdnData, cdnDataOk := data["cdnData"].(map[string]interface{}) if cdnTypeOk && cdnDataOk { // 准备下载所需参数 url, _ := cdnData["url"].(string) authKey, _ := cdnData["auth_key"].(string) aesKey, _ := cdnData["aes_key"].(string) size, _ := cdnData["size"].(float64) // 处理视频下载 var savePath string var fileNameFieldPath string if cdnType == 1 { // 生成保存路径 savePath = generateSavePath("videos", filepath.Base(url), ".mp4") if savePath != "" { globalLogger.Info("[辅助程序] 视频保存路径: %s", savePath) if DownloadMediaFile(url, authKey, aesKey, int(size), savePath) { // 下载成功,尝试上传 // 增加2秒延迟 //time.Sleep(2 * time.Second) uploadedPath := uploadMediaFile(savePath, "video") if uploadedPath != "" { // 从保存路径中提取新的文件名 //newFileName := filepath.Base(savePath) newFileName := uploadedPath globalLogger.Info("[辅助程序] 视频新文件名: %s", newFileName) fileNameFieldPath = "data.cdnData.file_name" // 更新file_name字段 updateNestedField(eventData, fileNameFieldPath, newFileName) // 返回新文件名和字段路径 return newFileName, fileNameFieldPath } } } } else { savePath = handleCdnType2Download(cdnData, "videos") if savePath != "" { // 下载成功,尝试上传 // 增加2秒延迟 //time.Sleep(2 * time.Second) uploadedPath := uploadMediaFile(savePath, "video") if uploadedPath != "" { // 从保存路径中提取新的文件名 //newFileName := filepath.Base(savePath) newFileName := uploadedPath globalLogger.Info("[辅助程序] 视频新文件名: %s", newFileName) fileNameFieldPath = "data.cdnData.file_name" // 更新file_name字段 updateNestedField(eventData, fileNameFieldPath, newFileName) // 返回新文件名和字段路径 return newFileName, fileNameFieldPath } } } } } // 如果没有找到合适的cdnType或cdnData,返回空字符串 return "", "" } // convertSilkToMp3 将silk格式的音频文件转换为mp3格式 // 返回转换后的mp3文件路径,如果转换失败则返回错误 func convertSilkToMp3(silkPath string) (string, error) { // 检查文件是否存在 if _, err := os.Stat(silkPath); os.IsNotExist(err) { return "", fmt.Errorf("silk文件不存在: %s", silkPath) } // 生成mp3文件路径 mp3Dir := filepath.Dir(silkPath) mp3Name := strings.TrimSuffix(filepath.Base(silkPath), ".silk") + ".mp3" mp3Path := filepath.Join(mp3Dir, mp3Name) // 检查ffmpeg是否可用 ffmpegPath, err := findFFmpeg() if err != nil { return "", fmt.Errorf("找不到ffmpeg,请确保已安装ffmpeg并添加到系统环境变量中: %v", err) } // 尝试多种转换方法,提高兼容性和音质 // 方法1: 使用高质量参数转换(保持原始音频特性) cmd := exec.Command(ffmpegPath, "-f", "s16le", "-ar", "24000", "-ac", "1", "-i", silkPath, "-codec:a", "libmp3lame", "-q:a", "4", "-ac", "1", "-ar", "24000", mp3Path) output, err := cmd.CombinedOutput() if err != nil { globalLogger.Info("[辅助程序] 方法1转换失败,尝试方法2") // 方法2: 自动检测格式并使用恒定质量编码 cmd = exec.Command(ffmpegPath, "-i", silkPath, "-codec:a", "libmp3lame", "-q:a", "3", "-ar", "24000", "-ac", "1", "-y", mp3Path) output, err = cmd.CombinedOutput() if err != nil { globalLogger.Info("[辅助程序] 方法2转换失败,尝试方法3") // 方法3: 使用pipe协议和更高比特率 cmd = exec.Command(ffmpegPath, "-protocol_whitelist", "file,pipe", "-i", silkPath, "-vn", "-y", "-codec:a", "libmp3lame", "-b:a", "128k", "-ar", "24000", "-ac", "1", "-preset", "medium", mp3Path) output, err = cmd.CombinedOutput() if err != nil { // 记录详细错误信息 globalLogger.Error("[辅助程序] 转换silk到mp3失败: ffmpeg转换失败: %v, 输出: %s", err, string(output)) } } } if err != nil { // 所有方法都失败,提供更详细的错误信息 return "", fmt.Errorf("ffmpeg转换失败: %v, 输出: %s", err, string(output)) } // 检查转换后的文件是否存在 if _, err := os.Stat(mp3Path); os.IsNotExist(err) { return "", fmt.Errorf("转换后的mp3文件不存在: %s", mp3Path) } // 检查文件大小,确保不是空文件 fileInfo, err := os.Stat(mp3Path) if err != nil || fileInfo.Size() < 100 { return "", fmt.Errorf("转换后的mp3文件可能为空或损坏: %s, 大小: %d 字节", mp3Path, fileInfo.Size()) } // 可选:删除原silk文件 // if err := os.Remove(silkPath); err != nil { // globalLogger.Warn("[辅助程序] 删除原silk文件失败: %v", err) // } return mp3Path, nil } // findFFmpeg 在系统中查找ffmpeg可执行文件 func findFFmpeg() (string, error) { // 首先尝试直接使用ffmpeg命令(如果已添加到系统环境变量) if path, err := exec.LookPath("ffmpeg"); err == nil { return path, nil } // 检查程序目录下是否有ffmpeg currentDir, _ := os.Getwd() ffmpegPath := filepath.Join(currentDir, "ffmpeg.exe") if _, err := os.Stat(ffmpegPath); err == nil { return ffmpegPath, nil } return "", fmt.Errorf("在系统中找不到ffmpeg可执行文件") } // handleVoiceMessageEvent 处理语音消息事件(event=20012) // 返回上传后的文件路径和需要更新的字段路径 func handleVoiceMessageEvent(eventData map[string]interface{}, clientID uint32) (string, string) { globalLogger.Info("[辅助程序] 检测到语音消息事件") // 获取data字段 if data, ok := eventData["data"].(map[string]interface{}); ok { // 获取c2cCdnData或cdnData var cdnData map[string]interface{} var hasCdnData bool var fileNameFieldPath string // 检查是否有c2cCdnData字段(语音消息特有) if c2cCdnData, ok := data["c2cCdnData"].(map[string]interface{}); ok { cdnData = c2cCdnData hasCdnData = true fileNameFieldPath = "data.c2cCdnData.file_name" } else { // 检查是否有cdnData字段(兼容其他格式) var ok bool cdnData, ok = data["cdnData"].(map[string]interface{}) if ok { hasCdnData = true fileNameFieldPath = "data.cdnData.file_name" } } if hasCdnData { // 准备下载所需参数 aesKey, _ := cdnData["aes_key"].(string) fileId, fileIdOk := cdnData["file_id"].(string) fileSize, fileSizeOk := cdnData["size"].(float64) // 语音消息通常是.silk格式 if fileIdOk && fileSizeOk { // 生成保存路径 savePath := generateSavePath("voices", fmt.Sprintf("voice_%s", fileId), ".silk") if savePath != "" { globalLogger.Info("[辅助程序] 语音保存路径: %s", savePath) // 语音消息通常使用fileType=5 if DownloadFileByFileIdForClient(clientID, aesKey, fileId, savePath, int(fileSize), 5) { // 下载成功,将silk文件转换为mp3 /* mp3Path, err := convertSilkToMp3(savePath) if err != nil { globalLogger.Error("[辅助程序] 转换silk到mp3失败: %v", err) } else { // 使用转换后的mp3文件 savePath = mp3Path globalLogger.Info("[辅助程序] 成功将silk转换为mp3: %s", mp3Path) } */ // 上传文件 uploadedPath := uploadMediaFile(savePath, "voice") if uploadedPath != "" { // 获取保存路径中的文件名 //newFileName := filepath.Base(savePath) newFileName := uploadedPath globalLogger.Info("[辅助程序] 更新file_name为保存路径中的文件名: %s", newFileName) // 直接在eventData中更新file_name字段 updateNestedField(eventData, fileNameFieldPath, newFileName) // 返回fileNameFieldPath和newFileName,确保更新被包含在updatedFields中 return newFileName, fileNameFieldPath } } } } else { globalLogger.Error("[辅助程序] 缺少必要的语音文件参数") } } else { globalLogger.Error("[辅助程序] 未找到语音消息的CDN数据") } } // 如果没有找到合适的cdnType或cdnData,返回空字符串 return "", "" } // handleCdnType2Download 处理cdnType=2的文件下载 // 返回下载后的文件路径,下载失败返回空字符串 func handleCdnType2Download(cdnData map[string]interface{}, subDir string) string { globalLogger.Info("[辅助程序] cdnType=2,调用DownloadFileByFileId函数") // 获取所需参数 fileId, fileIdOk := cdnData["file_id"].(string) fileSize, fileSizeOk := cdnData["size"].(float64) // 使用size字段而不是file_size fileType, fileTypeOk := cdnData["file_type"].(float64) aesKey, _ := cdnData["aes_key"].(string) fileName, _ := cdnData["file_name"].(string) // 检查基本参数 if fileIdOk && (fileSizeOk || fileName != "") { // 根据fileType或fileName确定文件扩展名 fileExt := ".bin" // 默认扩展名 // 优先从fileType确定扩展名 if fileName != "" { // 如果没有fileType,尝试从fileName获取扩展名 ext := filepath.Ext(fileName) if ext != "" { fileExt = ext } } else { if fileTypeOk { if int(fileType) == 1 { fileExt = ".jpg" } else if int(fileType) == 2 { fileExt = ".jpg" } else if int(fileType) == 3 { fileExt = ".jpg" } else if int(fileType) == 4 { fileExt = ".mp4" } else if int(fileType) == 5 { fileExt = ".silk" } } } // 生成保存路径 savePath := generateSavePath(subDir, fmt.Sprintf("file_%s", fileId), fileExt) if savePath != "" { globalLogger.Info("[辅助程序] 文件保存路径: %s", savePath) // 调用DownloadFileByFileId函数,fileType可能为0(如果从fileName获取扩展名) if DownloadFileByFileId(aesKey, fileId, savePath, int(fileSize), int(fileType)) { return savePath } } } else { // 详细记录缺少的参数信息 missingParams := []string{} if !fileIdOk { missingParams = append(missingParams, "file_id") } if !fileSizeOk && fileName == "" { missingParams = append(missingParams, "size/file_name") } globalLogger.Error("[辅助程序] 缺少必要的文件参数,无法下载文件\t缺少参数: %v\t文件类型可从\"file_name\":\"%s\"获取\tfile_size可从size\":%v获取", missingParams, fileName, cdnData["size"]) } return "" } // uploadMediaFile 上传媒体文件到配置的fileUploadUrl func uploadMediaFile(filePath string, fileType string) string { globalLogger.Info("[辅助程序] 开始上传媒体文件: %s", filePath) // 从全局配置中获取fileUploadUrl configManager := config.GetGlobalConfig() if configManager == nil { globalLogger.Error("[辅助程序] 无法获取全局配置") return "" } fileUploadUrl := configManager.CallbackConfig.FileUploadUrl if fileUploadUrl == "" { globalLogger.Error("[辅助程序] 配置中的fileUploadUrl为空") return "" } // 打开文件 file, err := os.Open(filePath) if err != nil { globalLogger.Error("[辅助程序] 无法打开文件: %v", err) return "" } defer file.Close() // 创建multipart表单 body := &bytes.Buffer{} writer := multipart.NewWriter(body) // 创建文件字段 fileField, err := writer.CreateFormFile("file", filepath.Base(filePath)) if err != nil { globalLogger.Error("[辅助程序] 创建表单文件字段失败: %v", err) return "" } // 复制文件内容到表单字段 _, err = io.Copy(fileField, file) if err != nil { globalLogger.Error("[辅助程序] 复制文件内容失败: %v", err) return "" } // 直接使用filePath中的带后缀文件名 newFileName := filepath.Base(filePath) err = writer.WriteField("path", newFileName) if err != nil { globalLogger.Error("[辅助程序] 写入path字段失败: %v", err) return "" } // 关闭multipart writer err = writer.Close() if err != nil { globalLogger.Error("[辅助程序] 关闭表单写入器失败: %v", err) return "" } // 创建HTTP请求 // 创建HTTP请求 req, err := http.NewRequest("POST", fileUploadUrl, body) if err != nil { globalLogger.Error("[辅助程序] 创建HTTP请求失败: %v", err) return "" } // 设置请求头 req.Header.Set("Content-Type", writer.FormDataContentType()) // 添加Authorization请求头 config := config.GetGlobalConfig() if config != nil && config.CallbackConfig.CallbackToken != "" { req.Header.Set("Authorization", "Bearer "+config.CallbackConfig.CallbackToken) } globalLogger.Info("[辅助程序] 请求头: %v", req.Header) // 发送请求 client := &http.Client{} resp, err := client.Do(req) if err != nil { globalLogger.Error("[辅助程序] 发送文件上传请求失败: %v", err) return "" } defer resp.Body.Close() // 检查响应状态 if resp.StatusCode != http.StatusOK { globalLogger.Error("[辅助程序] 文件上传失败,状态码: %d", resp.StatusCode) return "" } // 读取响应体 respBody, err := io.ReadAll(resp.Body) if err != nil { globalLogger.Error("[辅助程序] 读取上传响应失败: %v", err) return "" } // 处理响应结果 globalLogger.Info("[辅助程序] 文件上传成功,响应: %s", string(respBody)) // 解析JSON响应,提取data字段的值 var result map[string]interface{} if err := json.Unmarshal(respBody, &result); err == nil { // 检查是否有data字段且类型正确 if data, ok := result["data"]; ok { if dataStr, ok := data.(string); ok { // 使用data字段的值作为返回值 return dataStr } } } // 如果解析失败,仍然返回原始文件名 globalLogger.Info("[辅助程序] 解析响应JSON失败,返回原始文件名") return newFileName } // generateUniqueFileName 生成唯一的文件名 func generateUniqueFileName(originalFileName string) string { // 获取文件名和扩展名 baseName := filepath.Base(originalFileName) ext := filepath.Ext(baseName) nameWithoutExt := strings.TrimSuffix(baseName, ext) // 生成带时间戳的唯一文件名 timestamp := time.Now().Format("20060102_150405_000") uniqueName := fmt.Sprintf("%s_%s%s", nameWithoutExt, timestamp, ext) // 清理文件名,移除特殊字符 return sanitizeFileName(uniqueName) } // updateNestedField 更新嵌套的JSON字段 func updateNestedField(data map[string]interface{}, fieldPath string, value interface{}) { // 分割字段路径 parts := strings.Split(fieldPath, ".") current := data // 遍历路径,直到最后一个字段 for i := 0; i < len(parts)-1; i++ { part := parts[i] // 检查当前路径是否存在 if next, ok := current[part].(map[string]interface{}); ok { current = next } else { // 如果不存在,创建新的map newMap := make(map[string]interface{}) current[part] = newMap current = newMap } } // 更新最后一个字段 current[parts[len(parts)-1]] = value } // generateSavePath 生成文件保存路径 func generateSavePath(subDir string, baseName string, extension string) string { // 获取可执行文件所在目录 exePath, err := os.Executable() if err != nil { globalLogger.Error("[辅助程序] 获取可执行文件路径失败: %v", err) return "" } exeDir := filepath.Dir(exePath) // 创建保存目录 - exe父级目录的temp文件夹 exeParentDir := filepath.Dir(exeDir) // 获取exe目录的父级目录 saveDir := filepath.Join(exeParentDir, "temp") err = os.MkdirAll(saveDir, os.ModePerm) if err != nil { globalLogger.Error("[辅助程序] 创建保存目录失败: %v", err) return "" } // 生成文件名:日期加毫秒的时间再加上5位随机数 // 格式化日期(年月日时分秒毫秒)和5位随机数 timestamp := time.Now().Format("20060102150405.000") randomNum := rand.Intn(99999) + 10000 // 生成10000-99999之间的随机数 fileName := fmt.Sprintf("%s_%05d%s", strings.Replace(timestamp, ".", "", -1), randomNum, extension) return filepath.Join(saveDir, fileName) } // sanitizeFileName 清理文件名,移除可能的路径分隔符等特殊字符 func sanitizeFileName(name string) string { // 移除扩展名 name = strings.TrimSuffix(name, filepath.Ext(name)) // 移除路径分隔符和其他特殊字符 name = strings.ReplaceAll(name, "/", "_") name = strings.ReplaceAll(name, "\\", "_") name = strings.ReplaceAll(name, ":", "_") name = strings.ReplaceAll(name, "*", "_") name = strings.ReplaceAll(name, "?", "_") name = strings.ReplaceAll(name, "\"", "_") name = strings.ReplaceAll(name, "<", "_") name = strings.ReplaceAll(name, ">", "_") name = strings.ReplaceAll(name, "|", "_") return name } // replacePlaceholders 递归替换模板中的占位符,支持数组索引和复杂路径 func replacePlaceholders(templateData map[string]interface{}, sourceData map[string]interface{}) { for key, value := range templateData { // 检查是否是字符串类型 strValue, ok := value.(string) if ok { // 检查是否是占位符 {{...}} if strings.HasPrefix(strValue, "{{") && strings.HasSuffix(strValue, "}}") { // 提取占位符内容 placeholder := strings.Trim(strValue, "{}") placeholder = strings.TrimSpace(placeholder) // 尝试从sourceData中获取对应的值 // 支持从data字段中获取 if strings.HasPrefix(placeholder, "data.") { fieldPath := strings.TrimPrefix(placeholder, "data.") // 获取数据源,优先从data字段开始 var currentData interface{} = sourceData if dataMap, ok := sourceData["data"].(map[string]interface{}); ok { currentData = dataMap } // 使用新的路径解析函数 value, found := getValueByPath(currentData, fieldPath) if found { templateData[key] = value continue } // 如果仍然找不到,提供默认值 globalLogger.Debug("[调试] 占位符 %s 对应的值不存在,使用空字符串作为默认值", placeholder) templateData[key] = "" } } } else if nestedMap, ok := value.(map[string]interface{}); ok { // 递归处理嵌套对象 replacePlaceholders(nestedMap, sourceData) } else if nestedArray, ok := value.([]interface{}); ok { // 处理数组 for _, item := range nestedArray { if itemMap, ok := item.(map[string]interface{}); ok { replacePlaceholders(itemMap, sourceData) } } } } } // getValueByPath 根据路径获取嵌套值,支持数组索引 // 例如:image_list[0].cdn.aes_key func getValueByPath(data interface{}, path string) (interface{}, bool) { parts := parsePath(path) current := data for _, part := range parts { if current == nil { return nil, false } switch p := part.(type) { case string: // 处理对象字段访问 if m, ok := current.(map[string]interface{}); ok { if val, exists := m[p]; exists { current = val } else { return nil, false } } else { return nil, false } case int: // 处理数组索引访问 if arr, ok := current.([]interface{}); ok { if p >= 0 && p < len(arr) { current = arr[p] } else { return nil, false } } else { return nil, false } } } return current, true } // parsePath 解析路径字符串为部分数组 // 例如:image_list[0].cdn.aes_key -> ["image_list", 0, "cdn", "aes_key"] func parsePath(path string) []interface{} { var parts []interface{} re := regexp.MustCompile(`([^\[\].]+)|\[(\d+)\]`) matches := re.FindAllStringSubmatch(path, -1) for _, match := range matches { if match[1] != "" { // 普通字段名 parts = append(parts, match[1]) } else if match[2] != "" { // 数组索引 if idx, err := strconv.Atoi(match[2]); err == nil { parts = append(parts, idx) } } } return parts } // GetClientIdFromRequestParams 从第三方请求参数中获取clientId // 优先使用robotId,如果没有则使用instanceId,通过globalClientMap获取对应的客户端ID func GetClientIdFromRequestParams(params map[string]interface{}) uint32 { // 默认clientId为1 defaultClientId := uint32(0) // 检查params是否为空 if params == nil { globalLogger.Info("[辅助程序] 请求参数为空,使用默认clientId: %d", defaultClientId) return defaultClientId } if clientIDValue, exists := params["clientId"]; exists { switch v := clientIDValue.(type) { case float64: if v > 0 { globalLogger.Info("[辅助程序] 从请求参数获取数字clientId: %d", uint32(v)) return uint32(v) } case int: if v > 0 { globalLogger.Info("[辅助程序] 从请求参数获取数字clientId: %d", v) return uint32(v) } case string: if parsed, err := strconv.ParseUint(strings.TrimSpace(v), 10, 32); err == nil && parsed > 0 { globalLogger.Info("[辅助程序] 从请求参数获取字符串clientId: %d", parsed) return uint32(parsed) } } } // 首先尝试从params中获取robotId if robotId, ok := params["robotId"].(string); ok && robotId != "" && robotId != "机器人微信id" { // 从globalClientMap中查找对应的客户端ID for clientId, userId := range globalClientMap { if userId == robotId { globalLogger.Info("[辅助程序] 从请求参数获取robotId: %s,通过globalClientMap找到对应clientId: %d", robotId, clientId) return uint32(clientId) } } // 如果在globalClientMap中未找到,尝试将robotId作为数字clientId /*clientId, err := strconv.ParseUint(strings.TrimSpace(robotId), 10, 32) if err == nil { globalLogger.Info("[辅助程序] robotId作为数字clientId: %d", clientId) return uint32(clientId) } globalLogger.Warn("[辅助程序] robotId转换失败: %v,robotId值: %s", err, robotId)*/ } // 如果robotId不存在或查找失败,尝试从params中获取instanceId if instanceId, ok := params["instanceId"].(string); ok && instanceId != "" && instanceId != "实例id 和 robotId 二者填一者就可以" { // 从globalClientMap中查找对应的客户端ID for clientId, userId := range globalClientMap { if userId == instanceId { globalLogger.Info("[辅助程序] 从请求参数获取instanceId: %s,通过globalClientMap找到对应clientId: %d", instanceId, clientId) return uint32(clientId) } } // 如果在globalClientMap中未找到,尝试将instanceId作为数字clientId /*clientId, err := strconv.ParseUint(strings.TrimSpace(instanceId), 10, 32) if err == nil { globalLogger.Info("[辅助程序] instanceId作为数字clientId: %d", clientId) return uint32(clientId) } globalLogger.Warn("[辅助程序] instanceId转换失败: %v,instanceId值: %s", err, instanceId)*/ } // 如果robotId和instanceId都不存在或查找失败,使用默认值 globalLogger.Info("[辅助程序] 请求参数中未找到有效的robotId或instanceId,使用默认clientId: %d", defaultClientId) return defaultClientId } // DownloadMediaFile 下载媒体文件11171 cdn_type为1 func DownloadMediaFile(url string, authKey string, aesKey string, size int, savePath string) bool { return DownloadMediaFileForClient(0, url, authKey, aesKey, size, savePath) } func DownloadMediaFileForClient(clientID uint32, url string, authKey string, aesKey string, size int, savePath string) bool { globalLogger.Info("下载媒体文件") // 构建请求数据 requestData := map[string]interface{}{ "type": 11171, "data": map[string]interface{}{ "url": url, "auth_key": authKey, "aes_key": aesKey, "size": size, "save_path": savePath, }, } jsonData, err := json.Marshal(requestData) if err != nil { globalLogger.Error("构建下载媒体文件请求失败: %v", err) return false } // 优化:直接调用handleSendWxWorkData函数,而不是通过HTTP请求 // 因为handleSendWxWorkDataHTTP中已经将11170类型加入无需回调列表,所以这里直接调用即可 params := map[string]interface{}{ "data": string(jsonData), } if clientID > 0 { params["clientId"] = clientID } result, err := handleSendWxWorkData(params) if err != nil { globalLogger.Error("处理文件下载请求失败: %v", err) return false } // 验证结果 - 首先将interface{}断言为map[string]interface{} resultMap, ok := result.(map[string]interface{}) if !ok { globalLogger.Error("处理结果类型错误,不是map[string]interface{}") return false } // 验证success字段 if success, ok := resultMap["success"].(bool); ok && success { globalLogger.Info("媒体文件下载请求处理成功") // 不再使用响应通道,改为直接检测文件是否创建完成 globalLogger.Info("[辅助程序] 开始检测文件是否下载完成, savePath: %s", savePath) // 设置15秒超时 timeout := time.After(15 * time.Second) ticker := time.NewTicker(500 * time.Millisecond) // 每500毫秒检查一次 defer ticker.Stop() // 等待文件创建完成或超时 fileCreated := false for { select { case <-ticker.C: // 检查文件是否存在 if _, err := os.Stat(savePath); err == nil { globalLogger.Info("[辅助程序] 文件已成功下载并保存: %s", savePath) fileCreated = true goto fileCheckDone } case <-timeout: globalLogger.Warn("[辅助程序] 等待文件下载超时(15秒), savePath: %s", savePath) goto fileCheckDone } } fileCheckDone: globalLogger.Info("[辅助程序] 文件检查完成,fileCreated: %v", fileCreated) return fileCreated } globalLogger.Warn("媒体文件下载返回格式不正确: %v", resultMap) return false } // DownloadFileByFileId 通过file_id下载文件11170 cdn_type为2 // 优化:直接函数调用代替HTTP请求,避免同一进程内的HTTP自调用 func DownloadFileByFileId(aesKey string, fileId string, savePath string, fileSize int, fileType int) bool { return DownloadFileByFileIdForClient(0, aesKey, fileId, savePath, fileSize, fileType) } func DownloadFileByFileIdForClient(clientID uint32, aesKey string, fileId string, savePath string, fileSize int, fileType int) bool { globalLogger.Info("通过file_id下载文件") // 构建请求数据 requestData := map[string]interface{}{ "type": 11170, "data": map[string]interface{}{ "aes_key": aesKey, "file_id": fileId, "save_path": savePath, "file_size": fileSize, "file_type": fileType, }, } jsonData, err := json.Marshal(requestData) if err != nil { globalLogger.Error("构建下载媒体文件请求失败: %v", err) return false } // 优化:直接调用handleSendWxWorkData函数,而不是通过HTTP请求 // 因为handleSendWxWorkDataHTTP中已经将11170类型加入无需回调列表,所以这里直接调用即可 params := map[string]interface{}{ "data": string(jsonData), } if clientID > 0 { params["clientId"] = clientID } result, err := handleSendWxWorkData(params) if err != nil { globalLogger.Error("处理文件下载请求失败: %v", err) return false } // 验证结果 - 首先将interface{}断言为map[string]interface{} resultMap, ok := result.(map[string]interface{}) if !ok { globalLogger.Error("处理结果类型错误,不是map[string]interface{}") return false } // 验证success字段 if success, ok := resultMap["success"].(bool); ok && success { globalLogger.Info("媒体文件下载请求处理成功") // 不再使用响应通道,改为直接检测文件是否创建完成 globalLogger.Info("[辅助程序] 开始检测文件是否下载完成, savePath: %s", savePath) // 设置15秒超时 timeout := time.After(15 * time.Second) ticker := time.NewTicker(500 * time.Millisecond) // 每500毫秒检查一次 defer ticker.Stop() // 等待文件创建完成或超时 fileCreated := false for { select { case <-ticker.C: // 检查文件是否存在 if _, err := os.Stat(savePath); err == nil { globalLogger.Info("[辅助程序] 文件已成功下载并保存: %s", savePath) fileCreated = true goto fileCheckDone } case <-timeout: globalLogger.Warn("[辅助程序] 等待文件下载超时(15秒), savePath: %s", savePath) goto fileCheckDone } } fileCheckDone: globalLogger.Info("[辅助程序] 文件检查完成,fileCreated: %v", fileCreated) return fileCreated } globalLogger.Warn("媒体文件下载返回格式不正确: %v", resultMap) return false }