1561 lines
54 KiB
Go
1561 lines
54 KiB
Go
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
|
||
}
|