Create types/image.go
Browse files- internal/types/image.go +211 -0
internal/types/image.go
ADDED
@@ -0,0 +1,211 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
package types
|
2 |
+
|
3 |
+
import (
|
4 |
+
"context"
|
5 |
+
"fmt"
|
6 |
+
"log"
|
7 |
+
"monica-proxy/internal/config"
|
8 |
+
"monica-proxy/internal/utils"
|
9 |
+
"net/http"
|
10 |
+
"strings"
|
11 |
+
"sync"
|
12 |
+
"time"
|
13 |
+
|
14 |
+
"github.com/cespare/xxhash/v2"
|
15 |
+
"github.com/google/uuid"
|
16 |
+
)
|
17 |
+
|
18 |
+
const MaxFileSize = 10 * 1024 * 1024 // 10MB
|
19 |
+
|
20 |
+
var imageCache sync.Map
|
21 |
+
|
22 |
+
// sampleAndHash 对base64字符串进行采样并计算xxHash
|
23 |
+
func sampleAndHash(data string) string {
|
24 |
+
// 如果数据长度小于1024,直接计算整个字符串的哈希
|
25 |
+
if len(data) <= 1024 {
|
26 |
+
return fmt.Sprintf("%x", xxhash.Sum64String(data))
|
27 |
+
}
|
28 |
+
|
29 |
+
// 采样策略:
|
30 |
+
// 1. 取前256字节
|
31 |
+
// 2. 取中间256字节
|
32 |
+
// 3. 取最后256字节
|
33 |
+
var samples []string
|
34 |
+
samples = append(samples, data[:256])
|
35 |
+
mid := len(data) / 2
|
36 |
+
samples = append(samples, data[mid-128:mid+128])
|
37 |
+
samples = append(samples, data[len(data)-256:])
|
38 |
+
|
39 |
+
// 将采样数据拼接后计算哈希
|
40 |
+
return fmt.Sprintf("%x", xxhash.Sum64String(strings.Join(samples, "")))
|
41 |
+
}
|
42 |
+
|
43 |
+
// UploadBase64Image 上传base64编码的图片到Monica
|
44 |
+
func UploadBase64Image(ctx context.Context, base64Data string) (*FileInfo, error) {
|
45 |
+
// 1. 生成缓存key
|
46 |
+
cacheKey := sampleAndHash(base64Data)
|
47 |
+
|
48 |
+
// 2. 检查缓存
|
49 |
+
if value, exists := imageCache.Load(cacheKey); exists {
|
50 |
+
return value.(*FileInfo), nil
|
51 |
+
}
|
52 |
+
|
53 |
+
// 3. 解析base64数据
|
54 |
+
// 移除 "data:image/png;base64," 这样的前缀
|
55 |
+
parts := strings.Split(base64Data, ",")
|
56 |
+
if len(parts) != 2 {
|
57 |
+
return nil, fmt.Errorf("invalid base64 image format")
|
58 |
+
}
|
59 |
+
|
60 |
+
// 获取图片类型
|
61 |
+
mimeType := strings.TrimSuffix(strings.TrimPrefix(parts[0], "data:"), ";base64")
|
62 |
+
if !strings.HasPrefix(mimeType, "image/") {
|
63 |
+
return nil, fmt.Errorf("invalid image mime type: %s", mimeType)
|
64 |
+
}
|
65 |
+
|
66 |
+
// 解码base64数据
|
67 |
+
imageData, err := utils.Base64Decode(parts[1])
|
68 |
+
if err != nil {
|
69 |
+
return nil, fmt.Errorf("decode base64 failed: %v", err)
|
70 |
+
}
|
71 |
+
|
72 |
+
// 4. 验证图片格式和大小
|
73 |
+
fileInfo, err := validateImageBytes(imageData, mimeType)
|
74 |
+
if err != nil {
|
75 |
+
return nil, fmt.Errorf("validate image failed: %v", err)
|
76 |
+
}
|
77 |
+
log.Printf("file info: %+v", fileInfo)
|
78 |
+
|
79 |
+
// 5. 获取预签名URL
|
80 |
+
preSignReq := &PreSignRequest{
|
81 |
+
FilenameList: []string{fileInfo.FileName},
|
82 |
+
Module: ImageModule,
|
83 |
+
Location: ImageLocation,
|
84 |
+
ObjID: uuid.New().String(),
|
85 |
+
}
|
86 |
+
|
87 |
+
var preSignResp PreSignResponse
|
88 |
+
_, err = utils.RestyDefaultClient.R().
|
89 |
+
SetContext(ctx).
|
90 |
+
SetHeader("cookie", config.MonicaConfig.MonicaCookie).
|
91 |
+
SetBody(preSignReq).
|
92 |
+
SetResult(&preSignResp).
|
93 |
+
Post(PreSignURL)
|
94 |
+
|
95 |
+
if err != nil {
|
96 |
+
return nil, fmt.Errorf("get pre-sign url failed: %v", err)
|
97 |
+
}
|
98 |
+
|
99 |
+
if len(preSignResp.Data.PreSignURLList) == 0 || len(preSignResp.Data.ObjectURLList) == 0 {
|
100 |
+
return nil, fmt.Errorf("no pre-sign url or object url returned")
|
101 |
+
}
|
102 |
+
log.Printf("preSign info: %+v", preSignResp)
|
103 |
+
|
104 |
+
// 6. 上传图片数据
|
105 |
+
_, err = utils.RestyDefaultClient.R().
|
106 |
+
SetContext(ctx).
|
107 |
+
SetHeader("Content-Type", fileInfo.FileType).
|
108 |
+
SetBody(imageData).
|
109 |
+
Put(preSignResp.Data.PreSignURLList[0])
|
110 |
+
|
111 |
+
if err != nil {
|
112 |
+
return nil, fmt.Errorf("upload file failed: %v", err)
|
113 |
+
}
|
114 |
+
|
115 |
+
// 7. 创建文件对象
|
116 |
+
fileInfo.ObjectURL = preSignResp.Data.ObjectURLList[0]
|
117 |
+
uploadReq := &FileUploadRequest{
|
118 |
+
Data: []FileInfo{*fileInfo},
|
119 |
+
}
|
120 |
+
|
121 |
+
var uploadResp FileUploadResponse
|
122 |
+
_, err = utils.RestyDefaultClient.R().
|
123 |
+
SetContext(ctx).
|
124 |
+
SetHeader("cookie", config.MonicaConfig.MonicaCookie).
|
125 |
+
SetBody(uploadReq).
|
126 |
+
SetResult(&uploadResp).
|
127 |
+
Post(FileUploadURL)
|
128 |
+
|
129 |
+
if err != nil {
|
130 |
+
return nil, fmt.Errorf("create file object failed: %v", err)
|
131 |
+
}
|
132 |
+
log.Printf("uploadResp: %+v", uploadResp)
|
133 |
+
if len(uploadResp.Data.Items) > 0 {
|
134 |
+
fileInfo.FileName = uploadResp.Data.Items[0].FileName
|
135 |
+
fileInfo.FileType = uploadResp.Data.Items[0].FileType
|
136 |
+
fileInfo.FileSize = uploadResp.Data.Items[0].FileSize
|
137 |
+
fileInfo.FileUID = uploadResp.Data.Items[0].FileUID
|
138 |
+
fileInfo.FileExt = uploadResp.Data.Items[0].FileType
|
139 |
+
fileInfo.FileTokens = uploadResp.Data.Items[0].FileTokens
|
140 |
+
fileInfo.FileChunks = uploadResp.Data.Items[0].FileChunks
|
141 |
+
}
|
142 |
+
|
143 |
+
fileInfo.UseFullText = true
|
144 |
+
fileInfo.FileURL = preSignResp.Data.CDNURLList[0]
|
145 |
+
|
146 |
+
// 8. 获取文件llm读取结果知道有返回
|
147 |
+
var batchResp FileBatchGetResponse
|
148 |
+
reqMap := make(map[string][]string)
|
149 |
+
reqMap["file_uids"] = []string{fileInfo.FileUID}
|
150 |
+
var retryCount = 1
|
151 |
+
for {
|
152 |
+
if retryCount > 5 {
|
153 |
+
return nil, fmt.Errorf("retry limit exceeded")
|
154 |
+
}
|
155 |
+
_, err = utils.RestyDefaultClient.R().
|
156 |
+
SetContext(ctx).
|
157 |
+
SetHeader("cookie", config.MonicaConfig.MonicaCookie).
|
158 |
+
SetBody(reqMap).
|
159 |
+
SetResult(&batchResp).
|
160 |
+
Post(FileGetURL)
|
161 |
+
if err != nil {
|
162 |
+
return nil, fmt.Errorf("batch get file failed: %v", err)
|
163 |
+
}
|
164 |
+
if len(batchResp.Data.Items) > 0 && batchResp.Data.Items[0].FileChunks > 0 {
|
165 |
+
break
|
166 |
+
} else {
|
167 |
+
retryCount++
|
168 |
+
}
|
169 |
+
time.Sleep(1 * time.Second)
|
170 |
+
}
|
171 |
+
fileInfo.FileChunks = batchResp.Data.Items[0].FileChunks
|
172 |
+
fileInfo.FileTokens = batchResp.Data.Items[0].FileTokens
|
173 |
+
fileInfo.URL = ""
|
174 |
+
fileInfo.ObjectURL = ""
|
175 |
+
|
176 |
+
// 9. 保存到缓存
|
177 |
+
imageCache.Store(cacheKey, fileInfo)
|
178 |
+
|
179 |
+
return fileInfo, nil
|
180 |
+
}
|
181 |
+
|
182 |
+
// validateImageBytes 验证图片字节数据的格式和大小
|
183 |
+
func validateImageBytes(imageData []byte, mimeType string) (*FileInfo, error) {
|
184 |
+
if len(imageData) > MaxFileSize {
|
185 |
+
return nil, fmt.Errorf("file size exceeds limit: %d > %d", len(imageData), MaxFileSize)
|
186 |
+
}
|
187 |
+
|
188 |
+
contentType := http.DetectContentType(imageData)
|
189 |
+
if !SupportedImageTypes[contentType] {
|
190 |
+
return nil, fmt.Errorf("unsupported image type: %s", contentType)
|
191 |
+
}
|
192 |
+
|
193 |
+
// 根据MIME类型生成文件扩展名
|
194 |
+
ext := ".png"
|
195 |
+
switch mimeType {
|
196 |
+
case "image/jpeg":
|
197 |
+
ext = ".jpg"
|
198 |
+
case "image/gif":
|
199 |
+
ext = ".gif"
|
200 |
+
case "image/webp":
|
201 |
+
ext = ".webp"
|
202 |
+
}
|
203 |
+
|
204 |
+
fileName := fmt.Sprintf("%s%s", uuid.New().String(), ext)
|
205 |
+
|
206 |
+
return &FileInfo{
|
207 |
+
FileName: fileName,
|
208 |
+
FileSize: int64(len(imageData)),
|
209 |
+
FileType: contentType,
|
210 |
+
}, nil
|
211 |
+
}
|