nomid2 commited on
Commit
c666a95
·
verified ·
1 Parent(s): d2eed96

Upload 4 files

Browse files
src/lib/errorHandler.js ADDED
@@ -0,0 +1,181 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const logger = require('./logger')
2
+
3
+ /**
4
+ * 公共错误处理工具类
5
+ * 提供统一的错误处理和响应格式
6
+ */
7
+ class ErrorHandler {
8
+ /**
9
+ * 处理 API 错误响应
10
+ * @param {Object} res - Express 响应对象
11
+ * @param {Error} error - 错误对象
12
+ * @param {string} requestId - 请求ID
13
+ * @param {Object} context - 错误上下文信息
14
+ */
15
+ static handleApiError(res, error, requestId = null, context = {}) {
16
+ const errorInfo = this.parseError(error)
17
+
18
+ // 记录错误日志
19
+ if (requestId) {
20
+ logger.logError(requestId, 'API_ERROR', errorInfo.message, {
21
+ ...context,
22
+ statusCode: errorInfo.statusCode,
23
+ errorType: errorInfo.type
24
+ })
25
+ }
26
+
27
+ // 返回错误响应
28
+ res.status(errorInfo.statusCode).json({
29
+ error: {
30
+ message: errorInfo.message,
31
+ type: errorInfo.type,
32
+ code: errorInfo.code
33
+ }
34
+ })
35
+ }
36
+
37
+ /**
38
+ * 处理模型调用错误
39
+ * @param {Object} res - Express 响应对象
40
+ * @param {Error} error - 错误对象
41
+ * @param {string} requestId - 请求ID
42
+ * @param {string} model - 模型名称
43
+ * @param {boolean} isStream - 是否为流式请求
44
+ */
45
+ static handleModelError(res, error, requestId, model, isStream = false) {
46
+ const errorInfo = this.parseError(error)
47
+
48
+ logger.logError(requestId, 'MODEL_CALL_ERROR', errorInfo.message, {
49
+ model,
50
+ isStream,
51
+ statusCode: errorInfo.statusCode,
52
+ errorType: errorInfo.type
53
+ })
54
+
55
+ // 特殊处理403错误(使用限制)
56
+ if (errorInfo.statusCode === 403) {
57
+ return this.handleRateLimitError(res, requestId, model)
58
+ }
59
+
60
+ // 返回通用错误响应
61
+ res.status(errorInfo.statusCode).json({
62
+ error: {
63
+ message: errorInfo.message,
64
+ type: errorInfo.type,
65
+ code: errorInfo.code
66
+ }
67
+ })
68
+ }
69
+
70
+ /**
71
+ * 处理使用限制错误
72
+ * @param {Object} res - Express 响应对象
73
+ * @param {string} requestId - 请求ID
74
+ * @param {string} model - 模型名称
75
+ */
76
+ static handleRateLimitError(res, requestId, model) {
77
+ const errorMessage = `模型 ${model} 已达到使用限制,请稍后再试`
78
+
79
+ logger.logError(requestId, 'RATE_LIMIT_ERROR', errorMessage, {
80
+ model,
81
+ statusCode: 429
82
+ })
83
+
84
+ res.status(429).json({
85
+ error: {
86
+ message: errorMessage,
87
+ type: 'rate_limit_exceeded',
88
+ code: 'rate_limit_exceeded'
89
+ }
90
+ })
91
+ }
92
+
93
+ /**
94
+ * 处理图片上传错误
95
+ * @param {string} requestId - 请求ID
96
+ * @param {Error} error - 错误对象
97
+ * @param {Object} context - 上下文信息
98
+ */
99
+ static handleImageUploadError(requestId, error, context = {}) {
100
+ const errorInfo = this.parseError(error)
101
+
102
+ logger.logError(requestId, 'IMAGE_UPLOAD_ERROR', errorInfo.message, {
103
+ ...context,
104
+ errorType: errorInfo.type
105
+ })
106
+
107
+ throw new Error(`图片上传失败: ${errorInfo.message}`)
108
+ }
109
+
110
+ /**
111
+ * 解析错误对象,提取关键信息
112
+ * @param {Error} error - 错误对象
113
+ * @returns {Object} 解析后的错误信息
114
+ */
115
+ static parseError(error) {
116
+ let statusCode = 500
117
+ let message = '服务器内部错误'
118
+ let type = 'server_error'
119
+ let code = 'internal_error'
120
+
121
+ if (error.response) {
122
+ // Axios 错误
123
+ statusCode = error.response.status || 500
124
+ message = error.response.data?.message ||
125
+ error.response.data?.statusMessage ||
126
+ error.message ||
127
+ '请求失败'
128
+
129
+ if (statusCode === 403) {
130
+ type = 'rate_limit_exceeded'
131
+ code = 'rate_limit_exceeded'
132
+ } else if (statusCode === 401) {
133
+ type = 'authentication_error'
134
+ code = 'invalid_api_key'
135
+ } else if (statusCode >= 400 && statusCode < 500) {
136
+ type = 'client_error'
137
+ code = 'bad_request'
138
+ }
139
+ } else if (error.code) {
140
+ // 系统错误
141
+ if (error.code === 'ECONNREFUSED') {
142
+ message = '连接被拒绝'
143
+ type = 'connection_error'
144
+ code = 'connection_refused'
145
+ } else if (error.code === 'ETIMEDOUT') {
146
+ message = '请求超时'
147
+ type = 'timeout_error'
148
+ code = 'request_timeout'
149
+ statusCode = 408
150
+ }
151
+ } else {
152
+ // 普通错误
153
+ message = error.message || '未知错误'
154
+ }
155
+
156
+ return {
157
+ statusCode,
158
+ message,
159
+ type,
160
+ code
161
+ }
162
+ }
163
+
164
+ /**
165
+ * 创建标准化错误对象
166
+ * @param {string} message - 错误消息
167
+ * @param {number} statusCode - HTTP状态码
168
+ * @param {string} type - 错误类型
169
+ * @param {string} code - 错误代码
170
+ * @returns {Error} 标准化错误对象
171
+ */
172
+ static createError(message, statusCode = 500, type = 'server_error', code = 'internal_error') {
173
+ const error = new Error(message)
174
+ error.statusCode = statusCode
175
+ error.type = type
176
+ error.code = code
177
+ return error
178
+ }
179
+ }
180
+
181
+ module.exports = ErrorHandler
src/lib/imageProcessor.js CHANGED
@@ -18,13 +18,13 @@ class ImageProcessor {
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
  }
@@ -32,12 +32,13 @@ class ImageProcessor {
32
  const isLongImage = height > width * this.LONG_IMAGE_RATIO
33
  const threshold = width * this.LONG_IMAGE_RATIO
34
 
35
- console.log(`[长图检测] 图片尺寸: ${width}x${height}, 阈值: ${threshold}, 判断: ${isLongImage ? '是长图' : '非长图'}`)
36
 
37
  return {
38
  isLongImage,
39
  width,
40
  height,
 
41
  ratio: height / width,
42
  threshold
43
  }
@@ -93,6 +94,56 @@ class ImageProcessor {
93
  return regions
94
  }
95
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
96
  /**
97
  * 切割长图为多个片段
98
  * @param {Buffer} imageBuffer - 原始图片缓冲区
@@ -102,7 +153,7 @@ class ImageProcessor {
102
  async cropLongImage(imageBuffer, overlap = this.DEFAULT_OVERLAP) {
103
  try {
104
  const detection = await this.detectLongImage(imageBuffer)
105
-
106
  if (!detection.isLongImage) {
107
  // 不是长图,返回原图
108
  return [{
@@ -120,21 +171,26 @@ class ImageProcessor {
120
  }]
121
  }
122
 
123
- const { width, height } = detection
124
  const regions = this.calculateCropRegions(width, height, overlap)
125
  const segments = []
126
 
127
- console.log(`检测到长图 ${width}x${height},将切割为 ${regions.length} 个片段`)
 
 
 
128
 
129
  for (const region of regions) {
130
- const segmentBuffer = await sharp(imageBuffer)
131
  .extract({
132
  left: region.left,
133
  top: region.top,
134
  width: region.width,
135
  height: region.height
136
  })
137
- .png() // 统一输出为PNG格式
 
 
138
  .toBuffer()
139
 
140
  segments.push({
@@ -148,6 +204,8 @@ class ImageProcessor {
148
  originalHeight: height,
149
  segmentWidth: region.width,
150
  segmentHeight: region.height,
 
 
151
  cropRegion: {
152
  top: region.top,
153
  left: region.left,
 
18
  /**
19
  * 检测是否为长图
20
  * @param {Buffer} imageBuffer - 图片缓冲区
21
+ * @returns {Promise<{isLongImage: boolean, width: number, height: number, format: string}>}
22
  */
23
  async detectLongImage(imageBuffer) {
24
  try {
25
  const metadata = await sharp(imageBuffer).metadata()
26
+ const { width, height, format } = metadata
27
+
28
  if (!width || !height) {
29
  throw new Error('无法获取图片尺寸信息')
30
  }
 
32
  const isLongImage = height > width * this.LONG_IMAGE_RATIO
33
  const threshold = width * this.LONG_IMAGE_RATIO
34
 
35
+ console.log(`[长图检测] 图片尺寸: ${width}x${height} (${format}), 阈值: ${threshold}, 判断: ${isLongImage ? '是长图' : '非长图'}`)
36
 
37
  return {
38
  isLongImage,
39
  width,
40
  height,
41
+ format,
42
  ratio: height / width,
43
  threshold
44
  }
 
94
  return regions
95
  }
96
 
97
+ /**
98
+ * 根据原图格式选择最优输出格式
99
+ * @param {string} originalFormat - 原图格式
100
+ * @returns {string} 最优输出格式
101
+ */
102
+ getOptimalOutputFormat(originalFormat) {
103
+ // 格式映射表:原格式 -> 最优输出格式
104
+ const formatMap = {
105
+ 'jpeg': 'jpeg',
106
+ 'jpg': 'jpeg',
107
+ 'png': 'png',
108
+ 'webp': 'webp',
109
+ 'gif': 'png', // GIF转PNG保持透明度
110
+ 'bmp': 'png', // BMP转PNG减小文件大小
111
+ 'tiff': 'png', // TIFF转PNG兼容性更好
112
+ 'svg': 'png' // SVG转PNG用于显示
113
+ }
114
+
115
+ return formatMap[originalFormat?.toLowerCase()] || 'png'
116
+ }
117
+
118
+ /**
119
+ * 应用最优格式转换
120
+ * @param {Object} sharpInstance - Sharp实例
121
+ * @param {string} format - 目标格式
122
+ * @param {Object} options - 格式选项
123
+ * @returns {Object} 配置后的Sharp实例
124
+ */
125
+ applyOptimalFormat(sharpInstance, format, options = {}) {
126
+ switch (format) {
127
+ case 'jpeg':
128
+ return sharpInstance.jpeg({
129
+ quality: options.quality || 85,
130
+ progressive: true
131
+ })
132
+ case 'png':
133
+ return sharpInstance.png({
134
+ compressionLevel: options.compressionLevel || 6,
135
+ progressive: true
136
+ })
137
+ case 'webp':
138
+ return sharpInstance.webp({
139
+ quality: options.quality || 80,
140
+ effort: 4
141
+ })
142
+ default:
143
+ return sharpInstance.png({ compressionLevel: 6 })
144
+ }
145
+ }
146
+
147
  /**
148
  * 切割长图为多个片段
149
  * @param {Buffer} imageBuffer - 原始图片缓冲区
 
153
  async cropLongImage(imageBuffer, overlap = this.DEFAULT_OVERLAP) {
154
  try {
155
  const detection = await this.detectLongImage(imageBuffer)
156
+
157
  if (!detection.isLongImage) {
158
  // 不是长图,返回原图
159
  return [{
 
171
  }]
172
  }
173
 
174
+ const { width, height, format } = detection
175
  const regions = this.calculateCropRegions(width, height, overlap)
176
  const segments = []
177
 
178
+ // 选择最优输出格式
179
+ const outputFormat = this.getOptimalOutputFormat(format)
180
+
181
+ console.log(`检测到长图 ${width}x${height} (${format}),将切割为 ${regions.length} 个片段,输出格式: ${outputFormat}`)
182
 
183
  for (const region of regions) {
184
+ const sharpInstance = sharp(imageBuffer)
185
  .extract({
186
  left: region.left,
187
  top: region.top,
188
  width: region.width,
189
  height: region.height
190
  })
191
+
192
+ // 应用最优格式
193
+ const segmentBuffer = await this.applyOptimalFormat(sharpInstance, outputFormat)
194
  .toBuffer()
195
 
196
  segments.push({
 
204
  originalHeight: height,
205
  segmentWidth: region.width,
206
  segmentHeight: region.height,
207
+ originalFormat: format,
208
+ outputFormat: outputFormat,
209
  cropRegion: {
210
  top: region.top,
211
  left: region.left,
src/lib/logger.js CHANGED
@@ -35,28 +35,11 @@ class Logger {
35
  */
36
  logRequestStart(method, url, headers = {}, body = {}) {
37
  const requestId = this.generateRequestId()
38
- const timestamp = this.getTimestamp()
39
-
40
- // 提取关键信息,避免记录敏感数据
41
- const logData = {
42
- 请求ID: requestId,
43
- 时间: timestamp,
44
- 方法: method,
45
- 路径: url,
46
- 用户代理: headers['user-agent'] || '未知',
47
- 内容类型: headers['content-type'] || '未知',
48
- 内容长度: headers['content-length'] || '未知',
49
- 模型: body.model || '未知',
50
- 消息数量: Array.isArray(body.messages) ? body.messages.length : 0,
51
- 包含图片: this.detectImages(body.messages),
52
- 流式请求: body.stream === true
53
- }
54
 
55
  // 记录到活跃请求映射
56
  this.activeRequests.set(requestId, {
57
  startTime: Date.now(),
58
  requestId,
59
- timestamp,
60
  method,
61
  url,
62
  userAgent: headers['user-agent'] || 'unknown',
@@ -68,7 +51,20 @@ class Logger {
68
  isStream: body.stream === true
69
  })
70
 
71
- console.log(`[请求开始] ${JSON.stringify(logData)}`)
 
 
 
 
 
 
 
 
 
 
 
 
 
72
  return requestId
73
  }
74
 
@@ -259,17 +255,10 @@ class Logger {
259
  * @param {Object} errorDetails - 错误详情
260
  */
261
  logError(requestId, errorType, errorMessage, errorDetails = {}) {
262
- const timestamp = this.getTimestamp()
263
- const logData = {
264
- 请求ID: requestId,
265
- 时间: timestamp,
266
- 事件: '错误',
267
- 错误类型: errorType,
268
- 错误信息: errorMessage,
269
  ...errorDetails
270
- }
271
-
272
- console.error(`[错误] ${JSON.stringify(logData)}`)
273
  }
274
 
275
  /**
 
35
  */
36
  logRequestStart(method, url, headers = {}, body = {}) {
37
  const requestId = this.generateRequestId()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
38
 
39
  // 记录到活跃请求映射
40
  this.activeRequests.set(requestId, {
41
  startTime: Date.now(),
42
  requestId,
 
43
  method,
44
  url,
45
  userAgent: headers['user-agent'] || 'unknown',
 
51
  isStream: body.stream === true
52
  })
53
 
54
+ // 使用结构化日志记录请求开始
55
+ this.log('INFO', 'REQUEST', 'REQUEST_START', '请求开始', {
56
+ requestId,
57
+ method,
58
+ url,
59
+ userAgent: headers['user-agent'] || 'unknown',
60
+ contentType: headers['content-type'] || 'unknown',
61
+ contentLength: headers['content-length'] || 'unknown',
62
+ model: body.model || 'unknown',
63
+ messageCount: Array.isArray(body.messages) ? body.messages.length : 0,
64
+ hasImages: this.detectImages(body.messages),
65
+ isStream: body.stream === true
66
+ })
67
+
68
  return requestId
69
  }
70
 
 
255
  * @param {Object} errorDetails - 错误详情
256
  */
257
  logError(requestId, errorType, errorMessage, errorDetails = {}) {
258
+ this.log('ERROR', 'APPLICATION', errorType, errorMessage, {
259
+ requestId,
 
 
 
 
 
260
  ...errorDetails
261
+ })
 
 
262
  }
263
 
264
  /**
src/lib/uploader.js CHANGED
@@ -191,8 +191,8 @@ class ImageUploader {
191
  })
192
  }
193
 
194
- // 逐个上传切割后的片段
195
- for (const segment of segments) {
196
  const segmentName = imageProcessor.generateSegmentName(
197
  imageName,
198
  segment.metadata.segmentIndex,
@@ -201,23 +201,49 @@ class ImageUploader {
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) {
 
191
  })
192
  }
193
 
194
+ // 并行上传切割后的片段
195
+ const uploadPromises = segments.map(async (segment) => {
196
  const segmentName = imageProcessor.generateSegmentName(
197
  imageName,
198
  segment.metadata.segmentIndex,
 
201
 
202
  try {
203
  const url = await this.uploadImage(segment.buffer, segmentName)
 
204
 
205
  if (requestId) {
206
  logger.logImageUpload(requestId, imageIndex, segment.metadata.segmentIndex, true, url)
207
  }
208
 
209
  console.log(`片段 ${segment.metadata.segmentIndex + 1}/${segment.metadata.totalSegments} 上传成功: ${url}`)
210
+ return {
211
+ url,
212
+ segmentIndex: segment.metadata.segmentIndex,
213
+ success: true
214
+ }
215
  } catch (error) {
216
  if (requestId) {
217
  logger.logImageUpload(requestId, imageIndex, segment.metadata.segmentIndex, false, null, error.message)
218
  }
219
+
220
+ console.error(`片段 ${segment.metadata.segmentIndex + 1} 上传失败: ${error.message}`)
221
+ return {
222
+ error: error.message,
223
+ segmentIndex: segment.metadata.segmentIndex,
224
+ success: false
225
+ }
226
  }
227
+ })
228
+
229
+ // 等待所有上传完成
230
+ const uploadResults = await Promise.all(uploadPromises)
231
+
232
+ // 检查是否有失败的上传
233
+ const failedUploads = uploadResults.filter(result => !result.success)
234
+ if (failedUploads.length > 0) {
235
+ const failedIndexes = failedUploads.map(result => result.segmentIndex + 1).join(', ')
236
+ throw new Error(`片段 ${failedIndexes} 上传失败`)
237
  }
238
 
239
+ // 按照片段顺序排序并提取URL
240
+ const sortedResults = uploadResults
241
+ .filter(result => result.success)
242
+ .sort((a, b) => a.segmentIndex - b.segmentIndex)
243
+ .map(result => result.url)
244
+
245
+ console.log(`长图切割并行上传完成,共 ${sortedResults.length} 个片段`)
246
+ return sortedResults
247
 
248
  } catch (error) {
249
  if (requestId) {