Files
qiweimanager-master/helper/client_id_handler.go

1561 lines
54 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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转换失败: %vrobotId值: %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转换失败: %vinstanceId值: %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
}