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