Upload 2 files
Browse files- src/router/chat.js +116 -336
src/router/chat.js
CHANGED
|
@@ -5,50 +5,10 @@ const { v4: uuidv4 } = require('uuid')
|
|
| 5 |
const { MODEL_MAPPING, MAMMOUTH_API_URL, AUTH_TOKEN, UNLIMITED_MODELS } = require('../config')
|
| 6 |
const accountManager = require('../lib/manager')
|
| 7 |
const imageUploader = require('../lib/uploader')
|
|
|
|
| 8 |
|
| 9 |
const router = express.Router()
|
| 10 |
|
| 11 |
-
// 图片格式检测和调试函数
|
| 12 |
-
function debugImageFormats(messages, requestId, timestamp) {
|
| 13 |
-
console.log(`[${timestamp}] 🔍 [${requestId}] === 开始详细图片格式检测 ===`)
|
| 14 |
-
|
| 15 |
-
if (!messages || !Array.isArray(messages)) {
|
| 16 |
-
console.log(`[${timestamp}] ❌ [${requestId}] 消息不是数组格式`)
|
| 17 |
-
return
|
| 18 |
-
}
|
| 19 |
-
|
| 20 |
-
messages.forEach((msg, msgIndex) => {
|
| 21 |
-
console.log(`[${timestamp}] 📝 [${requestId}] 消息${msgIndex + 1}:`)
|
| 22 |
-
console.log(`[${timestamp}] 📝 [${requestId}] 角色: ${msg.role}`)
|
| 23 |
-
console.log(`[${timestamp}] 📝 [${requestId}] 内容类型: ${Array.isArray(msg.content) ? 'array' : typeof msg.content}`)
|
| 24 |
-
|
| 25 |
-
if (Array.isArray(msg.content)) {
|
| 26 |
-
msg.content.forEach((part, partIndex) => {
|
| 27 |
-
console.log(`[${timestamp}] 🔍 [${requestId}] 部分${partIndex + 1}:`)
|
| 28 |
-
console.log(`[${timestamp}] 🔍 [${requestId}] 类型: ${part.type}`)
|
| 29 |
-
console.log(`[${timestamp}] 🔍 [${requestId}] 键: ${Object.keys(part).join(', ')}`)
|
| 30 |
-
|
| 31 |
-
// 检测各种可能的图片格式
|
| 32 |
-
if (part.type === 'image_url') {
|
| 33 |
-
console.log(`[${timestamp}] ✅ [${requestId}] → 标准OpenAI图片格式`)
|
| 34 |
-
} else if (part.type === 'text' && part.text && part.text.includes('data:image/')) {
|
| 35 |
-
console.log(`[${timestamp}] ✅ [${requestId}] → 文本中包含base64图片`)
|
| 36 |
-
} else if (part.image || part.image_data || part.data) {
|
| 37 |
-
console.log(`[${timestamp}] ✅ [${requestId}] → 直接图片数据字段`)
|
| 38 |
-
} else if (part.type === 'image' || part.type === 'image_base64') {
|
| 39 |
-
console.log(`[${timestamp}] ✅ [${requestId}] → 非标准图片类型`)
|
| 40 |
-
} else {
|
| 41 |
-
console.log(`[${timestamp}] ❓ [${requestId}] → 未识别的格式`)
|
| 42 |
-
}
|
| 43 |
-
})
|
| 44 |
-
} else if (typeof msg.content === 'string' && msg.content.includes('data:image/')) {
|
| 45 |
-
console.log(`[${timestamp}] ✅ [${requestId}] → 字符串消息中包含base64图片`)
|
| 46 |
-
}
|
| 47 |
-
})
|
| 48 |
-
|
| 49 |
-
console.log(`[${timestamp}] 🔍 [${requestId}] === 图片格式检测完成 ===`)
|
| 50 |
-
}
|
| 51 |
-
|
| 52 |
// API密钥认证中间件
|
| 53 |
const authenticate = (req, res, next) => {
|
| 54 |
const authHeader = req.headers.authorization || req.headers.Authorization || req.headers['x-api-key']
|
|
@@ -84,7 +44,7 @@ function isUnlimitedModel(model) {
|
|
| 84 |
}
|
| 85 |
|
| 86 |
// 将OpenAI格式转换为Mammouth格式
|
| 87 |
-
async function convertOpenAIToMammouth(openaiRequest) {
|
| 88 |
const form = new FormData()
|
| 89 |
|
| 90 |
// 模型选择
|
|
@@ -109,6 +69,21 @@ async function convertOpenAIToMammouth(openaiRequest) {
|
|
| 109 |
form.append('preprompt', preprompt)
|
| 110 |
|
| 111 |
// 处理非system角色的消息
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 112 |
for (const message of regularMessages) {
|
| 113 |
// 处理包含图片的消息
|
| 114 |
const imagesData = []
|
|
@@ -120,124 +95,48 @@ async function convertOpenAIToMammouth(openaiRequest) {
|
|
| 120 |
|
| 121 |
// 遍历内容部分
|
| 122 |
for (const part of message.content) {
|
| 123 |
-
console.log(`[${new Date().toISOString()}] 🔍 处理消息部分:`, JSON.stringify(part, null, 2))
|
| 124 |
-
|
| 125 |
if (part.type === 'text') {
|
| 126 |
textParts.push(part.text)
|
| 127 |
-
// 检查文本中是否包含base64图片
|
| 128 |
-
if (part.text && part.text.includes('data:image/')) {
|
| 129 |
-
console.log(`[${new Date().toISOString()}] 🖼️ 在文本中发现base64图片`)
|
| 130 |
-
try {
|
| 131 |
-
const imageId = `img_text_${Date.now()}_${Math.random().toString(36).substr(2, 5)}`
|
| 132 |
-
console.log(`[${new Date().toISOString()}] 🔄 [${imageId}] 开始处理文本中的base64图片`)
|
| 133 |
-
const uploadedImageUrls = await imageUploader.uploadFromBase64(part.text)
|
| 134 |
-
console.log(`[${new Date().toISOString()}] ✅ [${imageId}] 文本中的图片处理完成`)
|
| 135 |
-
|
| 136 |
-
if (Array.isArray(uploadedImageUrls)) {
|
| 137 |
-
imagesData.push(...uploadedImageUrls)
|
| 138 |
-
} else {
|
| 139 |
-
imagesData.push(uploadedImageUrls)
|
| 140 |
-
}
|
| 141 |
-
} catch (error) {
|
| 142 |
-
console.error(`[${new Date().toISOString()}] ❌ 处理文本中的图片失败:`, error.message)
|
| 143 |
-
}
|
| 144 |
-
}
|
| 145 |
} else if (part.type === 'image_url') {
|
| 146 |
-
const imageId = `img_${Date.now()}_${Math.random().toString(36).substr(2, 5)}`
|
| 147 |
try {
|
| 148 |
-
console.log(`[${new Date().toISOString()}] 🖼️ [${imageId}] 开始处理图片`)
|
| 149 |
-
|
| 150 |
// 获取图片数据
|
| 151 |
let imageUrl = part.image_url
|
| 152 |
if (typeof imageUrl === 'object' && imageUrl.url) {
|
| 153 |
imageUrl = imageUrl.url
|
| 154 |
}
|
| 155 |
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
// 处理base64图片
|
| 160 |
if (imageUrl.startsWith('data:image')) {
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
imagesData.push(...uploadedImageUrls)
|
| 168 |
-
} else {
|
| 169 |
-
imagesData.push(uploadedImageUrls)
|
| 170 |
-
}
|
| 171 |
} else {
|
| 172 |
-
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
|
| 178 |
-
if (Array.isArray(uploadedImageUrls)) {
|
| 179 |
-
imagesData.push(...uploadedImageUrls)
|
| 180 |
-
} else {
|
| 181 |
-
imagesData.push(uploadedImageUrls)
|
| 182 |
-
}
|
| 183 |
-
}
|
| 184 |
-
} catch (error) {
|
| 185 |
-
console.error(`[${new Date().toISOString()}] ❌ [${imageId}] 图片处理错误:`, error.message)
|
| 186 |
-
console.error(`[${new Date().toISOString()}] 🔍 [${imageId}] 错误堆栈:`, error.stack?.split('\n')[0] || 'No stack trace')
|
| 187 |
-
}
|
| 188 |
-
} else if (part.type === 'image' || part.type === 'image_base64') {
|
| 189 |
-
// 处理非标准格式的图片
|
| 190 |
-
const imageId = `img_alt_${Date.now()}_${Math.random().toString(36).substr(2, 5)}`
|
| 191 |
-
try {
|
| 192 |
-
console.log(`[${new Date().toISOString()}] 🖼️ [${imageId}] 处理非标准格式图片: ${part.type}`)
|
| 193 |
-
|
| 194 |
-
let imageData = part.image || part.image_data || part.data || part.url
|
| 195 |
-
if (imageData) {
|
| 196 |
-
if (imageData.startsWith('data:image')) {
|
| 197 |
-
const uploadedImageUrls = await imageUploader.uploadFromBase64(imageData)
|
| 198 |
-
if (Array.isArray(uploadedImageUrls)) {
|
| 199 |
-
imagesData.push(...uploadedImageUrls)
|
| 200 |
-
} else {
|
| 201 |
-
imagesData.push(uploadedImageUrls)
|
| 202 |
-
}
|
| 203 |
-
} else {
|
| 204 |
-
const uploadedImageUrls = await imageUploader.uploadFromUrl(imageData)
|
| 205 |
-
if (Array.isArray(uploadedImageUrls)) {
|
| 206 |
-
imagesData.push(...uploadedImageUrls)
|
| 207 |
-
} else {
|
| 208 |
-
imagesData.push(uploadedImageUrls)
|
| 209 |
-
}
|
| 210 |
-
}
|
| 211 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 212 |
} catch (error) {
|
| 213 |
-
|
| 214 |
-
|
| 215 |
-
|
| 216 |
-
|
| 217 |
-
|
| 218 |
-
try {
|
| 219 |
-
console.log(`[${new Date().toISOString()}] 🖼️ [${imageId}] 处理直接图片数据`)
|
| 220 |
-
|
| 221 |
-
let imageData = part.image || part.image_data || part.data
|
| 222 |
-
if (typeof imageData === 'string') {
|
| 223 |
-
if (imageData.startsWith('data:image')) {
|
| 224 |
-
const uploadedImageUrls = await imageUploader.uploadFromBase64(imageData)
|
| 225 |
-
if (Array.isArray(uploadedImageUrls)) {
|
| 226 |
-
imagesData.push(...uploadedImageUrls)
|
| 227 |
-
} else {
|
| 228 |
-
imagesData.push(uploadedImageUrls)
|
| 229 |
-
}
|
| 230 |
-
} else {
|
| 231 |
-
const uploadedImageUrls = await imageUploader.uploadFromUrl(imageData)
|
| 232 |
-
if (Array.isArray(uploadedImageUrls)) {
|
| 233 |
-
imagesData.push(...uploadedImageUrls)
|
| 234 |
-
} else {
|
| 235 |
-
imagesData.push(uploadedImageUrls)
|
| 236 |
-
}
|
| 237 |
-
}
|
| 238 |
}
|
| 239 |
-
|
| 240 |
-
|
| 241 |
}
|
| 242 |
}
|
| 243 |
}
|
|
@@ -255,41 +154,20 @@ async function convertOpenAIToMammouth(openaiRequest) {
|
|
| 255 |
imagesData: imagesData,
|
| 256 |
documentsData: []
|
| 257 |
}))
|
| 258 |
-
|
| 259 |
-
console.log(`[${new Date().toISOString()}] 📊 消息处理完成 - 文本内容: ${content.length}字符, 图片数量: ${imagesData.length}`)
|
| 260 |
}
|
| 261 |
|
| 262 |
-
// 统计总图片数
|
| 263 |
-
let totalImages = 0
|
| 264 |
-
regularMessages.forEach(message => {
|
| 265 |
-
if (Array.isArray(message.content)) {
|
| 266 |
-
message.content.forEach(part => {
|
| 267 |
-
if (part.type === 'image_url' || part.image || part.image_data || part.data) {
|
| 268 |
-
totalImages++
|
| 269 |
-
}
|
| 270 |
-
})
|
| 271 |
-
}
|
| 272 |
-
})
|
| 273 |
-
|
| 274 |
-
console.log(`[${new Date().toISOString()}] 🎯 请求转换完成`)
|
| 275 |
-
console.log(`[${new Date().toISOString()}] 📊 统计信息:`)
|
| 276 |
-
console.log(`[${new Date().toISOString()}] - 总消息数: ${regularMessages.length}`)
|
| 277 |
-
console.log(`[${new Date().toISOString()}] - 检测到的图片数: ${totalImages}`)
|
| 278 |
-
console.log(`[${new Date().toISOString()}] - 模型: ${form._streams ? form._streams.find(s => s[0] === 'model')?.[1] : '未知'}`)
|
| 279 |
-
console.log(`[${new Date().toISOString()}] - FormData类型: ${form.getHeaders ? 'valid' : 'invalid'}`)
|
| 280 |
-
|
| 281 |
return form
|
| 282 |
}
|
| 283 |
|
| 284 |
// 处理流数据
|
| 285 |
-
async function handleStreamResponse(axiosResponse, res, requestedModel) {
|
| 286 |
-
const
|
| 287 |
const timestamp = Math.floor(Date.now() / 1000)
|
| 288 |
const decoder = new TextDecoder()
|
| 289 |
|
| 290 |
// 发送初始角色数据
|
| 291 |
res.write(`data: ${JSON.stringify({
|
| 292 |
-
id: `chatcmpl-${
|
| 293 |
object: "chat.completion.chunk",
|
| 294 |
created: timestamp,
|
| 295 |
model: requestedModel,
|
|
@@ -307,7 +185,7 @@ async function handleStreamResponse(axiosResponse, res, requestedModel) {
|
|
| 307 |
const textToSend = chunkStr
|
| 308 |
if (textToSend) {
|
| 309 |
res.write(`data: ${JSON.stringify({
|
| 310 |
-
id: `chatcmpl-${
|
| 311 |
object: "chat.completion.chunk",
|
| 312 |
created: timestamp,
|
| 313 |
model: requestedModel,
|
|
@@ -323,7 +201,7 @@ async function handleStreamResponse(axiosResponse, res, requestedModel) {
|
|
| 323 |
axiosResponse.data.on('end', () => {
|
| 324 |
// 发送完成信号
|
| 325 |
res.write(`data: ${JSON.stringify({
|
| 326 |
-
id: `chatcmpl-${
|
| 327 |
object: "chat.completion.chunk",
|
| 328 |
created: timestamp,
|
| 329 |
model: requestedModel,
|
|
@@ -339,14 +217,19 @@ async function handleStreamResponse(axiosResponse, res, requestedModel) {
|
|
| 339 |
})
|
| 340 |
|
| 341 |
axiosResponse.data.on('error', (err) => {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 342 |
console.error('流数据处理错误:', err)
|
| 343 |
res.status(500).end()
|
| 344 |
})
|
| 345 |
}
|
| 346 |
|
| 347 |
// 处理非流数据
|
| 348 |
-
function handleNonStreamResponse(axiosResponse, res, requestedModel) {
|
| 349 |
-
const
|
| 350 |
const timestamp = Math.floor(Date.now() / 1000)
|
| 351 |
|
| 352 |
// 格式化为OpenAI的响应格式
|
|
@@ -357,20 +240,8 @@ function handleNonStreamResponse(axiosResponse, res, requestedModel) {
|
|
| 357 |
content = content.slice(1, -1);
|
| 358 |
}
|
| 359 |
|
| 360 |
-
// 安全地处理内容,避免循环引用
|
| 361 |
-
let safeContent = content
|
| 362 |
-
if (!safeContent) {
|
| 363 |
-
if (typeof axiosResponse.data === 'string') {
|
| 364 |
-
safeContent = axiosResponse.data
|
| 365 |
-
} else if (axiosResponse.data && axiosResponse.data.message) {
|
| 366 |
-
safeContent = axiosResponse.data.message
|
| 367 |
-
} else {
|
| 368 |
-
safeContent = "响应内容为空"
|
| 369 |
-
}
|
| 370 |
-
}
|
| 371 |
-
|
| 372 |
const responseData = {
|
| 373 |
-
id: `chatcmpl-${
|
| 374 |
object: "chat.completion",
|
| 375 |
created: timestamp,
|
| 376 |
model: requestedModel,
|
|
@@ -378,7 +249,7 @@ function handleNonStreamResponse(axiosResponse, res, requestedModel) {
|
|
| 378 |
index: 0,
|
| 379 |
message: {
|
| 380 |
role: "assistant",
|
| 381 |
-
content:
|
| 382 |
},
|
| 383 |
finish_reason: "stop"
|
| 384 |
}],
|
|
@@ -423,116 +294,40 @@ async function retryWithNewCookie(req, res, config, currentCookie, requestedMode
|
|
| 423 |
|
| 424 |
// OpenAI兼容的聊天完成API接口,使用中间件验证API密钥
|
| 425 |
router.post('/completions', authenticate, async (req, res) => {
|
| 426 |
-
|
| 427 |
-
const
|
| 428 |
|
| 429 |
try {
|
| 430 |
const openaiRequest = req.body
|
| 431 |
-
|
| 432 |
-
// 检查请求体是否存在
|
| 433 |
-
if (!openaiRequest) {
|
| 434 |
-
console.error(`[${timestamp}] ❌ [${requestId}] 请求体为空`)
|
| 435 |
-
return res.status(400).json({
|
| 436 |
-
error: {
|
| 437 |
-
message: '请求体为空',
|
| 438 |
-
type: 'invalid_request_error',
|
| 439 |
-
code: 'missing_body'
|
| 440 |
-
}
|
| 441 |
-
})
|
| 442 |
-
}
|
| 443 |
-
|
| 444 |
const isStreamRequest = openaiRequest.stream === true
|
| 445 |
const requestedModel = openaiRequest.model
|
| 446 |
|
| 447 |
-
|
| 448 |
-
|
| 449 |
-
|
| 450 |
-
|
| 451 |
-
|
| 452 |
-
|
| 453 |
-
|
| 454 |
-
console.log(`[${timestamp}] 🐛 [${requestId}] 请求体键: ${Object.keys(openaiRequest || {}).join(', ')}`)
|
| 455 |
-
if (openaiRequest.messages) {
|
| 456 |
-
console.log(`[${timestamp}] 🐛 [${requestId}] 消息结构预览:`)
|
| 457 |
-
openaiRequest.messages.forEach((msg, i) => {
|
| 458 |
-
console.log(`[${timestamp}] 🐛 [${requestId}] 消息${i}: role=${msg.role}, content类型=${Array.isArray(msg.content) ? 'array' : typeof msg.content}`)
|
| 459 |
-
})
|
| 460 |
-
|
| 461 |
-
// 详细的图片格式检测
|
| 462 |
-
debugImageFormats(openaiRequest.messages, requestId, timestamp)
|
| 463 |
-
}
|
| 464 |
-
|
| 465 |
-
// 检查是否包含图片 - 增强检测逻辑
|
| 466 |
-
let hasImages = false
|
| 467 |
-
let imageCount = 0
|
| 468 |
-
console.log(`[${timestamp}] 🔍 [${requestId}] 开始检查消息中的图片`)
|
| 469 |
-
|
| 470 |
-
if (openaiRequest.messages) {
|
| 471 |
-
openaiRequest.messages.forEach((msg, msgIndex) => {
|
| 472 |
-
console.log(`[${timestamp}] 📝 [${requestId}] 消息${msgIndex + 1} - 角色: ${msg.role}`)
|
| 473 |
-
console.log(`[${timestamp}] 📝 [${requestId}] 消息${msgIndex + 1} - 内容类型: ${Array.isArray(msg.content) ? '数组' : typeof msg.content}`)
|
| 474 |
-
|
| 475 |
-
if (Array.isArray(msg.content)) {
|
| 476 |
-
msg.content.forEach((part, partIndex) => {
|
| 477 |
-
console.log(`[${timestamp}] 🔍 [${requestId}] 消息${msgIndex + 1}-部分${partIndex + 1} - 类型: ${part.type}`)
|
| 478 |
-
console.log(`[${timestamp}] 🔍 [${requestId}] 消息${msgIndex + 1}-部分${partIndex + 1} - 完整内容:`, JSON.stringify(part, null, 2))
|
| 479 |
-
|
| 480 |
-
// 标准OpenAI格式检测
|
| 481 |
-
if (part.type === 'image_url') {
|
| 482 |
-
hasImages = true
|
| 483 |
-
imageCount++
|
| 484 |
-
console.log(`[${timestamp}] 🖼️ [${requestId}] 发现标准格式图片${imageCount}: ${part.image_url?.url ? part.image_url.url.substring(0, 50) + '...' : '无URL'}`)
|
| 485 |
-
}
|
| 486 |
-
// 检测其他可能的图片格式
|
| 487 |
-
else if (part.type === 'image' || part.type === 'image_base64') {
|
| 488 |
-
hasImages = true
|
| 489 |
-
imageCount++
|
| 490 |
-
console.log(`[${timestamp}] 🖼️ [${requestId}] 发现非标准格式图片${imageCount}: 类型=${part.type}`)
|
| 491 |
-
}
|
| 492 |
-
// 检测文本中是否包含base64图片
|
| 493 |
-
else if (part.type === 'text' && part.text && typeof part.text === 'string' && part.text.includes('data:image/')) {
|
| 494 |
-
hasImages = true
|
| 495 |
-
imageCount++
|
| 496 |
-
console.log(`[${timestamp}] 🖼️ [${requestId}] 在文本部分发现base64图片${imageCount}`)
|
| 497 |
-
}
|
| 498 |
-
// 检测是否有直接的图片数据字段
|
| 499 |
-
else if (part.image || part.image_data || part.data) {
|
| 500 |
-
hasImages = true
|
| 501 |
-
imageCount++
|
| 502 |
-
console.log(`[${timestamp}] 🖼️ [${requestId}] 发现直接图片数据${imageCount}`)
|
| 503 |
-
}
|
| 504 |
-
})
|
| 505 |
-
} else if (typeof msg.content === 'string') {
|
| 506 |
-
// 检查字符串内容是否包含base64图片
|
| 507 |
-
if (msg.content.includes('data:image/')) {
|
| 508 |
-
console.log(`[${timestamp}] 🖼️ [${requestId}] 在字符串消息中发���base64图片`)
|
| 509 |
-
hasImages = true
|
| 510 |
-
imageCount++
|
| 511 |
-
}
|
| 512 |
-
}
|
| 513 |
-
})
|
| 514 |
-
}
|
| 515 |
-
|
| 516 |
-
console.log(`[${timestamp}] 📊 [${requestId}] 图片检查结果: ${hasImages ? `发现${imageCount}张图片` : '未发现图片'}`)
|
| 517 |
|
| 518 |
// 设置适当的响应头
|
| 519 |
if (isStreamRequest) {
|
| 520 |
res.setHeader('Content-Type', 'text/event-stream')
|
| 521 |
res.setHeader('Cache-Control', 'no-cache')
|
| 522 |
res.setHeader('Connection', 'keep-alive')
|
| 523 |
-
console.log(`[${timestamp}] 🌊 [${requestId}] 设置流式响应头`)
|
| 524 |
}
|
| 525 |
|
| 526 |
// 转换请求格式
|
| 527 |
-
|
| 528 |
-
const form = await convertOpenAIToMammouth(openaiRequest)
|
| 529 |
-
console.log(`[${timestamp}] ✅ [${requestId}] 请求格式转换完成`)
|
| 530 |
|
| 531 |
// 获取Cookie - 根据模型类型使用不同的获取方法
|
| 532 |
const cookieValue = isUnlimitedModel(requestedModel)
|
| 533 |
? accountManager.getAnyCookie()
|
| 534 |
: accountManager.getNextAvailableCookie()
|
| 535 |
-
|
|
|
|
|
|
|
|
|
|
| 536 |
|
| 537 |
// 准备请求配置
|
| 538 |
const config = {
|
|
@@ -549,82 +344,51 @@ router.post('/completions', authenticate, async (req, res) => {
|
|
| 549 |
|
| 550 |
try {
|
| 551 |
// 发送请求到Mammouth API
|
| 552 |
-
|
| 553 |
-
console.log(`[${timestamp}] 🌐 [${requestId}] 请求URL: ${config.url}`)
|
| 554 |
-
console.log(`[${timestamp}] 📋 [${requestId}] 请求方法: ${config.method}`)
|
| 555 |
-
console.log(`[${timestamp}] 🍪 [${requestId}] Cookie: ${cookieValue ? cookieValue.substring(0, 8) + '...' : '无'}`)
|
| 556 |
-
|
| 557 |
-
// 记录FormData的基本信息(不记录具体内容避免日志过长)
|
| 558 |
-
if (form && form.getHeaders) {
|
| 559 |
-
const headers = form.getHeaders()
|
| 560 |
-
console.log(`[${timestamp}] 📦 [${requestId}] FormData Content-Type: ${headers['content-type']?.substring(0, 50)}...`)
|
| 561 |
-
}
|
| 562 |
-
|
| 563 |
const response = await axios(config)
|
| 564 |
-
|
| 565 |
-
|
| 566 |
-
|
| 567 |
-
|
| 568 |
-
let responseSize = 'unknown'
|
| 569 |
-
try {
|
| 570 |
-
if (typeof response.data === 'string') {
|
| 571 |
-
responseSize = response.data.length
|
| 572 |
-
} else if (response.data && typeof response.data === 'object') {
|
| 573 |
-
responseSize = 'object'
|
| 574 |
-
}
|
| 575 |
-
} catch (error) {
|
| 576 |
-
responseSize = 'cannot_measure'
|
| 577 |
-
}
|
| 578 |
-
console.log(`[${timestamp}] 📏 [${requestId}] 响应大小: ${responseSize} 字符`)
|
| 579 |
|
| 580 |
// 处理响应
|
| 581 |
if (isStreamRequest) {
|
| 582 |
-
|
| 583 |
-
handleStreamResponse(response, res, requestedModel)
|
| 584 |
} else {
|
| 585 |
-
|
| 586 |
-
handleNonStreamResponse(response, res, requestedModel)
|
| 587 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 588 |
} catch (error) {
|
|
|
|
|
|
|
|
|
|
|
|
|
| 589 |
// 优化错误日志打印,只打印关键信息
|
| 590 |
const errorStatus = error.response?.status || 'unknown'
|
| 591 |
const errorMessage = error.response?.data?.message || error.message || 'Unknown error'
|
| 592 |
-
const errorData = error.response?.data || 'No response data'
|
| 593 |
-
|
| 594 |
-
console.error(`[${timestamp}] ❌ [${requestId}] API转发错误: [${errorStatus}] ${errorMessage}`)
|
| 595 |
-
console.error(`[${timestamp}] 🔍 [${requestId}] 错误详情: ${error.stack?.split('\n')[0] || 'No stack trace'}`)
|
| 596 |
-
|
| 597 |
-
// 安全地记录响应数据,避免循环引用
|
| 598 |
-
try {
|
| 599 |
-
if (typeof errorData === 'string') {
|
| 600 |
-
console.error(`[${timestamp}] 📋 [${requestId}] 响应数据: ${errorData}`)
|
| 601 |
-
} else if (errorData && typeof errorData === 'object') {
|
| 602 |
-
// 只记录基本属性,避免循环引用
|
| 603 |
-
const safeErrorData = {
|
| 604 |
-
message: errorData.message,
|
| 605 |
-
error: errorData.error,
|
| 606 |
-
status: errorData.status,
|
| 607 |
-
code: errorData.code
|
| 608 |
-
}
|
| 609 |
-
console.error(`[${timestamp}] 📋 [${requestId}] 响应数据:`, JSON.stringify(safeErrorData, null, 2))
|
| 610 |
-
}
|
| 611 |
-
} catch (jsonError) {
|
| 612 |
-
console.error(`[${timestamp}] 📋 [${requestId}] 响应数据: [无法序列化]`)
|
| 613 |
-
}
|
| 614 |
|
| 615 |
-
|
| 616 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 617 |
|
| 618 |
-
|
| 619 |
-
if (hasImages) {
|
| 620 |
-
console.error(`[${timestamp}] 🖼️ [${requestId}] 图片处理信息: 包含${imageCount}张图片`)
|
| 621 |
-
}
|
| 622 |
|
| 623 |
// 如果是403错误(达到使用限制)
|
| 624 |
if (error.response && error.response.status === 403) {
|
| 625 |
// console.log(error)
|
| 626 |
|
| 627 |
-
console.log(`
|
| 628 |
|
| 629 |
// 根据模型类型进行不同处理
|
| 630 |
if (isUnlimitedModel(requestedModel)) {
|
|
@@ -741,6 +505,11 @@ router.post('/completions', authenticate, async (req, res) => {
|
|
| 741 |
}
|
| 742 |
} else {
|
| 743 |
// 其他错误
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 744 |
res.status(500).json({
|
| 745 |
error: {
|
| 746 |
message: '处理请求时发生错误',
|
|
@@ -751,6 +520,17 @@ router.post('/completions', authenticate, async (req, res) => {
|
|
| 751 |
}
|
| 752 |
}
|
| 753 |
} catch (error) {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 754 |
console.error('请求处理错误:', error)
|
| 755 |
res.status(500).json({
|
| 756 |
error: {
|
|
|
|
| 5 |
const { MODEL_MAPPING, MAMMOUTH_API_URL, AUTH_TOKEN, UNLIMITED_MODELS } = require('../config')
|
| 6 |
const accountManager = require('../lib/manager')
|
| 7 |
const imageUploader = require('../lib/uploader')
|
| 8 |
+
const logger = require('../lib/logger')
|
| 9 |
|
| 10 |
const router = express.Router()
|
| 11 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 12 |
// API密钥认证中间件
|
| 13 |
const authenticate = (req, res, next) => {
|
| 14 |
const authHeader = req.headers.authorization || req.headers.Authorization || req.headers['x-api-key']
|
|
|
|
| 44 |
}
|
| 45 |
|
| 46 |
// 将OpenAI格式转换为Mammouth格式
|
| 47 |
+
async function convertOpenAIToMammouth(openaiRequest, requestId = null) {
|
| 48 |
const form = new FormData()
|
| 49 |
|
| 50 |
// 模型选择
|
|
|
|
| 69 |
form.append('preprompt', preprompt)
|
| 70 |
|
| 71 |
// 处理非system角色的消息
|
| 72 |
+
let totalImageCount = 0
|
| 73 |
+
|
| 74 |
+
// 先统计图片总数用于日志
|
| 75 |
+
regularMessages.forEach(message => {
|
| 76 |
+
if (Array.isArray(message.content)) {
|
| 77 |
+
totalImageCount += message.content.filter(part => part.type === 'image_url').length
|
| 78 |
+
}
|
| 79 |
+
})
|
| 80 |
+
|
| 81 |
+
if (requestId && totalImageCount > 0) {
|
| 82 |
+
logger.logImageProcessingStart(requestId, totalImageCount)
|
| 83 |
+
}
|
| 84 |
+
|
| 85 |
+
let currentImageIndex = 0
|
| 86 |
+
|
| 87 |
for (const message of regularMessages) {
|
| 88 |
// 处理包含图片的消息
|
| 89 |
const imagesData = []
|
|
|
|
| 95 |
|
| 96 |
// 遍历内容部分
|
| 97 |
for (const part of message.content) {
|
|
|
|
|
|
|
| 98 |
if (part.type === 'text') {
|
| 99 |
textParts.push(part.text)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 100 |
} else if (part.type === 'image_url') {
|
|
|
|
| 101 |
try {
|
|
|
|
|
|
|
| 102 |
// 获取图片数据
|
| 103 |
let imageUrl = part.image_url
|
| 104 |
if (typeof imageUrl === 'object' && imageUrl.url) {
|
| 105 |
imageUrl = imageUrl.url
|
| 106 |
}
|
| 107 |
|
| 108 |
+
// 使用智能上传方法处理图片(支持长图)
|
| 109 |
+
let uploadedUrls = []
|
|
|
|
|
|
|
| 110 |
if (imageUrl.startsWith('data:image')) {
|
| 111 |
+
uploadedUrls = await imageUploader.uploadFromBase64Smart(
|
| 112 |
+
imageUrl,
|
| 113 |
+
null,
|
| 114 |
+
requestId,
|
| 115 |
+
currentImageIndex
|
| 116 |
+
)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 117 |
} else {
|
| 118 |
+
uploadedUrls = await imageUploader.uploadFromUrlSmart(
|
| 119 |
+
imageUrl,
|
| 120 |
+
null,
|
| 121 |
+
requestId,
|
| 122 |
+
currentImageIndex
|
| 123 |
+
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 124 |
}
|
| 125 |
+
|
| 126 |
+
// 将所有上传的URL添加到imagesData中
|
| 127 |
+
// 对于长图,这将包含多个片段的URL
|
| 128 |
+
imagesData.push(...uploadedUrls)
|
| 129 |
+
|
| 130 |
+
currentImageIndex++
|
| 131 |
} catch (error) {
|
| 132 |
+
if (requestId) {
|
| 133 |
+
logger.logError(requestId, 'IMAGE_PROCESSING_ERROR', error.message, {
|
| 134 |
+
imageIndex: currentImageIndex,
|
| 135 |
+
imageUrl: typeof part.image_url === 'string' ? part.image_url.substring(0, 100) : 'object'
|
| 136 |
+
})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 137 |
}
|
| 138 |
+
console.error('图片处理错误:', error.message)
|
| 139 |
+
currentImageIndex++
|
| 140 |
}
|
| 141 |
}
|
| 142 |
}
|
|
|
|
| 154 |
imagesData: imagesData,
|
| 155 |
documentsData: []
|
| 156 |
}))
|
|
|
|
|
|
|
| 157 |
}
|
| 158 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 159 |
return form
|
| 160 |
}
|
| 161 |
|
| 162 |
// 处理流数据
|
| 163 |
+
async function handleStreamResponse(axiosResponse, res, requestedModel, logRequestId = null) {
|
| 164 |
+
const responseId = uuidv4()
|
| 165 |
const timestamp = Math.floor(Date.now() / 1000)
|
| 166 |
const decoder = new TextDecoder()
|
| 167 |
|
| 168 |
// 发送初始角色数据
|
| 169 |
res.write(`data: ${JSON.stringify({
|
| 170 |
+
id: `chatcmpl-${responseId}`,
|
| 171 |
object: "chat.completion.chunk",
|
| 172 |
created: timestamp,
|
| 173 |
model: requestedModel,
|
|
|
|
| 185 |
const textToSend = chunkStr
|
| 186 |
if (textToSend) {
|
| 187 |
res.write(`data: ${JSON.stringify({
|
| 188 |
+
id: `chatcmpl-${responseId}`,
|
| 189 |
object: "chat.completion.chunk",
|
| 190 |
created: timestamp,
|
| 191 |
model: requestedModel,
|
|
|
|
| 201 |
axiosResponse.data.on('end', () => {
|
| 202 |
// 发送完成信号
|
| 203 |
res.write(`data: ${JSON.stringify({
|
| 204 |
+
id: `chatcmpl-${responseId}`,
|
| 205 |
object: "chat.completion.chunk",
|
| 206 |
created: timestamp,
|
| 207 |
model: requestedModel,
|
|
|
|
| 217 |
})
|
| 218 |
|
| 219 |
axiosResponse.data.on('error', (err) => {
|
| 220 |
+
if (logRequestId) {
|
| 221 |
+
logger.logError(logRequestId, 'STREAM_ERROR', err.message, {
|
| 222 |
+
model: requestedModel
|
| 223 |
+
})
|
| 224 |
+
}
|
| 225 |
console.error('流数据处理错误:', err)
|
| 226 |
res.status(500).end()
|
| 227 |
})
|
| 228 |
}
|
| 229 |
|
| 230 |
// 处理非流数据
|
| 231 |
+
function handleNonStreamResponse(axiosResponse, res, requestedModel, logRequestId = null) {
|
| 232 |
+
const responseId = uuidv4()
|
| 233 |
const timestamp = Math.floor(Date.now() / 1000)
|
| 234 |
|
| 235 |
// 格式化为OpenAI的响应格式
|
|
|
|
| 240 |
content = content.slice(1, -1);
|
| 241 |
}
|
| 242 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 243 |
const responseData = {
|
| 244 |
+
id: `chatcmpl-${responseId}`,
|
| 245 |
object: "chat.completion",
|
| 246 |
created: timestamp,
|
| 247 |
model: requestedModel,
|
|
|
|
| 249 |
index: 0,
|
| 250 |
message: {
|
| 251 |
role: "assistant",
|
| 252 |
+
content: content || axiosResponse.data
|
| 253 |
},
|
| 254 |
finish_reason: "stop"
|
| 255 |
}],
|
|
|
|
| 294 |
|
| 295 |
// OpenAI兼容的聊天完成API接口,使用中间件验证API密钥
|
| 296 |
router.post('/completions', authenticate, async (req, res) => {
|
| 297 |
+
let requestId = null
|
| 298 |
+
const startTime = Date.now()
|
| 299 |
|
| 300 |
try {
|
| 301 |
const openaiRequest = req.body
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 302 |
const isStreamRequest = openaiRequest.stream === true
|
| 303 |
const requestedModel = openaiRequest.model
|
| 304 |
|
| 305 |
+
// 记录请求开始
|
| 306 |
+
requestId = logger.logRequestStart(
|
| 307 |
+
req.method,
|
| 308 |
+
req.originalUrl,
|
| 309 |
+
req.headers,
|
| 310 |
+
openaiRequest
|
| 311 |
+
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 312 |
|
| 313 |
// 设置适当的响应头
|
| 314 |
if (isStreamRequest) {
|
| 315 |
res.setHeader('Content-Type', 'text/event-stream')
|
| 316 |
res.setHeader('Cache-Control', 'no-cache')
|
| 317 |
res.setHeader('Connection', 'keep-alive')
|
|
|
|
| 318 |
}
|
| 319 |
|
| 320 |
// 转换请求格式
|
| 321 |
+
const form = await convertOpenAIToMammouth(openaiRequest, requestId)
|
|
|
|
|
|
|
| 322 |
|
| 323 |
// 获取Cookie - 根据模型类型使用不同的获取方法
|
| 324 |
const cookieValue = isUnlimitedModel(requestedModel)
|
| 325 |
? accountManager.getAnyCookie()
|
| 326 |
: accountManager.getNextAvailableCookie()
|
| 327 |
+
|
| 328 |
+
// 记录模型调用开始
|
| 329 |
+
const mammouthModel = MODEL_MAPPING[requestedModel] || requestedModel
|
| 330 |
+
logger.logModelCallStart(requestId, requestedModel, mammouthModel)
|
| 331 |
|
| 332 |
// 准备请求配置
|
| 333 |
const config = {
|
|
|
|
| 344 |
|
| 345 |
try {
|
| 346 |
// 发送请求到Mammouth API
|
| 347 |
+
const modelCallStartTime = Date.now()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 348 |
const response = await axios(config)
|
| 349 |
+
const modelCallDuration = Date.now() - modelCallStartTime
|
| 350 |
+
|
| 351 |
+
// 记录模型调用成功
|
| 352 |
+
logger.logModelCallEnd(requestId, true, null, modelCallDuration)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 353 |
|
| 354 |
// 处理响应
|
| 355 |
if (isStreamRequest) {
|
| 356 |
+
handleStreamResponse(response, res, requestedModel, requestId)
|
|
|
|
| 357 |
} else {
|
| 358 |
+
handleNonStreamResponse(response, res, requestedModel, requestId)
|
|
|
|
| 359 |
}
|
| 360 |
+
|
| 361 |
+
// 记录请求成功结束
|
| 362 |
+
logger.logRequestEnd(requestId, 200, {
|
| 363 |
+
responseType: isStreamRequest ? 'stream' : 'json',
|
| 364 |
+
totalDuration: Date.now() - startTime
|
| 365 |
+
})
|
| 366 |
+
|
| 367 |
+
} catch (error) {
|
| 368 |
} catch (error) {
|
| 369 |
+
// 记录模型调用失败
|
| 370 |
+
const modelCallDuration = Date.now() - startTime
|
| 371 |
+
logger.logModelCallEnd(requestId, false, error.message, modelCallDuration)
|
| 372 |
+
|
| 373 |
// 优化错误日志打印,只打印关键信息
|
| 374 |
const errorStatus = error.response?.status || 'unknown'
|
| 375 |
const errorMessage = error.response?.data?.message || error.message || 'Unknown error'
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 376 |
|
| 377 |
+
// 记录详细错误信息
|
| 378 |
+
logger.logError(requestId, 'MODEL_CALL_ERROR', errorMessage, {
|
| 379 |
+
status: errorStatus,
|
| 380 |
+
model: requestedModel,
|
| 381 |
+
isStream: isStreamRequest,
|
| 382 |
+
cookieUsed: cookieValue?.substring(0, 8) + '...'
|
| 383 |
+
})
|
| 384 |
|
| 385 |
+
console.error(`API转发错误: [${errorStatus}] ${errorMessage}`)
|
|
|
|
|
|
|
|
|
|
| 386 |
|
| 387 |
// 如果是403错误(达到使用限制)
|
| 388 |
if (error.response && error.response.status === 403) {
|
| 389 |
// console.log(error)
|
| 390 |
|
| 391 |
+
console.log(`账号 ${cookieValue.substring(0, 5)}... 使用模型 ${requestedModel} 已达到使用限制`)
|
| 392 |
|
| 393 |
// 根据模型类型进行不同处理
|
| 394 |
if (isUnlimitedModel(requestedModel)) {
|
|
|
|
| 505 |
}
|
| 506 |
} else {
|
| 507 |
// 其他错误
|
| 508 |
+
logger.logRequestEnd(requestId, 500, {
|
| 509 |
+
error: error.message,
|
| 510 |
+
totalDuration: Date.now() - startTime
|
| 511 |
+
})
|
| 512 |
+
|
| 513 |
res.status(500).json({
|
| 514 |
error: {
|
| 515 |
message: '处理请求时发生错误',
|
|
|
|
| 520 |
}
|
| 521 |
}
|
| 522 |
} catch (error) {
|
| 523 |
+
// 记录最终错误
|
| 524 |
+
if (requestId) {
|
| 525 |
+
logger.logError(requestId, 'REQUEST_ERROR', error.message, {
|
| 526 |
+
totalDuration: Date.now() - startTime
|
| 527 |
+
})
|
| 528 |
+
logger.logRequestEnd(requestId, 500, {
|
| 529 |
+
error: error.message,
|
| 530 |
+
totalDuration: Date.now() - startTime
|
| 531 |
+
})
|
| 532 |
+
}
|
| 533 |
+
|
| 534 |
console.error('请求处理错误:', error)
|
| 535 |
res.status(500).json({
|
| 536 |
error: {
|