File size: 33,089 Bytes
f049116 86c3fd8 ed0c017 f049116 86c3fd8 f049116 d995792 faba1b4 d995792 f049116 89082f1 f049116 86c3fd8 86e09a1 86c3fd8 86e09a1 86c3fd8 86e09a1 86c3fd8 f049116 d837981 f049116 d837981 f049116 d837981 f049116 d837981 f049116 d837981 23b00a5 e6b1cd3 23b00a5 e6b1cd3 23b00a5 0b715ff db50d18 5cdca7f 0b715ff 86c3fd8 0b715ff d837981 0b715ff d837981 0b715ff d837981 0b715ff ebc87b9 0b715ff db50d18 0b715ff 89082f1 d837981 0b715ff 89082f1 0b715ff db50d18 89082f1 0b715ff 5cdca7f 0b715ff db50d18 0b715ff 5cdca7f 0b715ff d837981 0b715ff 311ba51 db50d18 0b715ff 5cdca7f 0b715ff f049116 db50d18 0b715ff e6b1cd3 0b715ff db50d18 0b715ff db50d18 0b715ff db50d18 0b715ff db50d18 0b715ff db50d18 0b715ff db50d18 0b715ff e6b1cd3 0b715ff db50d18 0b715ff e6b1cd3 23b00a5 0b715ff 23b00a5 e6b1cd3 23b00a5 0b715ff 23b00a5 d837981 f049116 d837981 0b715ff d837981 f049116 faba1b4 f049116 9026a2c 86c3fd8 f049116 091b154 faba1b4 091b154 f049116 091b154 86c3fd8 f049116 091b154 f049116 091b154 f049116 c00016a 091b154 faba1b4 091b154 faba1b4 d995792 091b154 9026a2c d995792 faba1b4 d995792 9026a2c d995792 9026a2c d995792 9026a2c d995792 9026a2c faba1b4 d995792 9026a2c faba1b4 091b154 f049116 091b154 86c3fd8 f049116 091b154 f049116 091b154 f049116 091b154 f049116 86c3fd8 091b154 86c3fd8 091b154 86c3fd8 f049116 86c3fd8 f049116 86c3fd8 f049116 86c3fd8 f049116 9026a2c f049116 86c3fd8 a863a8a f049116 86c3fd8 a863a8a f049116 86c3fd8 f049116 86c3fd8 f049116 faba1b4 f049116 86c3fd8 f049116 86c3fd8 f049116 9026a2c f049116 86c3fd8 f049116 86c3fd8 f049116 86c3fd8 f049116 d33bb2e 86c3fd8 35b6e22 86c3fd8 f049116 86c3fd8 f049116 9026a2c f049116 ed0c017 86c3fd8 ed0c017 f049116 ed0c017 86c3fd8 ed0c017 f049116 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 | 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 |