nomid2 commited on
Commit
453b505
·
verified ·
1 Parent(s): b3784bd

Upload 4 files

Browse files
Files changed (3) hide show
  1. src/lib/imageProcessor.js +208 -0
  2. src/lib/logger.js +296 -0
  3. src/lib/uploader.js +159 -146
src/lib/imageProcessor.js ADDED
@@ -0,0 +1,208 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const sharp = require('sharp')
2
+
3
+ /**
4
+ * 图片处理工具类
5
+ * 专门处理长图检测、智能切割和重叠处理
6
+ */
7
+ class ImageProcessor {
8
+ constructor() {
9
+ // 长图阈值:高度超过宽度*4倍
10
+ this.LONG_IMAGE_RATIO = 4
11
+ // 重叠像素范围
12
+ this.OVERLAP_MIN = 50
13
+ this.OVERLAP_MAX = 100
14
+ // 默认重叠像素
15
+ this.DEFAULT_OVERLAP = 75
16
+ }
17
+
18
+ /**
19
+ * 检测是否为长图
20
+ * @param {Buffer} imageBuffer - 图片缓冲区
21
+ * @returns {Promise<{isLongImage: boolean, width: number, height: number}>}
22
+ */
23
+ async detectLongImage(imageBuffer) {
24
+ try {
25
+ const metadata = await sharp(imageBuffer).metadata()
26
+ const { width, height } = metadata
27
+
28
+ if (!width || !height) {
29
+ throw new Error('无法获取图片尺寸信息')
30
+ }
31
+
32
+ const isLongImage = height > width * this.LONG_IMAGE_RATIO
33
+
34
+ return {
35
+ isLongImage,
36
+ width,
37
+ height,
38
+ ratio: height / width
39
+ }
40
+ } catch (error) {
41
+ throw new Error(`图片尺寸检测失败: ${error.message}`)
42
+ }
43
+ }
44
+
45
+ /**
46
+ * 计算切割参数
47
+ * @param {number} width - 图片宽度
48
+ * @param {number} height - 图片高度
49
+ * @param {number} overlap - 重叠像素数
50
+ * @returns {Array<{top: number, left: number, width: number, height: number}>}
51
+ */
52
+ calculateCropRegions(width, height, overlap = this.DEFAULT_OVERLAP) {
53
+ const regions = []
54
+ const maxHeight = width * this.LONG_IMAGE_RATIO // 每个片段的最大高度
55
+
56
+ let currentTop = 0
57
+ let segmentIndex = 0
58
+
59
+ while (currentTop < height) {
60
+ const remainingHeight = height - currentTop
61
+ let segmentHeight = Math.min(maxHeight, remainingHeight)
62
+
63
+ // 如果不是第一个片段,需要向上扩展重叠区域
64
+ if (segmentIndex > 0) {
65
+ currentTop = Math.max(0, currentTop - overlap)
66
+ segmentHeight = Math.min(maxHeight + overlap, height - currentTop)
67
+ }
68
+
69
+ // 如果不是最后一个片段,需要向下扩展重叠区域
70
+ if (currentTop + segmentHeight < height) {
71
+ segmentHeight = Math.min(segmentHeight + overlap, height - currentTop)
72
+ }
73
+
74
+ regions.push({
75
+ top: currentTop,
76
+ left: 0,
77
+ width: width,
78
+ height: segmentHeight,
79
+ segmentIndex: segmentIndex,
80
+ isFirst: segmentIndex === 0,
81
+ isLast: currentTop + segmentHeight >= height
82
+ })
83
+
84
+ // 移动到下一个片段的起始位置(考虑重叠)
85
+ currentTop += (segmentIndex === 0 ? maxHeight : maxHeight - overlap)
86
+ segmentIndex++
87
+ }
88
+
89
+ return regions
90
+ }
91
+
92
+ /**
93
+ * 切割长图为多个片段
94
+ * @param {Buffer} imageBuffer - 原始图片缓冲区
95
+ * @param {number} overlap - 重叠像素数
96
+ * @returns {Promise<Array<{buffer: Buffer, metadata: Object}>>}
97
+ */
98
+ async cropLongImage(imageBuffer, overlap = this.DEFAULT_OVERLAP) {
99
+ try {
100
+ const detection = await this.detectLongImage(imageBuffer)
101
+
102
+ if (!detection.isLongImage) {
103
+ // 不是长图,返回原图
104
+ return [{
105
+ buffer: imageBuffer,
106
+ metadata: {
107
+ segmentIndex: 0,
108
+ totalSegments: 1,
109
+ isFirst: true,
110
+ isLast: true,
111
+ originalWidth: detection.width,
112
+ originalHeight: detection.height,
113
+ segmentWidth: detection.width,
114
+ segmentHeight: detection.height
115
+ }
116
+ }]
117
+ }
118
+
119
+ const { width, height } = detection
120
+ const regions = this.calculateCropRegions(width, height, overlap)
121
+ const segments = []
122
+
123
+ console.log(`检测到长图 ${width}x${height},将切割为 ${regions.length} 个片段`)
124
+
125
+ for (const region of regions) {
126
+ const segmentBuffer = await sharp(imageBuffer)
127
+ .extract({
128
+ left: region.left,
129
+ top: region.top,
130
+ width: region.width,
131
+ height: region.height
132
+ })
133
+ .png() // 统一输出为PNG格式
134
+ .toBuffer()
135
+
136
+ segments.push({
137
+ buffer: segmentBuffer,
138
+ metadata: {
139
+ segmentIndex: region.segmentIndex,
140
+ totalSegments: regions.length,
141
+ isFirst: region.isFirst,
142
+ isLast: region.isLast,
143
+ originalWidth: width,
144
+ originalHeight: height,
145
+ segmentWidth: region.width,
146
+ segmentHeight: region.height,
147
+ cropRegion: {
148
+ top: region.top,
149
+ left: region.left,
150
+ width: region.width,
151
+ height: region.height
152
+ }
153
+ }
154
+ })
155
+
156
+ console.log(`生成片段 ${region.segmentIndex + 1}/${regions.length}: ${region.width}x${region.height} (top: ${region.top})`)
157
+ }
158
+
159
+ return segments
160
+ } catch (error) {
161
+ throw new Error(`长图切割失败: ${error.message}`)
162
+ }
163
+ }
164
+
165
+ /**
166
+ * 生成片段文件名
167
+ * @param {string} originalName - 原始文件名
168
+ * @param {number} segmentIndex - 片段索引
169
+ * @param {number} totalSegments - 总片段数
170
+ * @returns {string}
171
+ */
172
+ generateSegmentName(originalName, segmentIndex, totalSegments) {
173
+ const nameWithoutExt = originalName.replace(/\.[^/.]+$/, '')
174
+ const ext = originalName.includes('.') ? originalName.split('.').pop() : 'png'
175
+
176
+ return `${nameWithoutExt}_part${segmentIndex + 1}of${totalSegments}.${ext}`
177
+ }
178
+
179
+ /**
180
+ * 获取处理统计信息
181
+ * @param {Array} segments - 切割后的片段数组
182
+ * @returns {Object}
183
+ */
184
+ getProcessingStats(segments) {
185
+ if (!segments || segments.length === 0) {
186
+ return { totalSegments: 0, isLongImage: false }
187
+ }
188
+
189
+ const firstSegment = segments[0]
190
+ const metadata = firstSegment.metadata
191
+
192
+ return {
193
+ totalSegments: segments.length,
194
+ isLongImage: segments.length > 1,
195
+ originalDimensions: {
196
+ width: metadata.originalWidth,
197
+ height: metadata.originalHeight
198
+ },
199
+ segmentDimensions: segments.map(segment => ({
200
+ width: segment.metadata.segmentWidth,
201
+ height: segment.metadata.segmentHeight,
202
+ index: segment.metadata.segmentIndex
203
+ }))
204
+ }
205
+ }
206
+ }
207
+
208
+ module.exports = new ImageProcessor()
src/lib/logger.js ADDED
@@ -0,0 +1,296 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * 日志系统模块
3
+ * 提供完整的请求追踪、性能监控和错误记录功能
4
+ */
5
+ class Logger {
6
+ constructor() {
7
+ this.requestCounter = 0
8
+ this.activeRequests = new Map()
9
+ }
10
+
11
+ /**
12
+ * 生成请求ID
13
+ * @returns {string}
14
+ */
15
+ generateRequestId() {
16
+ this.requestCounter++
17
+ return `req_${Date.now()}_${this.requestCounter.toString().padStart(4, '0')}`
18
+ }
19
+
20
+ /**
21
+ * 格式化时间戳
22
+ * @returns {string}
23
+ */
24
+ getTimestamp() {
25
+ return new Date().toISOString()
26
+ }
27
+
28
+ /**
29
+ * 记录请求开始
30
+ * @param {string} method - HTTP方法
31
+ * @param {string} url - 请求URL
32
+ * @param {Object} headers - 请求头
33
+ * @param {Object} body - 请求体(部分信息)
34
+ * @returns {string} requestId
35
+ */
36
+ logRequestStart(method, url, headers = {}, body = {}) {
37
+ const requestId = this.generateRequestId()
38
+ const timestamp = this.getTimestamp()
39
+
40
+ // 提取关键信息,避免记录敏感数据
41
+ const logData = {
42
+ requestId,
43
+ timestamp,
44
+ method,
45
+ url,
46
+ userAgent: headers['user-agent'] || 'unknown',
47
+ contentType: headers['content-type'] || 'unknown',
48
+ contentLength: headers['content-length'] || 'unknown',
49
+ model: body.model || 'unknown',
50
+ messageCount: Array.isArray(body.messages) ? body.messages.length : 0,
51
+ hasImages: this.detectImages(body.messages),
52
+ isStream: body.stream === true
53
+ }
54
+
55
+ // 记录到活跃请求映射
56
+ this.activeRequests.set(requestId, {
57
+ startTime: Date.now(),
58
+ ...logData
59
+ })
60
+
61
+ console.log(`[REQUEST_START] ${JSON.stringify(logData)}`)
62
+ return requestId
63
+ }
64
+
65
+ /**
66
+ * 记录请求结束
67
+ * @param {string} requestId - 请求ID
68
+ * @param {number} statusCode - 响应状态码
69
+ * @param {Object} responseInfo - 响应信息
70
+ */
71
+ logRequestEnd(requestId, statusCode, responseInfo = {}) {
72
+ const timestamp = this.getTimestamp()
73
+ const activeRequest = this.activeRequests.get(requestId)
74
+
75
+ if (!activeRequest) {
76
+ console.warn(`[REQUEST_END] 未找到请求ID: ${requestId}`)
77
+ return
78
+ }
79
+
80
+ const duration = Date.now() - activeRequest.startTime
81
+
82
+ const logData = {
83
+ requestId,
84
+ timestamp,
85
+ duration: `${duration}ms`,
86
+ statusCode,
87
+ success: statusCode >= 200 && statusCode < 300,
88
+ model: activeRequest.model,
89
+ isStream: activeRequest.isStream,
90
+ ...responseInfo
91
+ }
92
+
93
+ console.log(`[REQUEST_END] ${JSON.stringify(logData)}`)
94
+
95
+ // 清理活跃请求
96
+ this.activeRequests.delete(requestId)
97
+ }
98
+
99
+ /**
100
+ * 记录图片处理开始
101
+ * @param {string} requestId - 请求ID
102
+ * @param {number} imageCount - 图片数量
103
+ */
104
+ logImageProcessingStart(requestId, imageCount) {
105
+ const timestamp = this.getTimestamp()
106
+ const logData = {
107
+ requestId,
108
+ timestamp,
109
+ event: 'IMAGE_PROCESSING_START',
110
+ imageCount
111
+ }
112
+
113
+ console.log(`[IMAGE_PROCESSING] ${JSON.stringify(logData)}`)
114
+ }
115
+
116
+ /**
117
+ * 记录长图检测结果
118
+ * @param {string} requestId - 请求ID
119
+ * @param {number} imageIndex - 图片索引
120
+ * @param {Object} detection - 检测结果
121
+ */
122
+ logLongImageDetection(requestId, imageIndex, detection) {
123
+ const timestamp = this.getTimestamp()
124
+ const logData = {
125
+ requestId,
126
+ timestamp,
127
+ event: 'LONG_IMAGE_DETECTION',
128
+ imageIndex,
129
+ isLongImage: detection.isLongImage,
130
+ dimensions: `${detection.width}x${detection.height}`,
131
+ ratio: detection.ratio?.toFixed(2)
132
+ }
133
+
134
+ console.log(`[IMAGE_PROCESSING] ${JSON.stringify(logData)}`)
135
+ }
136
+
137
+ /**
138
+ * 记录图片切割结果
139
+ * @param {string} requestId - 请求ID
140
+ * @param {number} imageIndex - 图片索引
141
+ * @param {Object} cropResult - 切割结果
142
+ */
143
+ logImageCropping(requestId, imageIndex, cropResult) {
144
+ const timestamp = this.getTimestamp()
145
+ const stats = cropResult.stats || {}
146
+
147
+ const logData = {
148
+ requestId,
149
+ timestamp,
150
+ event: 'IMAGE_CROPPING',
151
+ imageIndex,
152
+ totalSegments: stats.totalSegments || 0,
153
+ originalDimensions: stats.originalDimensions,
154
+ segmentCount: cropResult.segments?.length || 0
155
+ }
156
+
157
+ console.log(`[IMAGE_PROCESSING] ${JSON.stringify(logData)}`)
158
+ }
159
+
160
+ /**
161
+ * 记录图片上传结果
162
+ * @param {string} requestId - 请求ID
163
+ * @param {number} imageIndex - 图片索引
164
+ * @param {number} segmentIndex - 片段索引(如果是长图)
165
+ * @param {boolean} success - 是否成功
166
+ * @param {string} url - 上传后的URL
167
+ * @param {string} error - 错误信息
168
+ */
169
+ logImageUpload(requestId, imageIndex, segmentIndex, success, url = null, error = null) {
170
+ const timestamp = this.getTimestamp()
171
+ const logData = {
172
+ requestId,
173
+ timestamp,
174
+ event: 'IMAGE_UPLOAD',
175
+ imageIndex,
176
+ segmentIndex,
177
+ success,
178
+ url: success ? url : null,
179
+ error: success ? null : error
180
+ }
181
+
182
+ console.log(`[IMAGE_PROCESSING] ${JSON.stringify(logData)}`)
183
+ }
184
+
185
+ /**
186
+ * 记录模型调用开始
187
+ * @param {string} requestId - 请求ID
188
+ * @param {string} model - 模型名称
189
+ * @param {string} mammouthModel - Mammouth平台模型名称
190
+ */
191
+ logModelCallStart(requestId, model, mammouthModel) {
192
+ const timestamp = this.getTimestamp()
193
+ const logData = {
194
+ requestId,
195
+ timestamp,
196
+ event: 'MODEL_CALL_START',
197
+ requestedModel: model,
198
+ mammouthModel,
199
+ }
200
+
201
+ console.log(`[MODEL_CALL] ${JSON.stringify(logData)}`)
202
+ }
203
+
204
+ /**
205
+ * 记录模型调用结果
206
+ * @param {string} requestId - 请求ID
207
+ * @param {boolean} success - 是否成功
208
+ * @param {string} error - 错误信息
209
+ * @param {number} duration - 调用耗时
210
+ */
211
+ logModelCallEnd(requestId, success, error = null, duration = null) {
212
+ const timestamp = this.getTimestamp()
213
+ const logData = {
214
+ requestId,
215
+ timestamp,
216
+ event: 'MODEL_CALL_END',
217
+ success,
218
+ error: success ? null : error,
219
+ duration: duration ? `${duration}ms` : null
220
+ }
221
+
222
+ console.log(`[MODEL_CALL] ${JSON.stringify(logData)}`)
223
+ }
224
+
225
+ /**
226
+ * 记录错误信息
227
+ * @param {string} requestId - 请求ID
228
+ * @param {string} errorType - 错误类型
229
+ * @param {string} errorMessage - 错误消息
230
+ * @param {Object} errorDetails - 错误详情
231
+ */
232
+ logError(requestId, errorType, errorMessage, errorDetails = {}) {
233
+ const timestamp = this.getTimestamp()
234
+ const logData = {
235
+ requestId,
236
+ timestamp,
237
+ event: 'ERROR',
238
+ errorType,
239
+ errorMessage,
240
+ ...errorDetails
241
+ }
242
+
243
+ console.error(`[ERROR] ${JSON.stringify(logData)}`)
244
+ }
245
+
246
+ /**
247
+ * 记录性能指标
248
+ * @param {string} requestId - 请求ID
249
+ * @param {Object} metrics - 性能指标
250
+ */
251
+ logPerformanceMetrics(requestId, metrics) {
252
+ const timestamp = this.getTimestamp()
253
+ const logData = {
254
+ requestId,
255
+ timestamp,
256
+ event: 'PERFORMANCE_METRICS',
257
+ ...metrics
258
+ }
259
+
260
+ console.log(`[PERFORMANCE] ${JSON.stringify(logData)}`)
261
+ }
262
+
263
+ /**
264
+ * 检测消息中是否包含图片
265
+ * @param {Array} messages - 消息数组
266
+ * @returns {boolean}
267
+ */
268
+ detectImages(messages) {
269
+ if (!Array.isArray(messages)) return false
270
+
271
+ return messages.some(message => {
272
+ if (Array.isArray(message.content)) {
273
+ return message.content.some(part => part.type === 'image_url')
274
+ }
275
+ return false
276
+ })
277
+ }
278
+
279
+ /**
280
+ * 获取活跃请求统计
281
+ * @returns {Object}
282
+ */
283
+ getActiveRequestsStats() {
284
+ return {
285
+ count: this.activeRequests.size,
286
+ requests: Array.from(this.activeRequests.entries()).map(([id, req]) => ({
287
+ requestId: id,
288
+ duration: Date.now() - req.startTime,
289
+ model: req.model,
290
+ isStream: req.isStream
291
+ }))
292
+ }
293
+ }
294
+ }
295
+
296
+ module.exports = new Logger()
src/lib/uploader.js CHANGED
@@ -2,31 +2,13 @@ const axios = require('axios')
2
  const FormData = require('form-data')
3
  const crypto = require('crypto')
4
  const accountManager = require('./manager')
5
- const imageSlicer = require('./imageSlicer')
6
- const imgbbUploader = require('./imgbbUploader')
7
- const { IMAGE_CONFIG, IMGBB_API_KEY } = require('../config')
8
 
9
  class ImageUploader {
10
  constructor() {
11
  this.uploadUrl = 'https://mammouth.ai/api/attachments/saveFile'
12
  this.imageCache = new Map()
13
-
14
- // 初始化配置
15
- this.useImgbb = IMAGE_CONFIG.USE_IMGBB
16
- this.uploadStrategy = IMAGE_CONFIG.UPLOAD_STRATEGY
17
-
18
- // 设置图片裁切配置
19
- imageSlicer.setConfig({
20
- maxSize: IMAGE_CONFIG.MAX_SIZE,
21
- overlapPixels: IMAGE_CONFIG.OVERLAP_PIXELS
22
- })
23
-
24
- // 设置Imgbb API密钥
25
- if (IMGBB_API_KEY) {
26
- imgbbUploader.setApiKey(IMGBB_API_KEY)
27
- }
28
-
29
- console.log(`图片上传器初始化: 策略=${this.uploadStrategy}, Imgbb=${this.useImgbb}`)
30
  }
31
 
32
 
@@ -34,122 +16,7 @@ class ImageUploader {
34
  return crypto.createHash('sha256').update(buffer).digest('hex')
35
  }
36
 
37
- /**
38
- * 智能处理图片:裁切大图并上传到图床
39
- * @param {Buffer} imageBuffer - 图片缓冲区
40
- * @param {string} imageName - 图片名称
41
- * @returns {Promise<Array>} 图片URL数组(按顺序)
42
- */
43
- async processImageIntelligently(imageBuffer, imageName) {
44
- const timestamp = new Date().toISOString()
45
- const imageId = imageName.substring(0, 8) + '...'
46
-
47
- try {
48
- console.log(`[${timestamp}] 🖼️ [${imageId}] 开始智能处理图片: ${imageName}`)
49
- console.log(`[${timestamp}] 📏 [${imageId}] 图片大小: ${(imageBuffer.length / 1024 / 1024).toFixed(2)} MB`)
50
-
51
- // 1. 检查并裁切图片
52
- const slices = await imageSlicer.sliceImage(imageBuffer, imageName)
53
- console.log(`[${timestamp}] ✂️ [${imageId}] 图片裁切完成,共 ${slices.length} 片`)
54
-
55
- // 检查是否为长图并记录特殊信息
56
- if (slices.length > 0 && slices[0].isLongImage) {
57
- console.log(`[${timestamp}] 📏 [${imageId}] 检测到长图,使用优化裁切策略`)
58
- console.log(`[${timestamp}] 📊 [${imageId}] 长图信息: ${slices[0].size?.width}x${slices[0].size?.height}`)
59
- }
60
-
61
- // 2. 根据配置选择上传方式
62
- // 优先尝试Mammouth原生上传,失败时使用Imgbb
63
- if (this.useImgbb) {
64
- // 先尝试Mammouth原生上传
65
- try {
66
- console.log(`[${timestamp}] 🏠 [${imageId}] 尝试Mammouth原生上传`)
67
- const mammouthUrls = []
68
- for (const slice of slices) {
69
- try {
70
- const url = await this.uploadImageToMammouth(slice.buffer, slice.name)
71
- mammouthUrls.push(url)
72
- console.log(`[${timestamp}] ✅ [${imageId}] 切片 ${slice.name} Mammouth上传成功`)
73
- } catch (error) {
74
- console.error(`[${timestamp}] ❌ [${imageId}] 切片 ${slice.name} Mammouth上传失败:`, error.message)
75
- throw error // 如果Mammouth失败,抛出错误以尝试Imgbb
76
- }
77
- }
78
-
79
- if (mammouthUrls.length === slices.length) {
80
- console.log(`[${timestamp}] ✅ [${imageId}] Mammouth原生上传完成,成功 ${mammouthUrls.length} 片`)
81
- return mammouthUrls
82
- }
83
- } catch (error) {
84
- console.log(`[${timestamp}] ⚠️ [${imageId}] Mammouth原生上传失败,尝试Imgbb备用方案`)
85
- }
86
- // 使用Imgbb图床作为备用方案
87
- console.log(`[${timestamp}] 🌐 [${imageId}] 使用Imgbb图床备用方案`)
88
- const uploadResults = await imgbbUploader.uploadSlices(slices)
89
-
90
- // 提取成功上传的URL,保持顺序
91
- const imageUrls = uploadResults
92
- .filter(result => result.uploadSuccess)
93
- .map(result => result.url)
94
-
95
- if (imageUrls.length === 0) {
96
- console.error(`[${timestamp}] ❌ [${imageId}] 所有图片切片上传失败`)
97
- throw new Error('所有图片切片上传失败')
98
- }
99
-
100
- if (imageUrls.length < slices.length) {
101
- console.warn(`[${timestamp}] ⚠️ [${imageId}] 部分切片上传失败: ${imageUrls.length}/${slices.length}`)
102
- }
103
-
104
- console.log(`[${timestamp}] ✅ [${imageId}] Imgbb上传完成,成功 ${imageUrls.length} 片`)
105
-
106
- // 验证上传的图片URL是否可访问
107
- if (imageUrls.length > 0) {
108
- console.log(`[${timestamp}] 🔍 [${imageId}] 验证图片URL可访问性`)
109
- for (let i = 0; i < Math.min(imageUrls.length, 3); i++) { // 最多验证前3个
110
- try {
111
- const testResponse = await axios.head(imageUrls[i], { timeout: 5000 })
112
- console.log(`[${timestamp}] ✅ [${imageId}] 图片${i+1}可访问: ${testResponse.status}`)
113
- } catch (error) {
114
- console.error(`[${timestamp}] ❌ [${imageId}] 图片${i+1}不可访问: ${error.message}`)
115
- }
116
- }
117
- }
118
-
119
- return imageUrls
120
-
121
- } else {
122
- // 使用原有的Mammouth平台上传
123
- console.log(`[${timestamp}] 🏠 [${imageId}] 开始上传到Mammouth平台`)
124
- const imageUrls = []
125
- for (const slice of slices) {
126
- try {
127
- const url = await this.uploadImageToMammouth(slice.buffer, slice.name)
128
- imageUrls.push(url)
129
- console.log(`[${timestamp}] ✅ [${imageId}] 切片 ${slice.name} 上传成功`)
130
- } catch (error) {
131
- console.error(`[${timestamp}] ❌ [${imageId}] 切片 ${slice.name} 上传到Mammouth失败:`, error.message)
132
- // 继续处理其他切片
133
- }
134
- }
135
-
136
- if (imageUrls.length === 0) {
137
- console.error(`[${timestamp}] ❌ [${imageId}] 所有图片切片上传失败`)
138
- throw new Error('所有图片切片上传失败')
139
- }
140
-
141
- console.log(`[${timestamp}] ✅ [${imageId}] Mammouth上传完成,成功 ${imageUrls.length} 片`)
142
- return imageUrls
143
- }
144
-
145
- } catch (error) {
146
- console.error(`[${timestamp}] ❌ [${imageId}] 智能图片处理失败:`, error.message)
147
- console.error(`[${timestamp}] 🔍 [${imageId}] 错误堆栈:`, error.stack?.split('\n')[0] || 'No stack trace')
148
- throw error
149
- }
150
- }
151
-
152
- async uploadImageToMammouth(imageBuffer, imageName) {
153
  // 确保参数有效
154
  if (!imageBuffer || !Buffer.isBuffer(imageBuffer)) {
155
  throw new Error('无效的图片数据')
@@ -213,7 +80,7 @@ class ImageUploader {
213
  if (error.response && error.response.status === 403) {
214
  accountManager.markAsUnavailable(cookieValue)
215
  // 递归尝试使用新账号
216
- return this.uploadImageToMammouth(imageBuffer, imageName)
217
  }
218
 
219
  throw new Error(`图片上传失败: ${error.message}`)
@@ -224,21 +91,21 @@ class ImageUploader {
224
  // 处理base64字符串,移除可能的前缀
225
  const base64Data = base64String.replace(/^data:image\/\w+;base64,/, '')
226
  const buffer = Buffer.from(base64Data, 'base64')
227
-
228
  // 如果提供了前缀,则从中提取图片类型
229
  let fileExt = 'png'
230
  const mimeMatch = base64String.match(/^data:image\/(\w+);base64,/)
231
  if (mimeMatch && mimeMatch[1]) {
232
  fileExt = mimeMatch[1]
233
  }
234
-
235
  // 如果没有提供文件名,则使用哈希值的前8位和时间戳生成
236
  if (!imageName) {
237
  const hash = this.generateHash(buffer).substring(0, 8)
238
  imageName = `image_${hash}_${Date.now()}.${fileExt}`
239
  }
240
-
241
- return this.processImageIntelligently(buffer, imageName)
242
  }
243
 
244
  async uploadFromUrl(imageUrl, imageName) {
@@ -247,24 +114,24 @@ class ImageUploader {
247
  const response = await axios.get(imageUrl, {
248
  responseType: 'arraybuffer'
249
  })
250
-
251
  // 从URL中提取文件扩展名
252
  let fileExt = 'png'
253
  const urlMatch = imageUrl.match(/\.([a-zA-Z0-9]+)(?:\?.*)?$/)
254
  if (urlMatch && urlMatch[1]) {
255
  fileExt = urlMatch[1].toLowerCase()
256
  }
257
-
258
  // 生成buffer并计算哈希
259
  const buffer = Buffer.from(response.data)
260
-
261
  // 如果没有提供文件名,则使用哈希值的前8位和时间戳生成
262
  if (!imageName) {
263
  const hash = this.generateHash(buffer).substring(0, 8)
264
  imageName = `image_${hash}_${Date.now()}.${fileExt}`
265
  }
266
-
267
- return this.processImageIntelligently(buffer, imageName)
268
  } catch (error) {
269
  throw new Error(`从URL获取图片失败: ${error.message}`)
270
  }
@@ -282,6 +149,152 @@ class ImageUploader {
282
  this.imageCache.clear()
283
  return { cleared: oldSize }
284
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
285
  }
286
 
287
 
 
2
  const FormData = require('form-data')
3
  const crypto = require('crypto')
4
  const accountManager = require('./manager')
5
+ const imageProcessor = require('./imageProcessor')
6
+ const logger = require('./logger')
 
7
 
8
  class ImageUploader {
9
  constructor() {
10
  this.uploadUrl = 'https://mammouth.ai/api/attachments/saveFile'
11
  this.imageCache = new Map()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
12
  }
13
 
14
 
 
16
  return crypto.createHash('sha256').update(buffer).digest('hex')
17
  }
18
 
19
+ async uploadImage(imageBuffer, imageName) {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
20
  // 确保参数有效
21
  if (!imageBuffer || !Buffer.isBuffer(imageBuffer)) {
22
  throw new Error('无效的图片数据')
 
80
  if (error.response && error.response.status === 403) {
81
  accountManager.markAsUnavailable(cookieValue)
82
  // 递归尝试使用新账号
83
+ return this.uploadImage(imageBuffer, imageName)
84
  }
85
 
86
  throw new Error(`图片上传失败: ${error.message}`)
 
91
  // 处理base64字符串,移除可能的前缀
92
  const base64Data = base64String.replace(/^data:image\/\w+;base64,/, '')
93
  const buffer = Buffer.from(base64Data, 'base64')
94
+
95
  // 如果提供了前缀,则从中提取图片类型
96
  let fileExt = 'png'
97
  const mimeMatch = base64String.match(/^data:image\/(\w+);base64,/)
98
  if (mimeMatch && mimeMatch[1]) {
99
  fileExt = mimeMatch[1]
100
  }
101
+
102
  // 如果没有提供文件名,则使用哈希值的前8位和时间戳生成
103
  if (!imageName) {
104
  const hash = this.generateHash(buffer).substring(0, 8)
105
  imageName = `image_${hash}_${Date.now()}.${fileExt}`
106
  }
107
+
108
+ return this.uploadImage(buffer, imageName)
109
  }
110
 
111
  async uploadFromUrl(imageUrl, imageName) {
 
114
  const response = await axios.get(imageUrl, {
115
  responseType: 'arraybuffer'
116
  })
117
+
118
  // 从URL中提取文件扩展名
119
  let fileExt = 'png'
120
  const urlMatch = imageUrl.match(/\.([a-zA-Z0-9]+)(?:\?.*)?$/)
121
  if (urlMatch && urlMatch[1]) {
122
  fileExt = urlMatch[1].toLowerCase()
123
  }
124
+
125
  // 生成buffer并计算哈希
126
  const buffer = Buffer.from(response.data)
127
+
128
  // 如果没有提供文件名,则使用哈希值的前8位和时间戳生成
129
  if (!imageName) {
130
  const hash = this.generateHash(buffer).substring(0, 8)
131
  imageName = `image_${hash}_${Date.now()}.${fileExt}`
132
  }
133
+
134
+ return this.uploadImage(buffer, imageName)
135
  } catch (error) {
136
  throw new Error(`从URL获取图片失败: ${error.message}`)
137
  }
 
149
  this.imageCache.clear()
150
  return { cleared: oldSize }
151
  }
152
+
153
+ /**
154
+ * 智能上传图片(支持长图处理)
155
+ * @param {Buffer} imageBuffer - 图片缓冲区
156
+ * @param {string} imageName - 图片名称
157
+ * @param {string} requestId - 请求ID(用于日志)
158
+ * @param {number} imageIndex - 图片索引(用于日志)
159
+ * @returns {Promise<Array<string>>} 上传后的URL数组
160
+ */
161
+ async uploadImageSmart(imageBuffer, imageName, requestId = null, imageIndex = 0) {
162
+ try {
163
+ // 检测是否为长图
164
+ const detection = await imageProcessor.detectLongImage(imageBuffer)
165
+
166
+ if (requestId) {
167
+ logger.logLongImageDetection(requestId, imageIndex, detection)
168
+ }
169
+
170
+ if (!detection.isLongImage) {
171
+ // 不是长图,使用原有方式上传
172
+ const url = await this.uploadImage(imageBuffer, imageName)
173
+
174
+ if (requestId) {
175
+ logger.logImageUpload(requestId, imageIndex, 0, true, url)
176
+ }
177
+
178
+ return [url]
179
+ }
180
+
181
+ // 是长图,进行切割处理
182
+ console.log(`检测到长图 ${detection.width}x${detection.height},开始切割处理`)
183
+
184
+ const segments = await imageProcessor.cropLongImage(imageBuffer)
185
+ const uploadedUrls = []
186
+
187
+ if (requestId) {
188
+ logger.logImageCropping(requestId, imageIndex, {
189
+ segments,
190
+ stats: imageProcessor.getProcessingStats(segments)
191
+ })
192
+ }
193
+
194
+ // 逐个上传切割后的片段
195
+ for (const segment of segments) {
196
+ const segmentName = imageProcessor.generateSegmentName(
197
+ imageName,
198
+ segment.metadata.segmentIndex,
199
+ segment.metadata.totalSegments
200
+ )
201
+
202
+ try {
203
+ const url = await this.uploadImage(segment.buffer, segmentName)
204
+ uploadedUrls.push(url)
205
+
206
+ if (requestId) {
207
+ logger.logImageUpload(requestId, imageIndex, segment.metadata.segmentIndex, true, url)
208
+ }
209
+
210
+ console.log(`片段 ${segment.metadata.segmentIndex + 1}/${segment.metadata.totalSegments} 上传成功: ${url}`)
211
+ } catch (error) {
212
+ if (requestId) {
213
+ logger.logImageUpload(requestId, imageIndex, segment.metadata.segmentIndex, false, null, error.message)
214
+ }
215
+ throw new Error(`片段 ${segment.metadata.segmentIndex + 1} 上传失败: ${error.message}`)
216
+ }
217
+ }
218
+
219
+ console.log(`长图切割上传完成,共 ${uploadedUrls.length} 个片段`)
220
+ return uploadedUrls
221
+
222
+ } catch (error) {
223
+ if (requestId) {
224
+ logger.logError(requestId, 'IMAGE_UPLOAD_ERROR', error.message, {
225
+ imageIndex,
226
+ imageName
227
+ })
228
+ }
229
+ throw error
230
+ }
231
+ }
232
+
233
+ /**
234
+ * 智能上传Base64图片(支持长图处理)
235
+ * @param {string} base64String - Base64字符串
236
+ * @param {string} imageName - 图片名称
237
+ * @param {string} requestId - 请求ID(用于日志)
238
+ * @param {number} imageIndex - 图片索引(用于日志)
239
+ * @returns {Promise<Array<string>>} 上传后的URL数组
240
+ */
241
+ async uploadFromBase64Smart(base64String, imageName, requestId = null, imageIndex = 0) {
242
+ // 处理base64字符串,移除可能的前缀
243
+ const base64Data = base64String.replace(/^data:image\/\w+;base64,/, '')
244
+ const buffer = Buffer.from(base64Data, 'base64')
245
+
246
+ // 如果提供了前缀,则从中提取图片类型
247
+ let fileExt = 'png'
248
+ const mimeMatch = base64String.match(/^data:image\/(\w+);base64,/)
249
+ if (mimeMatch && mimeMatch[1]) {
250
+ fileExt = mimeMatch[1]
251
+ }
252
+
253
+ // 如果没有提供文件名,则使用哈希值的前8位和时间戳生成
254
+ if (!imageName) {
255
+ const hash = this.generateHash(buffer).substring(0, 8)
256
+ imageName = `image_${hash}_${Date.now()}.${fileExt}`
257
+ }
258
+
259
+ return this.uploadImageSmart(buffer, imageName, requestId, imageIndex)
260
+ }
261
+
262
+ /**
263
+ * 智能上传URL图片(支持长图处理)
264
+ * @param {string} imageUrl - 图片URL
265
+ * @param {string} imageName - 图片名称
266
+ * @param {string} requestId - 请求ID(用于日志)
267
+ * @param {number} imageIndex - 图片索引(用于日志)
268
+ * @returns {Promise<Array<string>>} 上传后的URL数组
269
+ */
270
+ async uploadFromUrlSmart(imageUrl, imageName, requestId = null, imageIndex = 0) {
271
+ try {
272
+ // 下载图片
273
+ const response = await axios.get(imageUrl, {
274
+ responseType: 'arraybuffer'
275
+ })
276
+
277
+ // 从URL中提取文件扩展名
278
+ let fileExt = 'png'
279
+ const urlMatch = imageUrl.match(/\.([a-zA-Z0-9]+)(?:\?.*)?$/)
280
+ if (urlMatch && urlMatch[1]) {
281
+ fileExt = urlMatch[1].toLowerCase()
282
+ }
283
+
284
+ // 生成buffer并计算哈希
285
+ const buffer = Buffer.from(response.data)
286
+
287
+ // 如果没有提供文件名,则使用哈希值的前8位和时间戳生成
288
+ if (!imageName) {
289
+ const hash = this.generateHash(buffer).substring(0, 8)
290
+ imageName = `image_${hash}_${Date.now()}.${fileExt}`
291
+ }
292
+
293
+ return this.uploadImageSmart(buffer, imageName, requestId, imageIndex)
294
+ } catch (error) {
295
+ throw new Error(`从URL获取图片失败: ${error.message}`)
296
+ }
297
+ }
298
  }
299
 
300