Upload 4 files
Browse files- src/lib/imageProcessor.js +208 -0
- src/lib/logger.js +296 -0
- 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
|
| 6 |
-
const
|
| 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.
|
| 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.
|
| 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.
|
| 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 |
|