mamm / src /router /chat.js
nomid2's picture
Upload chat.js
89082f1 verified
const express = require('express')
const axios = require('axios')
const FormData = require('form-data')
const { v4: uuidv4 } = require('uuid')
const { MODEL_MAPPING, MAMMOUTH_API_URL, AUTH_TOKEN, UNLIMITED_MODELS } = require('../config')
const accountManager = require('../lib/manager')
const imageUploader = require('../lib/uploader')
const logger = require('../lib/logger')
const ErrorHandler = require('../lib/errorHandler')
const router = express.Router()
// API密钥认证中间件
const authenticate = (req, res, next) => {
const authHeader = req.headers.authorization || req.headers.Authorization || req.headers['x-api-key']
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return res.status(401).json({
error: {
message: '缺少有效的API密钥',
type: 'authentication_error',
code: 'invalid_api_key'
}
})
}
const apiKey = authHeader.substring(7)
if (apiKey !== AUTH_TOKEN) {
return res.status(401).json({
error: {
message: 'API密钥无效',
type: 'authentication_error',
code: 'invalid_api_key'
}
})
}
next()
}
// 检查模型是否在不受限制的列表中
function isUnlimitedModel(model) {
return UNLIMITED_MODELS.includes(model)
}
// 将OpenAI格式转换为Mammouth格式
async function convertOpenAIToMammouth(openaiRequest, requestId = null) {
const form = new FormData()
// 模型选择
const requestedModel = openaiRequest.model
const mammouthModel = MODEL_MAPPING[requestedModel] || openaiRequest.model
form.append('model', mammouthModel)
// 添加流式响应参数(如果请求是流式的)
if (openaiRequest.stream === true) {
form.append('stream', 'true')
form.append('streaming', 'true')
if (requestId) {
console.log(`[请求转换] 添加流式响应参数: stream=true, streaming=true`)
}
} else {
// 确保非流式请求不包含流式参数
if (requestId) {
console.log(`[请求转换] 非流式请求,不添加流式参数`)
}
}
// 提取system角色的消息作为preprompt
let systemMessages = []
let regularMessages = []
openaiRequest.messages.forEach(message => {
if (message.role === 'system') {
systemMessages.push(message.content)
} else {
regularMessages.push(message)
}
})
// 将所有system消息组合为preprompt
let preprompt = systemMessages.join('\n\n')
// 检查是否有长图需要处理,如果有则添加长图处理说明
const hasLongImages = regularMessages.some(message =>
Array.isArray(message.content) &&
message.content.some(part => part.type === 'image_url')
)
if (hasLongImages) {
const longImagePrompt = `
重要说明:本次对话可能包含长图片段。当你看到标记为"[长图片段 X/Y]"的图片时:
1. 这些是同一张长图的不同部分,按顺序排列
2. 请分析每个片段的内容,记住之前片段的信息
3. 在处理最后一个片段时,请提供基于所有片段的完整分析
4. 确保回答涵盖整张长图的所有重要内容,不要遗漏任何部分`
preprompt = preprompt ? `${preprompt}${longImagePrompt}` : longImagePrompt.trim()
}
form.append('preprompt', preprompt)
// 处理非system角色的消息
let totalImageCount = 0
// 先统计图片总数用于日志
regularMessages.forEach((message, index) => {
console.log(`[调试] 消息${index}内容类型:`, typeof message.content, Array.isArray(message.content) ? '数组' : '非数组')
if (Array.isArray(message.content)) {
const imageCount = message.content.filter(part => part.type === 'image_url').length
totalImageCount += imageCount
console.log(`[调试] 消息${index}包含${imageCount}张图片`)
message.content.forEach((part, partIndex) => {
console.log(`[调试] 消息${index}部分${partIndex}类型:`, part.type)
})
}
})
console.log(`[调试] 总图片数量: ${totalImageCount}`)
if (requestId && totalImageCount > 0) {
logger.logImageProcessingStart(requestId, totalImageCount)
}
let currentImageIndex = 0
for (const message of regularMessages) {
// 处理包含图片的消息
let content = message.content
let processedMessages = [] // 存储处理后的消息(可能包含多个片段)
// 如果是对象数组(多模态内容)
if (Array.isArray(message.content)) {
const textParts = []
const imageParts = []
// 分离文本和图片部分
for (const part of message.content) {
if (part.type === 'text') {
textParts.push(part.text)
} else if (part.type === 'image_url') {
imageParts.push(part)
}
}
// 合并所有文本部分
const combinedText = textParts.join('\n')
// 分别收集长图和普通图片
const longImageSegments = [] // 存储长图片段
const normalImageResults = [] // 存储普通图片结果,按索引排序
let hasProcessedText = false // 标记是否已处理文本
let normalImageCount = 0 // 普通图片计数
// 处理每个图片 - 严格按顺序处理,确保不会出现顺序混乱
console.log(`[图片处理开始] 共${imageParts.length}张图片待处理,将严格按顺序处理`)
for (let imagePartIndex = 0; imagePartIndex < imageParts.length; imagePartIndex++) {
const imagePart = imageParts[imagePartIndex]
console.log(`[图片处理] 开始处理第${imagePartIndex + 1}张图片 (消息位置: ${imagePartIndex}, 全局索引: ${currentImageIndex + 1})`)
try {
// 获取图片数据
let imageUrl = imagePart.image_url
if (typeof imageUrl === 'object' && imageUrl.url) {
imageUrl = imageUrl.url
}
console.log(`[图片处理] 图片${currentImageIndex + 1}类型: ${imageUrl.startsWith('data:image') ? 'Base64' : 'URL'}`)
// 使用智能上传方法处理图片(支持长图)
let uploadedUrls = []
if (imageUrl.startsWith('data:image')) {
uploadedUrls = await imageUploader.uploadFromBase64Smart(
imageUrl,
null,
requestId,
currentImageIndex,
false // 恢复正常缓存机制
)
} else {
uploadedUrls = await imageUploader.uploadFromUrlSmart(
imageUrl,
null,
requestId,
currentImageIndex,
false // 恢复正常缓存机制
)
}
console.log(`[图片处理] 图片${currentImageIndex + 1}上传完成,获得${uploadedUrls.length}个URL,位置索引: ${imagePartIndex}`)
// 如果是长图(多个片段),为每个片段创建单独的消息
if (uploadedUrls.length > 1) {
console.log(`[长图处理] 图片${currentImageIndex + 1}被切割为${uploadedUrls.length}个片段,将按顺序发送`)
if (requestId) {
logger.logMessageSegmentation(requestId, currentImageIndex, uploadedUrls.length)
}
uploadedUrls.forEach((url, segmentIndex) => {
// 为每个片段生成更详细的提示文本
let segmentText = ''
if (segmentIndex === 0) {
// 第一个片段:包含原始文本和长图说明
const originalText = combinedText || '请分析这张长图的内容'
segmentText = `${originalText}
注意:这是一张长图,已被切割为${uploadedUrls.length}个片段。请分析每个片段的内容,并在最后一个片段时提供完整的总结。
[长图片段 ${segmentIndex + 1}/${uploadedUrls.length}] - 这是长图的开始部分`
hasProcessedText = true
} else if (segmentIndex === uploadedUrls.length - 1) {
// 最后一个片段:要求提供完整总结
segmentText = `[长图片段 ${segmentIndex + 1}/${uploadedUrls.length}] - 这是长图的结束部分
请基于所有${uploadedUrls.length}个片段的内容,提供这张长图的完整分析和总结。`
} else {
// 中间片段:说明这是连续内容
segmentText = `[长图片段 ${segmentIndex + 1}/${uploadedUrls.length}] - 这是长图的中间部分,请继续分析内容`
}
processedMessages.push({
content: segmentText,
imagesData: [url],
documentsData: []
})
console.log(`[消息生成] 长图片段${segmentIndex + 1}: "${segmentText.substring(0, 80)}..."`)
})
} else {
// 普通图片,严格按顺序存储到结果数组中
const imageResult = {
index: imagePartIndex, // 在消息中的位置索引(关键排序字段)
urls: uploadedUrls,
originalIndex: currentImageIndex, // 全局图片索引
processOrder: normalImageCount // 处理顺序
}
normalImageResults.push(imageResult)
normalImageCount++
console.log(`[图片收集] 普通图片${currentImageIndex + 1}已收集 (消息位置: ${imagePartIndex}, 处理顺序: ${normalImageCount}),当前共${normalImageCount}张普通图片`)
}
currentImageIndex++
} catch (error) {
if (requestId) {
logger.logError(requestId, 'IMAGE_PROCESSING_ERROR', error.message, {
imageIndex: currentImageIndex,
imagePartIndex: imagePartIndex,
imageUrl: typeof imagePart.image_url === 'string' ? imagePart.image_url.substring(0, 100) : 'object'
})
}
console.error(`图片处理错误 (位置${imagePartIndex}, 全局${currentImageIndex + 1}):`, error.message)
// 图片处理失败时,添加一个错误占位符,避免完全跳过
const errorPlaceholder = {
index: imagePartIndex,
urls: [],
originalIndex: currentImageIndex,
processOrder: normalImageCount,
error: true,
errorMessage: error.message
}
normalImageResults.push(errorPlaceholder)
normalImageCount++
console.log(`[图片错误] 图片${currentImageIndex + 1}处理失败,已添加错误占位符`)
currentImageIndex++
}
}
console.log(`[图片处理完成] 共处理${imageParts.length}张图片,成功收集${normalImageResults.length}张普通图片`)
// 如果有普通图片,严格按原始顺序创建一个包含所有普通图片的消息
if (normalImageResults.length > 0) {
console.log(`[排序前验证] 收集到${normalImageResults.length}张普通图片`)
normalImageResults.forEach((result, idx) => {
console.log(` 图片${idx + 1}: 消息位置=${result.index}, 全局索引=${result.originalIndex}, 处理顺序=${result.processOrder}`)
})
// 严格按照imagePartIndex排序,确保完全按客户端上传顺序
console.log(`[开始排序] 严格按消息中的位置索引排序...`)
normalImageResults.sort((a, b) => {
const diff = a.index - b.index
console.log(`[排序比较] 位置${a.index} vs 位置${b.index} = ${diff}`)
return diff
})
console.log(`[排序后验证] 最终图片顺序:`)
normalImageResults.forEach((result, idx) => {
console.log(` 第${idx + 1}位: 消息位置=${result.index}, 全局索引=${result.originalIndex}, 处理顺序=${result.processOrder}`)
})
// 提取所有URL,严格保持顺序,跳过错误的图片
const orderedImageUrls = []
const errorMessages = []
normalImageResults.forEach((result, idx) => {
if (result.error) {
console.log(`[URL提取] 第${idx + 1}个结果,位置${result.index},图片处理失败: ${result.errorMessage}`)
errorMessages.push(`图片${result.originalIndex + 1}处理失败`)
} else {
console.log(`[URL提取] 第${idx + 1}个结果,位置${result.index},添加${result.urls.length}个URL`)
orderedImageUrls.push(...result.urls)
}
})
const includeOriginalText = !hasProcessedText && combinedText
let normalImageText = includeOriginalText ? combinedText : ''
// 构建图片状态信息
const successCount = orderedImageUrls.length
const errorCount = errorMessages.length
if (successCount > 0 && errorCount > 0) {
const statusText = `[包含 ${successCount} 张图片,${errorCount} 张图片处理失败]`
normalImageText = normalImageText ? `${normalImageText}\n\n${statusText}` : statusText
} else if (successCount > 0) {
const statusText = `[包含 ${successCount} 张图片]`
normalImageText = normalImageText ? `${normalImageText}\n\n${statusText}` : statusText
} else if (errorCount > 0) {
const statusText = `[${errorCount} 张图片处理失败]`
normalImageText = normalImageText ? `${normalImageText}\n\n${statusText}` : statusText
}
processedMessages.push({
content: normalImageText || '.',
imagesData: orderedImageUrls,
documentsData: []
})
console.log(`[消息生成] 普通图片批量消息: ${successCount}张成功,${errorCount}张失败,严格按顺序排列`)
console.log(`[最终顺序验证] 图片顺序: ${normalImageResults.map(r => `位置${r.index}(图片${r.originalIndex + 1}${r.error ? '-失败' : ''})`).join(' -> ')}`)
}
// 如果没有图片,只有文本
if (imageParts.length === 0) {
processedMessages.push({
content: combinedText || '.',
imagesData: [],
documentsData: []
})
}
} else {
// 纯文本消息
processedMessages.push({
content: content || '.',
imagesData: [],
documentsData: []
})
}
// 将所有处理后的消息添加到表单
processedMessages.forEach(msg => {
form.append('messages', JSON.stringify(msg))
})
}
// 统计总消息数量并记录警告
const totalMessages = form.getBuffer().toString().split('messages').length - 1
if (requestId && totalMessages > 4) {
console.warn(`[消息转换] 警告:消息数量较多(${totalMessages}个),可能影响流式响应性能`)
logger.logError(requestId, 'HIGH_MESSAGE_COUNT', `消息数量过多: ${totalMessages}个`, {
totalMessages,
recommendation: '考虑减少长图片段数量或使用非流式模式'
})
}
return form
}
// 处理流数据
async function handleStreamResponse(axiosResponse, res, requestedModel, logRequestId = null) {
const responseId = uuidv4()
const timestamp = Math.floor(Date.now() / 1000)
const decoder = new TextDecoder()
if (logRequestId) {
console.log(`[流式响应] 开始处理流式响应,请求ID: ${logRequestId}`)
console.log(`[流式响应] 响应状态码: ${axiosResponse.status}`)
console.log(`[流式响应] 响应头: ${JSON.stringify(axiosResponse.headers)}`)
console.log(`[流式响应] 数据流类型: ${typeof axiosResponse.data}`)
}
// 发送初始角色数据
const initialData = {
id: `chatcmpl-${responseId}`,
object: "chat.completion.chunk",
created: timestamp,
model: requestedModel,
choices: [{
index: 0,
delta: { role: "assistant", content: "" },
finish_reason: null
}]
}
const initialMessage = `data: ${JSON.stringify(initialData)}\n\n`
res.write(initialMessage)
if (logRequestId) {
console.log(`[流式响应] 已发送初始角色数据`)
}
let totalChunks = 0
let totalContentLength = 0
axiosResponse.data.on('data', (chunk) => {
totalChunks++
try {
const chunkStr = decoder.decode(chunk, { stream: true })
if (logRequestId) {
console.log(`[流式响应] 收到数据块 ${totalChunks},原始长度: ${chunk.length},解码后长度: ${chunkStr.length}`)
console.log(`[流式响应] 数据块内容预览: "${chunkStr.substring(0, 200)}${chunkStr.length > 200 ? '...' : ''}"`)
}
// 检查是否为有效的文本内容
const textToSend = chunkStr.trim()
if (textToSend && textToSend.length > 0) {
totalContentLength += textToSend.length
const responseData = {
id: `chatcmpl-${responseId}`,
object: "chat.completion.chunk",
created: timestamp,
model: requestedModel,
choices: [{
index: 0,
delta: { content: textToSend },
finish_reason: null
}]
}
const responseMessage = `data: ${JSON.stringify(responseData)}\n\n`
res.write(responseMessage)
if (logRequestId) {
console.log(`[流式响应] 已发送内容块 ${totalChunks},内容长度: ${textToSend.length}`)
}
} else {
if (logRequestId) {
console.log(`[流式响应] 数据块 ${totalChunks} 为空或无效,跳过发送`)
}
}
} catch (decodeError) {
if (logRequestId) {
console.error(`[流式响应] 数据块 ${totalChunks} 解码失败: ${decodeError.message}`)
logger.logError(logRequestId, 'STREAM_DECODE_ERROR', decodeError.message, {
chunkLength: chunk.length,
chunkNumber: totalChunks
})
}
}
})
axiosResponse.data.on('end', async () => {
if (logRequestId) {
console.log(`[流式响应] 数据流结束,总共处理 ${totalChunks} 个数据块,总内容长度: ${totalContentLength}`)
// 如果没有收到任何内容,检查是否API返回了非流式响应
if (totalChunks === 0) {
console.warn(`[流式响应] 警告:未收到任何数据块,可能API返回了非流式响应`)
// 尝试检查响应是否是JSON格式
try {
if (axiosResponse.data && typeof axiosResponse.data === 'object' && axiosResponse.data.choices) {
console.log(`[流式回退] 检测到非流式JSON响应,尝试转换为流式格式`)
const content = axiosResponse.data.choices[0]?.message?.content || ''
if (content) {
// 将非流式响应转换为流式格式发送
const chunkSize = 20
for (let i = 0; i < content.length; i += chunkSize) {
const chunk = content.substring(i, i + chunkSize)
const chunkData = {
id: `chatcmpl-${responseId}`,
object: "chat.completion.chunk",
created: timestamp,
model: requestedModel,
choices: [{
index: 0,
delta: { content: chunk },
finish_reason: null
}]
}
const chunkMessage = `data: ${JSON.stringify(chunkData)}\n\n`
res.write(chunkMessage)
await new Promise(resolve => setTimeout(resolve, 50))
}
console.log(`[流式回退] 成功转换非流式响应为流式格式,内容长度: ${content.length}`)
totalContentLength = content.length
}
}
} catch (parseError) {
console.error(`[流式回退] 解析非流式响应失败: ${parseError.message}`)
}
}
// 如果仍然没有收到任何内容,使用默认回退机制
if (totalContentLength === 0) {
console.warn(`[流式响应] 警告:未收到任何内容,使用默认回退机制`)
logger.logError(logRequestId, 'STREAM_NO_CONTENT', '流式响应未收到任何内容,尝试回退', {
totalChunks,
model: requestedModel
})
// 发送默认回退消息,避免复杂的HTTP请求导致连接问题
console.log(`[流式回退] 使用默认消息回退`)
const fallbackMessage = "抱歉,图片处理完成但响应出现问题。\n\n这可能是由于长图切割导致的流式响应问题。建议:\n1. 重新发送请求\n2. 使用非流式模式\n3. 或尝试上传较短的图片"
// 将回退消息分块发送,模拟流式效果
const chunkSize = 20
for (let i = 0; i < fallbackMessage.length; i += chunkSize) {
const chunk = fallbackMessage.substring(i, i + chunkSize)
const chunkData = {
id: `chatcmpl-${responseId}`,
object: "chat.completion.chunk",
created: timestamp,
model: requestedModel,
choices: [{
index: 0,
delta: { content: chunk },
finish_reason: null
}]
}
const chunkMessage = `data: ${JSON.stringify(chunkData)}\n\n`
res.write(chunkMessage)
// 添加小延迟模拟流式效果
await new Promise(resolve => setTimeout(resolve, 50))
}
console.log(`[流式回退] 默认消息发送完成`)
totalContentLength = fallbackMessage.length
}
}
// 发送完成信号
const endData = {
id: `chatcmpl-${responseId}`,
object: "chat.completion.chunk",
created: timestamp,
model: requestedModel,
choices: [{
index: 0,
delta: {},
finish_reason: "stop"
}]
}
const endMessage = `data: ${JSON.stringify(endData)}\n\n`
res.write(endMessage)
res.write('data: [DONE]\n\n')
res.end()
if (logRequestId) {
console.log(`[流式响应] 已发送完成信号和结束标记`)
}
})
axiosResponse.data.on('error', (err) => {
if (logRequestId) {
logger.logError(logRequestId, 'STREAM_ERROR', err.message, {
model: requestedModel,
totalChunks,
totalContentLength
})
console.log(`[流式响应] 流处理错误: ${err.message},已处理 ${totalChunks} 个数据块`)
}
console.error('流数据处理错误:', err)
res.status(500).end()
})
}
// 处理非流数据
function handleNonStreamResponse(axiosResponse, res, requestedModel, logRequestId = null) {
const responseId = uuidv4()
const timestamp = Math.floor(Date.now() / 1000)
// 格式化为OpenAI的响应格式
let content = axiosResponse.data.content;
// 如果内容是字符串且被引号包裹,移除外层引号
if (typeof content === 'string' && content.startsWith('"') && content.endsWith('"')) {
content = content.slice(1, -1);
}
const responseData = {
id: `chatcmpl-${responseId}`,
object: "chat.completion",
created: timestamp,
model: requestedModel,
choices: [{
index: 0,
message: {
role: "assistant",
content: content || axiosResponse.data
},
finish_reason: "stop"
}],
usage: {
prompt_tokens: 0,
completion_tokens: 0,
total_tokens: 0
}
}
res.json(responseData)
}
// 使用新的Cookie重新发送请求
async function retryWithNewCookie(req, res, config, currentCookie, requestedModel, isStreamRequest) {
try {
// 标记当前Cookie为不可用
accountManager.markAsUnavailable(currentCookie)
// 获取新的Cookie
const newCookie = accountManager.getNextAvailableCookie()
// 更新请求配置中的Cookie
config.headers.Cookie = `auth_session=${newCookie}`
// 发送请求到Mammouth API
const response = await axios(config)
// 处理响应
if (isStreamRequest) {
handleStreamResponse(response, res, requestedModel)
} else {
handleNonStreamResponse(response, res, requestedModel)
}
return true
} catch (error) {
// 如果重试也失败了,返回false
return false
}
}
// OpenAI兼容的聊天完成API接口,使用中间件验证API密钥
router.post('/completions', authenticate, async (req, res) => {
let requestId = null
const startTime = Date.now()
try {
const openaiRequest = req.body
const isStreamRequest = openaiRequest.stream === true
const requestedModel = openaiRequest.model
// 记录请求开始
requestId = logger.logRequestStart(
req.method,
req.originalUrl,
req.headers,
openaiRequest
)
// 设置适当的响应头
if (isStreamRequest) {
res.setHeader('Content-Type', 'text/event-stream')
res.setHeader('Cache-Control', 'no-cache')
res.setHeader('Connection', 'keep-alive')
}
// 转换请求格式
const form = await convertOpenAIToMammouth(openaiRequest, requestId)
// 获取Cookie - 根据模型类型使用不同的获取方法
const cookieValue = isUnlimitedModel(requestedModel)
? accountManager.getAnyCookie()
: accountManager.getNextAvailableCookie()
// 记录模型调用开始
const mammouthModel = MODEL_MAPPING[requestedModel] || requestedModel
logger.logModelCallStart(requestId, requestedModel, mammouthModel)
// 准备请求配置
const config = {
method: 'post',
url: MAMMOUTH_API_URL,
headers: {
...form.getHeaders(),
'Cookie': `auth_session=${cookieValue}`,
'origin': 'https://mammouth.ai'
},
data: form,
responseType: isStreamRequest ? 'stream' : 'json',
// 添加流式请求的额外配置
...(isStreamRequest && {
timeout: 60000, // 60秒超时
maxRedirects: 0 // 禁用重定向
})
}
if (requestId && isStreamRequest) {
console.log(`[请求配置] 流式请求配置: responseType=stream, timeout=60s`)
}
try {
// 发送请求到Mammouth API
const modelCallStartTime = Date.now()
const response = await axios(config)
const modelCallDuration = Date.now() - modelCallStartTime
// 记录模型调用成功
logger.logModelCallEnd(requestId, true, null, modelCallDuration)
// 处理响应
if (isStreamRequest) {
handleStreamResponse(response, res, requestedModel, requestId)
} else {
handleNonStreamResponse(response, res, requestedModel, requestId)
}
// 记录请求成功结束
logger.logRequestEnd(requestId, 200, {
responseType: isStreamRequest ? 'stream' : 'json',
totalDuration: Date.now() - startTime
})
} catch (error) {
// 记录模型调用失败
const modelCallDuration = Date.now() - startTime
logger.logModelCallEnd(requestId, false, error.message, modelCallDuration)
// 优化错误日志打印,只打印关键信息
const errorStatus = error.response?.status || 'unknown'
const errorMessage = error.response?.data?.message || error.message || 'Unknown error'
// 记录详细错误信息
logger.logError(requestId, 'MODEL_CALL_ERROR', errorMessage, {
status: errorStatus,
model: requestedModel,
isStream: isStreamRequest,
cookieUsed: cookieValue?.substring(0, 8) + '...'
})
console.error(`API转发错误: [${errorStatus}] ${errorMessage}`)
// 如果是403错误(达到使用限制)
if (error.response && error.response.status === 403) {
// console.log(error)
console.log(`账号 ${cookieValue.substring(0, 5)}... 使用模型 ${requestedModel} 已达到使用限制`)
// 根据模型类型进行不同处理
if (isUnlimitedModel(requestedModel)) {
// 不受限模型也返回403,尝试将当前账号标记为不可用并再试一次
accountManager.markAsUnavailable(cookieValue)
// 对于不受限模型再次获取一个任意Cookie尝试
const newCookie = accountManager.getAnyCookie()
console.log(`尝试使用不受限模型的另一个账号: ${newCookie.substring(0, 5)}...`)
// 更新配置
config.headers.Cookie = `auth_session=${newCookie}`
try {
// 再次尝试请求
const response = await axios(config)
// 处理响应
if (isStreamRequest) {
handleStreamResponse(response, res, requestedModel)
} else {
handleNonStreamResponse(response, res, requestedModel)
}
// 成功,直接返回
return
} catch (retryError) {
console.error(`无限制模型二次尝试也失败: ${retryError.message}`)
// 继续到错误处理
}
} else {
// 普通模型,尝试切换账号
console.log(`尝试使用新账号...`)
const cookieRetrySuccess = await retryWithNewCookie(
req, res, config, cookieValue, requestedModel, isStreamRequest
)
// 如果切换账号成功,就返回
if (cookieRetrySuccess) return
}
// 所有重试方法都失败,返回错误信息
const errorMessage =
error.response.data?.message ||
error.response.data?.statusMessage ||
'使用限制:所有账号已临时达到使用限制。请稍后再试。'
const requestId = uuidv4()
const timestamp = Math.floor(Date.now() / 1000)
if (isStreamRequest) {
// 流式响应情况下,以SSE格式返回错误消息
res.write(`data: ${JSON.stringify({
id: `chatcmpl-${requestId}`,
object: "chat.completion.chunk",
created: timestamp,
model: requestedModel,
choices: [{
index: 0,
delta: { role: "assistant", content: "" },
finish_reason: null
}]
})}\n\n`)
// 发送错误消息内容
res.write(`data: ${JSON.stringify({
id: `chatcmpl-${requestId}`,
object: "chat.completion.chunk",
created: timestamp,
model: requestedModel,
choices: [{
index: 0,
delta: { content: errorMessage },
finish_reason: null
}]
})}\n\n`)
// 发送完成信号
res.write(`data: ${JSON.stringify({
id: `chatcmpl-${requestId}`,
object: "chat.completion.chunk",
created: timestamp,
model: requestedModel,
choices: [{
index: 0,
delta: {},
finish_reason: "stop"
}]
})}\n\n`)
res.write('data: [DONE]\n\n')
res.end()
} else {
// 非流式响应情况下,以普通JSON格式返回错误消息
res.json({
id: `chatcmpl-${requestId}`,
object: "chat.completion",
created: timestamp,
model: requestedModel,
choices: [{
index: 0,
message: {
role: "assistant",
content: errorMessage
},
finish_reason: "stop"
}],
usage: {
prompt_tokens: 0,
completion_tokens: 0,
total_tokens: 0
}
})
}
} else {
// 其他错误,使用统一错误处理
logger.logRequestEnd(requestId, 500, {
error: error.message,
totalDuration: Date.now() - startTime
})
ErrorHandler.handleModelError(res, error, requestId, requestedModel, isStreamRequest)
}
}
} catch (error) {
// 使用统一错误处理
if (requestId) {
logger.logRequestEnd(requestId, 500, {
error: error.message,
totalDuration: Date.now() - startTime
})
}
ErrorHandler.handleApiError(res, error, requestId, {
totalDuration: Date.now() - startTime,
endpoint: '/v1/chat/completions'
})
}
})
module.exports = router