const axios = require('axios') const FormData = require('form-data') const crypto = require('crypto') const accountManager = require('./manager') const imageProcessor = require('./imageProcessor') const logger = require('./logger') class ImageUploader { constructor() { this.uploadUrl = 'https://mammouth.ai/api/attachments/saveFile' this.imageCache = new Map() } generateHash(buffer) { return crypto.createHash('sha256').update(buffer).digest('hex') } async uploadImage(imageBuffer, imageName) { // 确保参数有效 if (!imageBuffer || !Buffer.isBuffer(imageBuffer)) { throw new Error('无效的图片数据') } if (!imageName) { imageName = `image_${Date.now()}.png` } // 生成图片数据的哈希值作为缓存key const imageHash = this.generateHash(imageBuffer) // 检查缓存中是否已存在该图片的上传结果 if (this.imageCache.has(imageHash)) { console.log(`图片缓存命中: ${imageHash.substring(0, 8)}...`) return this.imageCache.get(imageHash) } console.log(`图片缓存未命中,开始上传: ${imageHash.substring(0, 8)}...`) // 获取一个可用的cookie const cookieValue = accountManager.getNextAvailableCookie() if (!cookieValue) { throw new Error('没有可用的账号') } // 创建FormData对象 const form = new FormData() form.append('type', 'image') form.append('name', imageName) form.append('file', imageBuffer, { filename: 'blob', contentType: 'image/png' // 默认为PNG,可根据实际情况调整 }) try { // 发送请求 const response = await axios.post(this.uploadUrl, form, { headers: { ...form.getHeaders(), 'Cookie': `auth_session=${cookieValue}`, 'Origin': 'https://mammouth.ai', 'Referer': 'https://mammouth.ai/app/a/default' } }) // 检查响应 if (response.data && response.data.location) { const imageUrl = response.data.location // 将结果保存到缓存 this.imageCache.set(imageHash, imageUrl) console.log(`图片上传成功并缓存: ${imageHash.substring(0, 8)}... -> ${imageUrl}`) return imageUrl } else { throw new Error('上传成功但返回格式不正确') } } catch (error) { // 如果是账号限制问题,尝试其他账号 if (error.response && error.response.status === 403) { accountManager.markAsUnavailable(cookieValue) // 递归尝试使用新账号 return this.uploadImage(imageBuffer, imageName) } throw new Error(`图片上传失败: ${error.message}`) } } async uploadFromBase64(base64String, imageName) { // 处理base64字符串,移除可能的前缀 const base64Data = base64String.replace(/^data:image\/\w+;base64,/, '') const buffer = Buffer.from(base64Data, 'base64') // 如果提供了前缀,则从中提取图片类型 let fileExt = 'png' const mimeMatch = base64String.match(/^data:image\/(\w+);base64,/) if (mimeMatch && mimeMatch[1]) { fileExt = mimeMatch[1] } // 如果没有提供文件名,则使用哈希值的前8位和时间戳生成 if (!imageName) { const hash = this.generateHash(buffer).substring(0, 8) imageName = `image_${hash}_${Date.now()}.${fileExt}` } return this.uploadImage(buffer, imageName) } async uploadFromUrl(imageUrl, imageName) { try { // 下载图片 const response = await axios.get(imageUrl, { responseType: 'arraybuffer' }) // 从URL中提取文件扩展名 let fileExt = 'png' const urlMatch = imageUrl.match(/\.([a-zA-Z0-9]+)(?:\?.*)?$/) if (urlMatch && urlMatch[1]) { fileExt = urlMatch[1].toLowerCase() } // 生成buffer并计算哈希 const buffer = Buffer.from(response.data) // 如果没有提供文件名,则使用哈希值的前8位和时间戳生成 if (!imageName) { const hash = this.generateHash(buffer).substring(0, 8) imageName = `image_${hash}_${Date.now()}.${fileExt}` } return this.uploadImage(buffer, imageName) } catch (error) { throw new Error(`从URL获取图片失败: ${error.message}`) } } getCacheStats() { return { size: this.imageCache.size, keys: Array.from(this.imageCache.keys()).map(k => k.substring(0, 8) + '...') } } clearCache() { const oldSize = this.imageCache.size this.imageCache.clear() return { cleared: oldSize } } /** * 智能上传图片(支持长图处理) * @param {Buffer} imageBuffer - 图片缓冲区 * @param {string} imageName - 图片名称 * @param {string} requestId - 请求ID(用于日志) * @param {number} imageIndex - 图片索引(用于日志) * @returns {Promise>} 上传后的URL数组 */ async uploadImageSmart(imageBuffer, imageName, requestId = null, imageIndex = 0) { try { // 检测是否为长图 const detection = await imageProcessor.detectLongImage(imageBuffer) if (requestId) { logger.logLongImageDetection(requestId, imageIndex, detection) } if (!detection.isLongImage) { // 不是长图,使用原有方式上传 const url = await this.uploadImage(imageBuffer, imageName) if (requestId) { logger.logImageUpload(requestId, imageIndex, 0, true, url) } return [url] } // 是长图,进行切割处理 console.log(`检测到长图 ${detection.width}x${detection.height},开始切割处理`) try { const segments = await imageProcessor.cropLongImage(imageBuffer) if (!segments || segments.length === 0) { throw new Error('长图切割失败:未生成任何片段') } if (requestId) { logger.logImageCropping(requestId, imageIndex, { segments, stats: imageProcessor.getProcessingStats(segments) }) } // 并行上传切割后的片段 const uploadPromises = segments.map(async (segment) => { const segmentName = imageProcessor.generateSegmentName( imageName, segment.metadata.segmentIndex, segment.metadata.totalSegments ) try { const url = await this.uploadImage(segment.buffer, segmentName) if (requestId) { logger.logImageUpload(requestId, imageIndex, segment.metadata.segmentIndex, true, url) } console.log(`片段 ${segment.metadata.segmentIndex + 1}/${segment.metadata.totalSegments} 上传成功: ${url}`) return { url, segmentIndex: segment.metadata.segmentIndex, success: true } } catch (error) { if (requestId) { logger.logImageUpload(requestId, imageIndex, segment.metadata.segmentIndex, false, null, error.message) } console.error(`片段 ${segment.metadata.segmentIndex + 1} 上传失败: ${error.message}`) return { error: error.message, segmentIndex: segment.metadata.segmentIndex, success: false } } }) // 等待所有上传完成 const uploadResults = await Promise.all(uploadPromises) // 检查是否有失败的上传 const failedUploads = uploadResults.filter(result => !result.success) if (failedUploads.length > 0) { const failedIndexes = failedUploads.map(result => result.segmentIndex + 1).join(', ') throw new Error(`片段 ${failedIndexes} 上传失败`) } // 按照片段顺序排序并提取URL const sortedResults = uploadResults .filter(result => result.success) .sort((a, b) => a.segmentIndex - b.segmentIndex) .map(result => result.url) console.log(`长图切割并行上传完成,共 ${sortedResults.length} 个片段`) return sortedResults } catch (longImageError) { // 长图处理失败,回退到原图上传 console.warn(`长图处理失败: ${longImageError.message},回退到原图上传`) if (requestId) { logger.logError(requestId, 'LONG_IMAGE_FALLBACK', `长图处理失败,回退到原图: ${longImageError.message}`, { imageIndex, originalSize: `${detection.width}x${detection.height}` }) } try { const fallbackUrl = await this.uploadImage(imageBuffer, imageName) if (requestId) { logger.logImageUpload(requestId, imageIndex, 0, true, fallbackUrl, '长图回退上传') } console.log(`长图回退上传成功: ${fallbackUrl}`) return [fallbackUrl] } catch (fallbackError) { throw new Error(`长图处理和回退上传均失败: ${longImageError.message} | ${fallbackError.message}`) } } } catch (error) { if (requestId) { logger.logError(requestId, 'IMAGE_UPLOAD_ERROR', error.message, { imageIndex, imageName }) } throw error } } /** * 智能上传Base64图片(支持长图处理) * @param {string} base64String - Base64字符串 * @param {string} imageName - 图片名称 * @param {string} requestId - 请求ID(用于日志) * @param {number} imageIndex - 图片索引(用于日志) * @param {boolean} preserveOrder - 是否保持顺序(禁用缓存优化) * @returns {Promise>} 上传后的URL数组 */ async uploadFromBase64Smart(base64String, imageName, requestId = null, imageIndex = 0, preserveOrder = false) { // 处理base64字符串,移除可能的前缀 const base64Data = base64String.replace(/^data:image\/\w+;base64,/, '') const buffer = Buffer.from(base64Data, 'base64') // 如果提供了前缀,则从中提取图片类型 let fileExt = 'png' const mimeMatch = base64String.match(/^data:image\/(\w+);base64,/) if (mimeMatch && mimeMatch[1]) { fileExt = mimeMatch[1] } // 如果没有提供文件名,则使用哈希值的前8位和时间戳生成 if (!imageName) { const hash = this.generateHash(buffer).substring(0, 8) imageName = `image_${hash}_${Date.now()}.${fileExt}` } return this.uploadImageSmart(buffer, imageName, requestId, imageIndex, preserveOrder) } /** * 智能上传URL图片(支持长图处理) * @param {string} imageUrl - 图片URL * @param {string} imageName - 图片名称 * @param {string} requestId - 请求ID(用于日志) * @param {number} imageIndex - 图片索引(用于日志) * @param {boolean} preserveOrder - 是否保持顺序(禁用缓存优化) * @returns {Promise>} 上传后的URL数组 */ async uploadFromUrlSmart(imageUrl, imageName, requestId = null, imageIndex = 0, preserveOrder = false) { try { // 下载图片 const response = await axios.get(imageUrl, { responseType: 'arraybuffer' }) // 从URL中提取文件扩展名 let fileExt = 'png' const urlMatch = imageUrl.match(/\.([a-zA-Z0-9]+)(?:\?.*)?$/) if (urlMatch && urlMatch[1]) { fileExt = urlMatch[1].toLowerCase() } // 生成buffer并计算哈希 const buffer = Buffer.from(response.data) // 如果没有提供文件名,则使用哈希值的前8位和时间戳生成 if (!imageName) { const hash = this.generateHash(buffer).substring(0, 8) imageName = `image_${hash}_${Date.now()}.${fileExt}` } return this.uploadImageSmart(buffer, imageName, requestId, imageIndex, preserveOrder) } catch (error) { throw new Error(`从URL获取图片失败: ${error.message}`) } } } module.exports = new ImageUploader()