|
|
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`
|
|
|
}
|
|
|
|
|
|
|
|
|
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)}...`)
|
|
|
|
|
|
|
|
|
const cookieValue = accountManager.getNextAvailableCookie()
|
|
|
if (!cookieValue) {
|
|
|
throw new Error('没有可用的账号')
|
|
|
}
|
|
|
|
|
|
|
|
|
const form = new FormData()
|
|
|
form.append('type', 'image')
|
|
|
form.append('name', imageName)
|
|
|
form.append('file', imageBuffer, {
|
|
|
filename: 'blob',
|
|
|
contentType: 'image/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) {
|
|
|
|
|
|
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]
|
|
|
}
|
|
|
|
|
|
|
|
|
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'
|
|
|
})
|
|
|
|
|
|
|
|
|
let fileExt = 'png'
|
|
|
const urlMatch = imageUrl.match(/\.([a-zA-Z0-9]+)(?:\?.*)?$/)
|
|
|
if (urlMatch && urlMatch[1]) {
|
|
|
fileExt = urlMatch[1].toLowerCase()
|
|
|
}
|
|
|
|
|
|
|
|
|
const buffer = Buffer.from(response.data)
|
|
|
|
|
|
|
|
|
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 }
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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} 上传失败`)
|
|
|
}
|
|
|
|
|
|
|
|
|
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
|
|
|
}
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async uploadFromBase64Smart(base64String, imageName, requestId = null, imageIndex = 0, preserveOrder = false) {
|
|
|
|
|
|
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]
|
|
|
}
|
|
|
|
|
|
|
|
|
if (!imageName) {
|
|
|
const hash = this.generateHash(buffer).substring(0, 8)
|
|
|
imageName = `image_${hash}_${Date.now()}.${fileExt}`
|
|
|
}
|
|
|
|
|
|
return this.uploadImageSmart(buffer, imageName, requestId, imageIndex, preserveOrder)
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async uploadFromUrlSmart(imageUrl, imageName, requestId = null, imageIndex = 0, preserveOrder = false) {
|
|
|
try {
|
|
|
|
|
|
const response = await axios.get(imageUrl, {
|
|
|
responseType: 'arraybuffer'
|
|
|
})
|
|
|
|
|
|
|
|
|
let fileExt = 'png'
|
|
|
const urlMatch = imageUrl.match(/\.([a-zA-Z0-9]+)(?:\?.*)?$/)
|
|
|
if (urlMatch && urlMatch[1]) {
|
|
|
fileExt = urlMatch[1].toLowerCase()
|
|
|
}
|
|
|
|
|
|
|
|
|
const buffer = Buffer.from(response.data)
|
|
|
|
|
|
|
|
|
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() |