mamm / src /lib /uploader.js
nomid2's picture
Upload 2 files
0b04ef0 verified
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<Array<string>>} 上传后的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<Array<string>>} 上传后的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<Array<string>>} 上传后的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()