package types import ( "context" "fmt" "monica-proxy/internal/config" "monica-proxy/internal/utils" "net/http" "strings" "sync" "time" "github.com/cespare/xxhash/v2" "github.com/google/uuid" ) const MaxFileSize = 10 * 1024 * 1024 // 10MB var imageCache sync.Map // sampleAndHash 对base64字符串进行采样并计算xxHash func sampleAndHash(data string) string { // 如果数据长度小于1024,直接计算整个字符串的哈希 if len(data) <= 1024 { return fmt.Sprintf("%x", xxhash.Sum64String(data)) } // 采样策略: // 1. 取前256字节 // 2. 取中间256字节 // 3. 取最后256字节 var samples []string samples = append(samples, data[:256]) mid := len(data) / 2 samples = append(samples, data[mid-128:mid+128]) samples = append(samples, data[len(data)-256:]) // 将采样数据拼接后计算哈希 return fmt.Sprintf("%x", xxhash.Sum64String(strings.Join(samples, ""))) } // UploadBase64Image 上传base64编码的图片到Monica func UploadBase64Image(ctx context.Context, base64Data string) (*FileInfo, error) { // 1. 生成缓存key cacheKey := sampleAndHash(base64Data) // 2. 检查缓存 if value, exists := imageCache.Load(cacheKey); exists { return value.(*FileInfo), nil } // 3. 解析base64数据 // 移除 "data:image/png;base64," 这样的前缀 parts := strings.Split(base64Data, ",") if len(parts) != 2 { return nil, fmt.Errorf("invalid base64 image format") } // 获取图片类型 mimeType := strings.TrimSuffix(strings.TrimPrefix(parts[0], "data:"), ";base64") if !strings.HasPrefix(mimeType, "image/") { return nil, fmt.Errorf("invalid image mime type: %s", mimeType) } // 解码base64数据 imageData, err := utils.Base64Decode(parts[1]) if err != nil { return nil, fmt.Errorf("decode base64 failed: %v", err) } // 4. 验证图片格式和大小 fileInfo, err := validateImageBytes(imageData, mimeType) if err != nil { return nil, fmt.Errorf("validate image failed: %v", err) } // log.Printf("file info: %+v", fileInfo) // 5. 获取预签名URL preSignReq := &PreSignRequest{ FilenameList: []string{fileInfo.FileName}, Module: ImageModule, Location: ImageLocation, ObjID: uuid.New().String(), } var preSignResp PreSignResponse _, err = utils.RestyDefaultClient.R(). SetContext(ctx). SetHeader("cookie", config.MonicaConfig.MonicaCookie). SetBody(preSignReq). SetResult(&preSignResp). Post(PreSignURL) if err != nil { return nil, fmt.Errorf("get pre-sign url failed: %v", err) } if len(preSignResp.Data.PreSignURLList) == 0 || len(preSignResp.Data.ObjectURLList) == 0 { return nil, fmt.Errorf("no pre-sign url or object url returned") } // log.Printf("preSign info: %+v", preSignResp) // 6. 上传图片数据 _, err = utils.RestyDefaultClient.R(). SetContext(ctx). SetHeader("Content-Type", fileInfo.FileType). SetBody(imageData). Put(preSignResp.Data.PreSignURLList[0]) if err != nil { return nil, fmt.Errorf("upload file failed: %v", err) } // 7. 创建文件对象 fileInfo.ObjectURL = preSignResp.Data.ObjectURLList[0] uploadReq := &FileUploadRequest{ Data: []FileInfo{*fileInfo}, } var uploadResp FileUploadResponse _, err = utils.RestyDefaultClient.R(). SetContext(ctx). SetHeader("cookie", config.MonicaConfig.MonicaCookie). SetBody(uploadReq). SetResult(&uploadResp). Post(FileUploadURL) if err != nil { return nil, fmt.Errorf("create file object failed: %v", err) } // log.Printf("uploadResp: %+v", uploadResp) if len(uploadResp.Data.Items) > 0 { fileInfo.FileName = uploadResp.Data.Items[0].FileName fileInfo.FileType = uploadResp.Data.Items[0].FileType fileInfo.FileSize = uploadResp.Data.Items[0].FileSize fileInfo.FileUID = uploadResp.Data.Items[0].FileUID fileInfo.FileExt = uploadResp.Data.Items[0].FileType fileInfo.FileTokens = uploadResp.Data.Items[0].FileTokens fileInfo.FileChunks = uploadResp.Data.Items[0].FileChunks } fileInfo.UseFullText = true fileInfo.FileURL = preSignResp.Data.CDNURLList[0] // 8. 获取文件llm读取结果知道有返回 var batchResp FileBatchGetResponse reqMap := make(map[string][]string) reqMap["file_uids"] = []string{fileInfo.FileUID} var retryCount = 1 for { if retryCount > 5 { return nil, fmt.Errorf("retry limit exceeded") } _, err = utils.RestyDefaultClient.R(). SetContext(ctx). SetHeader("cookie", config.MonicaConfig.MonicaCookie). SetBody(reqMap). SetResult(&batchResp). Post(FileGetURL) if err != nil { return nil, fmt.Errorf("batch get file failed: %v", err) } if len(batchResp.Data.Items) > 0 && batchResp.Data.Items[0].FileChunks > 0 { break } else { retryCount++ } time.Sleep(1 * time.Second) } fileInfo.FileChunks = batchResp.Data.Items[0].FileChunks fileInfo.FileTokens = batchResp.Data.Items[0].FileTokens fileInfo.URL = "" fileInfo.ObjectURL = "" // 9. 保存到缓存 imageCache.Store(cacheKey, fileInfo) return fileInfo, nil } // validateImageBytes 验证图片字节数据的格式和大小 func validateImageBytes(imageData []byte, mimeType string) (*FileInfo, error) { if len(imageData) > MaxFileSize { return nil, fmt.Errorf("file size exceeds limit: %d > %d", len(imageData), MaxFileSize) } contentType := http.DetectContentType(imageData) if !SupportedImageTypes[contentType] { return nil, fmt.Errorf("unsupported image type: %s", contentType) } // 根据MIME类型生成文件扩展名 ext := ".png" switch mimeType { case "image/jpeg": ext = ".jpg" case "image/gif": ext = ".gif" case "image/webp": ext = ".webp" } fileName := fmt.Sprintf("%s%s", uuid.New().String(), ext) return &FileInfo{ FileName: fileName, FileSize: int64(len(imageData)), FileType: contentType, }, nil }