qwen-api-proxy 1.0.11 → 1.0.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/api/routes.js CHANGED
@@ -2,12 +2,12 @@ import express from 'express';
2
2
  import { sendMessage, getAllModels, getApiKeys, createChatV2, pollTaskStatus, pagePool, extractAuthToken } from './chat.js';
3
3
  import { getAuthenticationStatus, getBrowserContext } from '../browser/browser.js';
4
4
  import { checkAuthentication } from '../browser/auth.js';
5
- import { logInfo, logError, logDebug } from '../logger/index.js';
5
+ import { logInfo, logError, logDebug, logWarn } from '../logger/index.js';
6
6
  import { getMappedModel } from './modelMapping.js';
7
7
  import { getStsToken, uploadFileToQwen } from './fileUpload.js';
8
8
  import { loadHistory, saveHistory } from './chatHistory.js';
9
9
  import { generateImage, getAvailableImageModels, checkImageApiAvailability } from './imageGeneration.js';
10
- import { MAX_FILE_SIZE, UPLOADS_DIR, STREAMING_CHUNK_DELAY, ALLOW_UNSCOPED_SESSION_CHAT_RESTORE, IMAGE_GENERATION_MODE, DASHSCOPE_API_KEY, FORCE_NEW_CHAT_PER_REQUEST } from '../config.js';
10
+ import { MAX_FILE_SIZE, UPLOADS_DIR, STREAMING_CHUNK_DELAY, ALLOW_UNSCOPED_SESSION_CHAT_RESTORE, IMAGE_GENERATION_MODE, DASHSCOPE_API_KEY, FORCE_NEW_CHAT_PER_REQUEST, SESSION_DIR } from '../config.js';
11
11
  import { getActiveModel } from '../utils/botSettings.js';
12
12
  import { getFileDownloadProxyAgent } from '../utils/proxy.js';
13
13
  import multer from 'multer';
@@ -24,7 +24,7 @@ import { listTokens, markInvalid, markRateLimited, markValid } from './tokenMana
24
24
  * 1. Base64 data URLs (data:image/jpeg;base64,...)
25
25
  * 2. HTTP/HTTPS URLs (скачивает и загружает в OSS)
26
26
  * 3. Локальные файлы (загруженные через multer)
27
- *
27
+ *
28
28
  * Возвращает массив в формате: [{type: 'image', image: 'url'}] или [{type: 'file', file: 'url'}]
29
29
  */
30
30
  async function processFilesForQwen(files) {
@@ -38,7 +38,7 @@ async function processFilesForQwen(files) {
38
38
  try {
39
39
  for (const file of files) {
40
40
  let fileUrl = null;
41
-
41
+
42
42
  // Формат 1: {url: 'data:image/...;base64,...'} или {url: 'http://...'}
43
43
  if (file.url) {
44
44
  // Если это base64 data URL - сохраняем во временный файл и загружаем в OSS
@@ -46,15 +46,15 @@ async function processFilesForQwen(files) {
46
46
  const [header, base64Data] = file.url.split(',');
47
47
  const mimeType = header.match(/data:([^;]+)/)?.[1] || 'application/octet-stream';
48
48
  const ext = mimeType.split('/')[1]?.split('+')[0] || 'bin';
49
-
49
+
50
50
  const tempFileName = `${Date.now()}-${crypto.randomBytes(8).toString('hex')}.${ext}`;
51
51
  const tempFilePath = path.join(UPLOADS_DIR, tempFileName);
52
-
52
+
53
53
  fs.writeFileSync(tempFilePath, Buffer.from(base64Data, 'base64'));
54
54
  tempFiles.push(tempFilePath);
55
-
55
+
56
56
  logInfo(`📁 Base64 файл сохранен: ${tempFilePath}`);
57
-
57
+
58
58
  // Загружаем в OSS
59
59
  const uploadResult = await uploadFileToQwen(tempFilePath);
60
60
  if (uploadResult.success) {
@@ -68,13 +68,13 @@ async function processFilesForQwen(files) {
68
68
  // HTTP/HTTPS URL - скачиваем файл и загружаем в OSS
69
69
  logInfo(`📥 Скачивание файла: ${file.url}`);
70
70
  logInfo(`📥 Хост файла: ${new URL(file.url).hostname}`);
71
-
71
+
72
72
  try {
73
73
  // Получаем прокси агент если настроен
74
74
  const downloadProxyAgent = getFileDownloadProxyAgent();
75
-
75
+
76
76
  let response;
77
-
77
+
78
78
  if (downloadProxyAgent) {
79
79
  // Используем node-fetch с прокси
80
80
  const { default: nodeFetch } = await import('node-fetch');
@@ -91,36 +91,36 @@ async function processFilesForQwen(files) {
91
91
  redirect: 'follow'
92
92
  });
93
93
  }
94
-
94
+
95
95
  if (!response.ok) {
96
96
  const errorBody = await response.text().catch(() => '');
97
97
  logError(`❌ Ошибка скачивания: ${response.status} ${response.statusText}${errorBody ? ` | Ответ: ${errorBody.substring(0, 500)}` : ''}`);
98
98
  continue;
99
99
  }
100
-
100
+
101
101
  // Определяем тип файла из Content-Type или расширения URL
102
102
  const contentType = response.headers.get('content-type') || '';
103
103
  const urlExt = file.url.split('.').pop()?.split('?')[0] || '';
104
104
  let ext = 'bin';
105
-
106
- if (contentType.includes('image/jpeg')) ext = 'jpg';
107
- else if (contentType.includes('image/png')) ext = 'png';
108
- else if (contentType.includes('image/gif')) ext = 'gif';
109
- else if (contentType.includes('image/webp')) ext = 'webp';
105
+
106
+ if (contentType.includes('image/jpeg')) {ext = 'jpg';}
107
+ else if (contentType.includes('image/png')) {ext = 'png';}
108
+ else if (contentType.includes('image/gif')) {ext = 'gif';}
109
+ else if (contentType.includes('image/webp')) {ext = 'webp';}
110
110
  else if (['jpg', 'jpeg', 'png', 'gif', 'webp', 'bmp', 'pdf', 'txt', 'doc', 'docx'].includes(urlExt)) {
111
111
  ext = urlExt;
112
112
  }
113
-
113
+
114
114
  // Скачиваем во временный файл
115
115
  const tempFileName = `${Date.now()}-${crypto.randomBytes(8).toString('hex')}.${ext}`;
116
116
  const tempFilePath = path.join(UPLOADS_DIR, tempFileName);
117
-
117
+
118
118
  const buffer = Buffer.from(await response.arrayBuffer());
119
119
  fs.writeFileSync(tempFilePath, buffer);
120
120
  tempFiles.push(tempFilePath);
121
-
121
+
122
122
  logInfo(`📁 Файл скачан: ${tempFilePath} (${buffer.length} байт)`);
123
-
123
+
124
124
  // Загружаем в OSS
125
125
  const uploadResult = await uploadFileToQwen(tempFilePath);
126
126
  if (uploadResult.success) {
@@ -131,7 +131,7 @@ async function processFilesForQwen(files) {
131
131
  continue;
132
132
  }
133
133
  } catch (error) {
134
- logError(`❌ Ошибка при скачивании файла`, error);
134
+ logError('❌ Ошибка при скачивании файла', error);
135
135
  continue;
136
136
  }
137
137
  } else {
@@ -151,7 +151,7 @@ async function processFilesForQwen(files) {
151
151
  continue;
152
152
  }
153
153
  }
154
-
154
+
155
155
  // Определяем тип файла и добавляем в правильном формате
156
156
  if (fileUrl) {
157
157
  const isImage = fileUrl.match(/\.(jpg|jpeg|png|gif|webp|bmp)(\?|$)/i);
@@ -176,14 +176,14 @@ async function processFilesForQwen(files) {
176
176
  return qwenFiles;
177
177
  } catch (error) {
178
178
  logError('Ошибка при обработке файлов', error);
179
-
179
+
180
180
  // Очищаем временные файлы при ошибке
181
181
  for (const tempFile of tempFiles) {
182
182
  try {
183
183
  fs.unlinkSync(tempFile);
184
184
  } catch (e) { /* ignore */ }
185
185
  }
186
-
186
+
187
187
  throw error;
188
188
  }
189
189
  }
@@ -193,47 +193,47 @@ function generateChatIdFromHistory(messages) {
193
193
  if (!Array.isArray(messages) || messages.length === 0) {
194
194
  return null;
195
195
  }
196
-
196
+
197
197
  // Фильтруем служебные сообщения Open WebUI
198
198
  // Игнорируем сообщения, которые начинаются с "### Task:" или "History:"
199
- const realMessages = messages.filter(m => {
200
- if (m.role !== 'user') return true;
199
+ const realMessages = messages.filter((m) => {
200
+ if (m.role !== 'user') {return true;}
201
201
  const content = typeof m.content === 'string' ? m.content : '';
202
202
  return !content.startsWith('### Task:') && !content.startsWith('History:');
203
203
  });
204
-
204
+
205
205
  // Если остались только служебные сообщения, используем исходные
206
206
  const messagesToUse = realMessages.length > 0 ? realMessages : messages;
207
-
207
+
208
208
  // Используем хеш первого реального сообщения пользователя для создания стабильного ID
209
209
  const userMessages = messagesToUse
210
- .filter(m => m.role === 'user')
210
+ .filter((m) => m.role === 'user')
211
211
  .slice(0, 1) // Берём первое сообщение пользователя
212
- .map(m => typeof m.content === 'string' ? m.content : JSON.stringify(m.content))
212
+ .map((m) => typeof m.content === 'string' ? m.content : JSON.stringify(m.content))
213
213
  .join('||');
214
-
215
- if (!userMessages) return null;
216
-
214
+
215
+ if (!userMessages) {return null;}
216
+
217
217
  // Создаём хеш для детерминированного ID
218
218
  const hash = crypto
219
219
  .createHash('sha256')
220
220
  .update(userMessages)
221
221
  .digest('hex')
222
222
  .substring(0, 16);
223
-
223
+
224
224
  return `chat_${hash}`;
225
225
  }
226
226
 
227
227
  function normalizeIdValue(value) {
228
- if (value === null || value === undefined) return null;
229
- if (typeof value === 'number' || typeof value === 'bigint') return String(value);
230
- if (typeof value !== 'string') return null;
228
+ if (value === null || value === undefined) {return null;}
229
+ if (typeof value === 'number' || typeof value === 'bigint') {return String(value);}
230
+ if (typeof value !== 'string') {return null;}
231
231
 
232
232
  const trimmed = value.trim();
233
- if (!trimmed) return null;
233
+ if (!trimmed) {return null;}
234
234
 
235
235
  const lower = trimmed.toLowerCase();
236
- if (lower === 'null' || lower === 'undefined') return null;
236
+ if (lower === 'null' || lower === 'undefined') {return null;}
237
237
 
238
238
  return trimmed;
239
239
  }
@@ -241,14 +241,14 @@ function normalizeIdValue(value) {
241
241
  function pickFirstId(candidates) {
242
242
  for (const candidate of candidates) {
243
243
  const normalized = normalizeIdValue(candidate);
244
- if (normalized) return normalized;
244
+ if (normalized) {return normalized;}
245
245
  }
246
246
  return null;
247
247
  }
248
248
 
249
249
  function buildInternalChatIdFromHint(hint) {
250
250
  const normalizedHint = normalizeIdValue(hint);
251
- if (!normalizedHint) return null;
251
+ if (!normalizedHint) {return null;}
252
252
 
253
253
  const hash = crypto
254
254
  .createHash('sha256')
@@ -296,9 +296,9 @@ function extractParentHint(req) {
296
296
  }
297
297
 
298
298
  function isTruthyFlag(value) {
299
- if (typeof value === 'boolean') return value;
300
- if (typeof value === 'number') return value === 1;
301
- if (typeof value !== 'string') return false;
299
+ if (typeof value === 'boolean') {return value;}
300
+ if (typeof value === 'number') {return value === 1;}
301
+ if (typeof value !== 'string') {return false;}
302
302
  return ['1', 'true', 'yes', 'on'].includes(value.trim().toLowerCase());
303
303
  }
304
304
 
@@ -367,22 +367,22 @@ async function resolveQwenChatId(effectiveChatId, mappedModel) {
367
367
  import { testToken } from './chat.js';
368
368
 
369
369
  function isOpenWebUiMetaRequest(messages) {
370
- if (!Array.isArray(messages) || messages.length === 0) return false;
371
- const lastUserMessage = messages.filter(m => m && m.role === 'user').pop();
372
- if (!lastUserMessage) return false;
370
+ if (!Array.isArray(messages) || messages.length === 0) {return false;}
371
+ const lastUserMessage = messages.filter((m) => m && m.role === 'user').pop();
372
+ if (!lastUserMessage) {return false;}
373
373
 
374
374
  const content = lastUserMessage.content;
375
- if (Array.isArray(content)) return false; // multimodal / normal user message
376
- if (typeof content !== 'string') return false;
375
+ if (Array.isArray(content)) {return false;} // multimodal / normal user message
376
+ if (typeof content !== 'string') {return false;}
377
377
 
378
378
  const text = content.trimStart();
379
379
 
380
380
  // OpenWebUI background/meta prompts that should not reuse the main chatId/session.
381
- if (text.startsWith('### Task:')) return true;
382
- if (text.startsWith('History:')) return true;
381
+ if (text.startsWith('### Task:')) {return true;}
382
+ if (text.startsWith('History:')) {return true;}
383
383
 
384
384
  // Some variants embed history blocks and task instructions.
385
- if (text.includes('<chat_history>') && text.includes('### Task:')) return true;
385
+ if (text.includes('<chat_history>') && text.includes('### Task:')) {return true;}
386
386
 
387
387
  return false;
388
388
  }
@@ -431,7 +431,7 @@ function saveChatIdForSession(req, chatId, parentId, scope = null) {
431
431
  timestamp: Date.now()
432
432
  });
433
433
 
434
- const scopeSuffix = normalizedScope ? ` (scope=${normalizedScope})` : "";
434
+ const scopeSuffix = normalizedScope ? ` (scope=${normalizedScope})` : '';
435
435
  logDebug(`Saved chatId ${chatId} for session ${sessionKey.substring(0, 8)}${scopeSuffix}`);
436
436
  }
437
437
  // Очистка старых сессий каждые 10 минут
@@ -457,7 +457,7 @@ const router = express.Router();
457
457
  const storage = multer.diskStorage({
458
458
  destination(req, file, cb) {
459
459
  const uploadDir = path.join(process.cwd(), UPLOADS_DIR);
460
- if (!fs.existsSync(uploadDir)) fs.mkdirSync(uploadDir, { recursive: true });
460
+ if (!fs.existsSync(uploadDir)) {fs.mkdirSync(uploadDir, { recursive: true });}
461
461
  cb(null, uploadDir);
462
462
  },
463
463
  filename(req, file, cb) {
@@ -471,7 +471,7 @@ const upload = multer({ storage, limits: { fileSize: MAX_FILE_SIZE } });
471
471
 
472
472
  function authMiddleware(req, res, next) {
473
473
  const apiKeys = getApiKeys();
474
- if (apiKeys.length === 0) return next();
474
+ if (apiKeys.length === 0) {return next();}
475
475
 
476
476
  const authHeader = req.headers.authorization;
477
477
  if (!authHeader || !authHeader.startsWith('Bearer ')) {
@@ -496,20 +496,20 @@ router.use((req, res, next) => {
496
496
  // ─── Helpers: message parsing ────────────────────────────────────────────────
497
497
 
498
498
  async function parseOpenAIMessages(messages) {
499
- const systemMsg = messages.find(msg => msg.role === 'system');
499
+ const systemMsg = messages.find((msg) => msg.role === 'system');
500
500
  const systemMessage = systemMsg ? systemMsg.content : null;
501
- const lastUserMessage = messages.filter(msg => msg.role === 'user').pop();
502
-
501
+ const lastUserMessage = messages.filter((msg) => msg.role === 'user').pop();
502
+
503
503
  if (!lastUserMessage) {
504
504
  return { messageContent: null, systemMessage };
505
505
  }
506
-
506
+
507
507
  let messageContent = lastUserMessage.content;
508
508
  const extractedFiles = [];
509
-
509
+
510
510
  // Преобразуем OpenAI format content array во внутренний формат
511
511
  if (Array.isArray(messageContent)) {
512
- messageContent = messageContent.map(item => {
512
+ messageContent = messageContent.map((item) => {
513
513
  if (item.type === 'text') {
514
514
  return { type: 'text', text: item.text };
515
515
  } else if (item.type === 'image_url' && item.image_url) {
@@ -523,25 +523,25 @@ async function parseOpenAIMessages(messages) {
523
523
  return null;
524
524
  }
525
525
  return item;
526
- }).filter(item => item !== null); // Убираем null
526
+ }).filter((item) => item !== null); // Убираем null
527
527
  }
528
-
528
+
529
529
  // Поддерживаем также формат: content: 'text', files: [{url: '...'}]
530
530
  // Добавляем файлы из lastUserMessage.files в extractedFiles
531
531
  if (lastUserMessage.files && Array.isArray(lastUserMessage.files)) {
532
- lastUserMessage.files.forEach(f => {
532
+ lastUserMessage.files.forEach((f) => {
533
533
  if (f.url) {
534
534
  extractedFiles.push({url: f.url});
535
535
  }
536
536
  });
537
537
  }
538
-
538
+
539
539
  // Используем только extractedFiles (уже содержит все файлы)
540
540
  const rawFiles = extractedFiles;
541
-
541
+
542
542
  // Обрабатываем все файлы (base64 -> OSS, локальные -> OSS, URLs -> как есть)
543
543
  const files = rawFiles.length > 0 ? await processFilesForQwen(rawFiles) : [];
544
-
544
+
545
545
  // Добавляем файлы в messageContent
546
546
  if (Array.isArray(messageContent) && files.length > 0) {
547
547
  messageContent = [...messageContent, ...files];
@@ -552,12 +552,12 @@ async function parseOpenAIMessages(messages) {
552
552
  ...files
553
553
  ];
554
554
  }
555
-
555
+
556
556
  return { messageContent, systemMessage };
557
557
  }
558
558
 
559
559
  function buildCombinedTools(tools, functions, toolChoice) {
560
- const combinedTools = tools || (functions ? functions.map(fn => ({ type: 'function', function: fn })) : null);
560
+ const combinedTools = tools || (functions ? functions.map((fn) => ({ type: 'function', function: fn })) : null);
561
561
  return { combinedTools, toolChoice };
562
562
  }
563
563
 
@@ -595,7 +595,7 @@ async function handleStreamingResponse(res, mappedModel, messageContent, chatId,
595
595
  created: Math.floor(Date.now() / 1000), model: mappedModel,
596
596
  choices: [{ index: 0, delta: { content: codePoints.slice(i, i + chunkSize).join('') }, finish_reason: null }]
597
597
  });
598
- await new Promise(r => setTimeout(r, STREAMING_CHUNK_DELAY));
598
+ await new Promise((r) => setTimeout(r, STREAMING_CHUNK_DELAY));
599
599
  }
600
600
  }
601
601
 
@@ -650,7 +650,7 @@ router.post('/chat', async (req, res) => {
650
650
  if (messages && Array.isArray(messages)) {
651
651
  const parsed = await parseOpenAIMessages(messages);
652
652
  systemMessage = parsed.systemMessage;
653
- if (parsed.messageContent) messageContent = parsed.messageContent;
653
+ if (parsed.messageContent) {messageContent = parsed.messageContent;}
654
654
  }
655
655
 
656
656
  if (!messageContent) {
@@ -779,12 +779,12 @@ router.post('/chat', async (req, res) => {
779
779
  }
780
780
  }
781
781
 
782
- const result = await sendMessage(messageContent, mappedModel, isMeta ? null : chatId, isMeta ? null : parentId, null, null, null, systemMessage);
782
+ const result = await sendMessage(messageContent, mappedModel, isMeta ? null : chatId, isMeta ? null : parentId, null, null, null, systemMessage);
783
783
 
784
784
  if (result.choices && result.choices[0] && result.choices[0].message) {
785
785
  const responseLength = result.choices[0].message.content ? result.choices[0].message.content.length : 0;
786
786
  logInfo(`Ответ успешно сформирован для запроса, длина ответа: ${responseLength}`);
787
-
787
+
788
788
  // Сохраняем историю чата
789
789
  if (result.chatId) {
790
790
  try {
@@ -815,7 +815,7 @@ router.get('/models', async (req, res) => {
815
815
  const modelsRaw = getAllModels();
816
816
  const openAiModels = {
817
817
  object: 'list',
818
- data: modelsRaw.models.map(m => ({
818
+ data: modelsRaw.models.map((m) => ({
819
819
  id: m.id || m.name || m,
820
820
  object: 'model',
821
821
  created: 0,
@@ -836,19 +836,19 @@ router.get('/status', async (req, res) => {
836
836
  logInfo('Запрос статуса авторизации');
837
837
  const tokens = listTokens();
838
838
  const now = Date.now();
839
-
839
+
840
840
  // Фильтруем только действительные токены с cookies
841
- const validTokens = tokens.filter(t => {
842
- if (t.invalid) return false;
843
- if (t.resetAt && new Date(t.resetAt).getTime() > now) return false;
844
- if (t.expiryTime && t.expiryTime <= now) return false;
841
+ const validTokens = tokens.filter((t) => {
842
+ if (t.invalid) {return false;}
843
+ if (t.resetAt && new Date(t.resetAt).getTime() > now) {return false;}
844
+ if (t.expiryTime && t.expiryTime <= now) {return false;}
845
845
  // Проверяем наличие cookies.json
846
846
  const cookiesPath = path.join(process.cwd(), SESSION_DIR, 'accounts', t.id, 'cookies.json');
847
- if (!fs.existsSync(cookiesPath)) return false;
847
+ if (!fs.existsSync(cookiesPath)) {return false;}
848
848
  return true;
849
849
  });
850
-
851
- const accounts = await Promise.all(validTokens.map(async t => {
850
+
851
+ const accounts = await Promise.all(validTokens.map(async (t) => {
852
852
  const accInfo = { id: t.id, status: 'UNKNOWN', resetAt: t.resetAt || null };
853
853
 
854
854
  if (t.resetAt) {
@@ -857,9 +857,9 @@ router.get('/status', async (req, res) => {
857
857
  }
858
858
 
859
859
  const testResult = await testToken(t.token);
860
- if (testResult === 'OK') { accInfo.status = 'OK'; if (t.invalid || t.resetAt) markValid(t.id); }
860
+ if (testResult === 'OK') { accInfo.status = 'OK'; if (t.invalid || t.resetAt) {markValid(t.id);} }
861
861
  else if (testResult === 'RATELIMIT') { accInfo.status = 'WAIT'; markRateLimited(t.id, 24); }
862
- else if (testResult === 'UNAUTHORIZED') { accInfo.status = 'INVALID'; if (!t.invalid) markInvalid(t.id); }
862
+ else if (testResult === 'UNAUTHORIZED') { accInfo.status = 'INVALID'; if (!t.invalid) {markInvalid(t.id);} }
863
863
  else { accInfo.status = 'ERROR'; }
864
864
  return accInfo;
865
865
  }));
@@ -870,7 +870,7 @@ router.get('/status', async (req, res) => {
870
870
  return res.json({ authenticated: false, message: 'Браузер не инициализирован', accounts });
871
871
  }
872
872
 
873
- if (getAuthenticationStatus()) return res.json({ accounts });
873
+ if (getAuthenticationStatus()) {return res.json({ accounts });}
874
874
 
875
875
  await checkAuthentication(browserContext);
876
876
  const isAuthenticated = getAuthenticationStatus();
@@ -969,22 +969,22 @@ router.post('/chat/completions', async (req, res) => {
969
969
  }
970
970
 
971
971
  // Извлекаем system message если есть
972
- const systemMsg = messages.find(msg => msg.role === 'system');
972
+ const systemMsg = messages.find((msg) => msg.role === 'system');
973
973
  const systemMessage = systemMsg ? systemMsg.content : null;
974
974
 
975
- const lastUserMessage = messages.filter(msg => msg.role === 'user').pop();
975
+ const lastUserMessage = messages.filter((msg) => msg.role === 'user').pop();
976
976
  if (!lastUserMessage) {
977
977
  logError('В запросе нет сообщений от пользователя');
978
978
  return res.status(400).json({ error: 'В запросе нет сообщений от пользователя' });
979
979
  }
980
980
 
981
981
  let messageContent = lastUserMessage.content;
982
-
982
+
983
983
  // Преобразуем OpenAI format content array во внутренний формат
984
984
  const extractedFiles = []; // Files из content array
985
-
985
+
986
986
  if (Array.isArray(messageContent)) {
987
- messageContent = messageContent.map(item => {
987
+ messageContent = messageContent.map((item) => {
988
988
  if (item.type === 'text') {
989
989
  return { type: 'text', text: item.text };
990
990
  } else if (item.type === 'image_url' && item.image_url) {
@@ -999,27 +999,27 @@ router.post('/chat/completions', async (req, res) => {
999
999
  }
1000
1000
  return item;
1001
1001
  });
1002
-
1002
+
1003
1003
  // Убираем пустые text элементы
1004
- messageContent = messageContent.filter(item => item.type !== 'text' || item.text.trim());
1004
+ messageContent = messageContent.filter((item) => item.type !== 'text' || item.text.trim());
1005
1005
  }
1006
-
1006
+
1007
1007
  // Поддерживаем также формат: content: 'text', files: [{url: '...'}]
1008
1008
  // Добавляем файлы из lastUserMessage.files в extractedFiles
1009
1009
  if (lastUserMessage.files && Array.isArray(lastUserMessage.files)) {
1010
- lastUserMessage.files.forEach(f => {
1010
+ lastUserMessage.files.forEach((f) => {
1011
1011
  if (f.url) {
1012
1012
  extractedFiles.push({url: f.url});
1013
1013
  }
1014
1014
  });
1015
1015
  }
1016
-
1016
+
1017
1017
  // Используем только extractedFiles (уже содержит все файлы)
1018
1018
  const rawFiles = extractedFiles;
1019
-
1019
+
1020
1020
  // Обрабатываем все файлы (base64 -> OSS, локальные -> OSS, URLs -> как есть)
1021
1021
  const files = rawFiles.length > 0 ? await processFilesForQwen(rawFiles) : [];
1022
-
1022
+
1023
1023
  // Добавляем файлы в messageContent
1024
1024
  if (Array.isArray(messageContent) && files.length > 0) {
1025
1025
  messageContent = [...messageContent, ...files];
@@ -1045,7 +1045,7 @@ router.post('/chat/completions', async (req, res) => {
1045
1045
  logInfo(`Модель "${model}" заменена на "${mappedModel}"`);
1046
1046
  }
1047
1047
  logInfo(`Используется модель: ${mappedModel}`);
1048
- if (systemMessage) logInfo(`System message: ${systemMessage.substring(0, 50)}${systemMessage.length > 50 ? '...' : ''}`);
1048
+ if (systemMessage) {logInfo(`System message: ${systemMessage.substring(0, 50)}${systemMessage.length > 50 ? '...' : ''}`);}
1049
1049
 
1050
1050
  const { combinedTools } = buildCombinedTools(tools, functions, tool_choice);
1051
1051
 
@@ -1054,7 +1054,7 @@ router.post('/chat/completions', async (req, res) => {
1054
1054
  }
1055
1055
 
1056
1056
  // Логируем полную историю сообщений
1057
- logInfo(`История содержит ${messages.length} сообщений: ${messages.map(m => m.role).join(', ')}`);
1057
+ logInfo(`История содержит ${messages.length} сообщений: ${messages.map((m) => m.role).join(', ')}`);
1058
1058
  if (effectiveChatId) {
1059
1059
  logInfo(`Используется chatId: ${effectiveChatId}, parentId: ${effectiveParentId || 'null'}`);
1060
1060
  }
@@ -1073,7 +1073,7 @@ router.post('/chat/completions', async (req, res) => {
1073
1073
  };
1074
1074
 
1075
1075
  try {
1076
- const combinedTools = tools || (functions ? functions.map(fn => ({ type: 'function', function: fn })) : null);
1076
+ const combinedTools = tools || (functions ? functions.map((fn) => ({ type: 'function', function: fn })) : null);
1077
1077
  const qwenChatId = await resolveQwenChatId(effectiveChatId, mappedModel);
1078
1078
 
1079
1079
  // Setup streaming callback if stream=true
@@ -1171,7 +1171,7 @@ router.post('/chat/completions', async (req, res) => {
1171
1171
  res.end();
1172
1172
  }
1173
1173
  } else {
1174
- const combinedTools = tools || (functions ? functions.map(fn => ({ type: 'function', function: fn })) : null);
1174
+ const combinedTools = tools || (functions ? functions.map((fn) => ({ type: 'function', function: fn })) : null);
1175
1175
  const qwenChatId = await resolveQwenChatId(effectiveChatId, mappedModel);
1176
1176
  const result = await sendMessage(messageContent, mappedModel, qwenChatId, effectiveParentId, null, combinedTools, tool_choice, systemMessage);
1177
1177
 
@@ -1188,22 +1188,22 @@ router.post('/chat/completions', async (req, res) => {
1188
1188
 
1189
1189
  if (result.error) {
1190
1190
  return res.status(500).json({
1191
- error: { message: result.error, type: "server_error" }
1191
+ error: { message: result.error, type: 'server_error' }
1192
1192
  });
1193
1193
  }
1194
1194
 
1195
1195
  const openaiResponse = {
1196
- id: result.id || "chatcmpl-" + Date.now(),
1197
- object: "chat.completion",
1196
+ id: result.id || 'chatcmpl-' + Date.now(),
1197
+ object: 'chat.completion',
1198
1198
  created: Math.floor(Date.now() / 1000),
1199
1199
  model: result.model || mappedModel,
1200
1200
  choices: result.choices || [{
1201
1201
  index: 0,
1202
1202
  message: {
1203
- role: "assistant",
1204
- content: result.choices?.[0]?.message?.content || ""
1203
+ role: 'assistant',
1204
+ content: result.choices?.[0]?.message?.content || ''
1205
1205
  },
1206
- finish_reason: "stop"
1206
+ finish_reason: 'stop'
1207
1207
  }],
1208
1208
  usage: result.usage || {
1209
1209
  prompt_tokens: 0,
@@ -1233,7 +1233,7 @@ router.post('/chat/completions', async (req, res) => {
1233
1233
  }
1234
1234
  } catch (error) {
1235
1235
  logError('Ошибка при обработке запроса', error);
1236
- res.status(500).json({ error: { message: 'Внутренняя ошибка сервера', type: "server_error" } });
1236
+ res.status(500).json({ error: { message: 'Внутренняя ошибка сервера', type: 'server_error' } });
1237
1237
  }
1238
1238
  });
1239
1239
 
@@ -1303,22 +1303,22 @@ router.post('/v1/chat/completions', async (req, res) => {
1303
1303
  }
1304
1304
 
1305
1305
  // Извлекаем system message если есть
1306
- const systemMsg = messages.find(msg => msg.role === 'system');
1306
+ const systemMsg = messages.find((msg) => msg.role === 'system');
1307
1307
  const systemMessage = systemMsg ? systemMsg.content : null;
1308
1308
 
1309
- const lastUserMessage = messages.filter(msg => msg.role === 'user').pop();
1309
+ const lastUserMessage = messages.filter((msg) => msg.role === 'user').pop();
1310
1310
  if (!lastUserMessage) {
1311
1311
  logError('В запросе нет сообщений от пользователя');
1312
1312
  return res.status(400).json({ error: 'В запросе нет сообщений от пользователя' });
1313
1313
  }
1314
1314
 
1315
1315
  let messageContent = lastUserMessage.content;
1316
-
1316
+
1317
1317
  // Преобразуем OpenAI format content array во внутренний формат
1318
1318
  const extractedFiles = []; // Files из content array
1319
-
1319
+
1320
1320
  if (Array.isArray(messageContent)) {
1321
- messageContent = messageContent.map(item => {
1321
+ messageContent = messageContent.map((item) => {
1322
1322
  if (item.type === 'text') {
1323
1323
  return { type: 'text', text: item.text };
1324
1324
  } else if (item.type === 'image_url' && item.image_url) {
@@ -1333,27 +1333,27 @@ router.post('/v1/chat/completions', async (req, res) => {
1333
1333
  }
1334
1334
  return item;
1335
1335
  });
1336
-
1336
+
1337
1337
  // Убираем пустые text элементы
1338
- messageContent = messageContent.filter(item => item.type !== 'text' || item.text.trim());
1338
+ messageContent = messageContent.filter((item) => item.type !== 'text' || item.text.trim());
1339
1339
  }
1340
-
1340
+
1341
1341
  // Поддерживаем также формат: content: 'text', files: [{url: '...'}]
1342
1342
  // Добавляем файлы из lastUserMessage.files в extractedFiles
1343
1343
  if (lastUserMessage.files && Array.isArray(lastUserMessage.files)) {
1344
- lastUserMessage.files.forEach(f => {
1344
+ lastUserMessage.files.forEach((f) => {
1345
1345
  if (f.url) {
1346
1346
  extractedFiles.push({url: f.url});
1347
1347
  }
1348
1348
  });
1349
1349
  }
1350
-
1350
+
1351
1351
  // Используем только extractedFiles (уже содержит все файлы)
1352
1352
  const rawFiles = extractedFiles;
1353
-
1353
+
1354
1354
  // Обрабатываем все файлы (base64 -> OSS, локальные -> OSS, URLs -> как есть)
1355
1355
  const files = rawFiles.length > 0 ? await processFilesForQwen(rawFiles) : [];
1356
-
1356
+
1357
1357
  // Добавляем файлы в messageContent
1358
1358
  if (Array.isArray(messageContent) && files.length > 0) {
1359
1359
  messageContent = [...messageContent, ...files];
@@ -1385,7 +1385,7 @@ router.post('/v1/chat/completions', async (req, res) => {
1385
1385
  }
1386
1386
 
1387
1387
  // Логируем полную историю сообщений
1388
- logInfo(`История содержит ${messages.length} сообщений: ${messages.map(m => m.role).join(', ')}`);
1388
+ logInfo(`История содержит ${messages.length} сообщений: ${messages.map((m) => m.role).join(', ')}`);
1389
1389
  if (effectiveChatId) {
1390
1390
  logInfo(`Используется chatId: ${effectiveChatId}, parentId: ${effectiveParentId || 'null'}`);
1391
1391
  }
@@ -1404,7 +1404,7 @@ router.post('/v1/chat/completions', async (req, res) => {
1404
1404
  };
1405
1405
 
1406
1406
  try {
1407
- const combinedTools = tools || (functions ? functions.map(fn => ({ type: 'function', function: fn })) : null);
1407
+ const combinedTools = tools || (functions ? functions.map((fn) => ({ type: 'function', function: fn })) : null);
1408
1408
  const qwenChatId = await resolveQwenChatId(effectiveChatId, mappedModel);
1409
1409
 
1410
1410
  // Setup streaming callback if stream=true
@@ -1425,7 +1425,7 @@ router.post('/v1/chat/completions', async (req, res) => {
1425
1425
  });
1426
1426
  };
1427
1427
  }
1428
-
1428
+
1429
1429
  const result = await sendMessage(
1430
1430
  messageContent,
1431
1431
  mappedModel,
@@ -1498,7 +1498,7 @@ router.post('/v1/chat/completions', async (req, res) => {
1498
1498
  res.end();
1499
1499
  }
1500
1500
  } else {
1501
- const combinedTools = tools || (functions ? functions.map(fn => ({ type: 'function', function: fn })) : null);
1501
+ const combinedTools = tools || (functions ? functions.map((fn) => ({ type: 'function', function: fn })) : null);
1502
1502
  const qwenChatId = await resolveQwenChatId(effectiveChatId, mappedModel);
1503
1503
 
1504
1504
  const result = await sendMessage(messageContent, mappedModel, qwenChatId, effectiveParentId, filesForSend, combinedTools, tool_choice, systemMessage);
@@ -1517,7 +1517,7 @@ router.post('/v1/chat/completions', async (req, res) => {
1517
1517
 
1518
1518
  if (result.error) {
1519
1519
  return res.status(500).json({
1520
- error: { message: result.error, type: "server_error" }
1520
+ error: { message: result.error, type: 'server_error' }
1521
1521
  });
1522
1522
  }
1523
1523
 
@@ -1530,17 +1530,17 @@ router.post('/v1/chat/completions', async (req, res) => {
1530
1530
  }
1531
1531
 
1532
1532
  const openaiResponse = {
1533
- id: result.id || "chatcmpl-" + Date.now(),
1534
- object: "chat.completion",
1533
+ id: result.id || 'chatcmpl-' + Date.now(),
1534
+ object: 'chat.completion',
1535
1535
  created: Math.floor(Date.now() / 1000),
1536
1536
  model: result.model || mappedModel,
1537
1537
  choices: [{
1538
1538
  index: 0,
1539
1539
  message: {
1540
- role: "assistant",
1540
+ role: 'assistant',
1541
1541
  content: messageText
1542
1542
  },
1543
- finish_reason: "stop"
1543
+ finish_reason: 'stop'
1544
1544
  }],
1545
1545
  usage: result.usage || {
1546
1546
  prompt_tokens: 0,
@@ -1582,7 +1582,7 @@ router.post('/v1/chat/completions', async (req, res) => {
1582
1582
  }
1583
1583
  } catch (error) {
1584
1584
  logError('Ошибка при обработке v1 запроса', error);
1585
- res.status(500).json({ error: { message: 'Внутренняя ошибка сервера', type: "server_error" } });
1585
+ res.status(500).json({ error: { message: 'Внутренняя ошибка сервера', type: 'server_error' } });
1586
1586
  }
1587
1587
  });
1588
1588
 
@@ -1629,7 +1629,7 @@ router.post('/files/upload', upload.single('file'), async (req, res) => {
1629
1629
  /**
1630
1630
  * POST /api/chat/multipart - Chat with file uploads via multipart/form-data
1631
1631
  * OpenAI-compatible endpoint that supports both JSON and file uploads
1632
- *
1632
+ *
1633
1633
  * Fields:
1634
1634
  * - message: Text message (required)
1635
1635
  * - model: Model name (optional)
@@ -1662,7 +1662,7 @@ router.post('/chat/multipart', upload.array('files', 5), async (req, res) => {
1662
1662
 
1663
1663
  // Обрабатываем загруженные файлы
1664
1664
  const files = uploadedFiles.length > 0 ? await processFilesForQwen(uploadedFiles) : [];
1665
-
1665
+
1666
1666
  // Встраиваем файлы в messageContent как в test 2
1667
1667
  let messageContent = message;
1668
1668
  if (files.length > 0) {
@@ -1748,7 +1748,7 @@ router.post('/chat/multipart', upload.array('files', 5), async (req, res) => {
1748
1748
  });
1749
1749
  res.write('data: [DONE]\n\n');
1750
1750
  res.end();
1751
- return;
1751
+
1752
1752
  } catch (error) {
1753
1753
  logError('Ошибка при обработке потокового multipart запроса', error);
1754
1754
  writeSse({
@@ -1760,7 +1760,7 @@ router.post('/chat/multipart', upload.array('files', 5), async (req, res) => {
1760
1760
  });
1761
1761
  res.write('data: [DONE]\n\n');
1762
1762
  res.end();
1763
- return;
1763
+
1764
1764
  }
1765
1765
  } else {
1766
1766
  // Non-streaming response
@@ -1872,7 +1872,7 @@ router.post('/images/generations', async (req, res) => {
1872
1872
  try {
1873
1873
  const { prompt, model, n, size, response_format, user } = req.body;
1874
1874
 
1875
- logInfo(`Получен запрос на генерацию изображения`);
1875
+ logInfo('Получен запрос на генерацию изображения');
1876
1876
  logDebug(`Prompt: ${prompt?.substring(0, 100)}${prompt?.length > 100 ? '...' : ''}`);
1877
1877
 
1878
1878
  if (!prompt) {
@@ -1949,10 +1949,10 @@ router.post('/images/generations', async (req, res) => {
1949
1949
  router.get('/images/models', async (req, res) => {
1950
1950
  try {
1951
1951
  const models = getAvailableImageModels();
1952
-
1952
+
1953
1953
  res.json({
1954
1954
  object: 'list',
1955
- data: models.map(model => ({
1955
+ data: models.map((model) => ({
1956
1956
  id: model,
1957
1957
  object: 'model',
1958
1958
  created: Date.now(),
@@ -1980,9 +1980,9 @@ router.get('/images/status', async (req, res) => {
1980
1980
  apiKeyConfigured: !!DASHSCOPE_API_KEY,
1981
1981
  message: IMAGE_GENERATION_MODE === 'browser'
1982
1982
  ? (isAvailable ? 'Browser mode активен' : 'Браузер не инициализирован')
1983
- : (isAvailable
1984
- ? 'DashScope API доступен'
1985
- : DASHSCOPE_API_KEY
1983
+ : (isAvailable
1984
+ ? 'DashScope API доступен'
1985
+ : DASHSCOPE_API_KEY
1986
1986
  ? 'DashScope API недоступен или неверные учётные данные'
1987
1987
  : 'API ключ DASHSCOPE_API_KEY не настроен')
1988
1988
  });
@@ -2004,7 +2004,7 @@ router.post('/v1/images/generations', async (req, res) => {
2004
2004
  try {
2005
2005
  const { prompt, model, n, size, response_format, quality, style, user } = req.body;
2006
2006
 
2007
- logInfo(`[OpenAI v1] Получен запрос на генерацию изображения`);
2007
+ logInfo('[OpenAI v1] Получен запрос на генерацию изображения');
2008
2008
  logDebug(`Prompt: ${prompt?.substring(0, 100)}${prompt?.length > 100 ? '...' : ''}`);
2009
2009
 
2010
2010
  if (!prompt) {
@@ -2088,7 +2088,7 @@ router.post('/v1/images/generations', async (req, res) => {
2088
2088
 
2089
2089
  // Если запрошено несколько изображений (когда API поддерживает)
2090
2090
  if (n > 1 && result.imageUrls) {
2091
- responseData.data = result.imageUrls.map(url => ({
2091
+ responseData.data = result.imageUrls.map((url) => ({
2092
2092
  url,
2093
2093
  revised_prompt: prompt
2094
2094
  }));
@@ -2117,12 +2117,12 @@ router.get('/v1/models', async (req, res) => {
2117
2117
  try {
2118
2118
  const chatModels = getAllModels();
2119
2119
  const imageModels = getAvailableImageModels();
2120
-
2120
+
2121
2121
  const allModels = {
2122
2122
  object: 'list',
2123
2123
  data: [
2124
2124
  // Chat models
2125
- ...chatModels.models.map(m => ({
2125
+ ...chatModels.models.map((m) => ({
2126
2126
  id: m.id || m.name || m,
2127
2127
  object: 'model',
2128
2128
  created: 0,
@@ -2131,7 +2131,7 @@ router.get('/v1/models', async (req, res) => {
2131
2131
  capabilities: ['chat', 'completion']
2132
2132
  })),
2133
2133
  // Image generation models
2134
- ...imageModels.map(model => ({
2134
+ ...imageModels.map((model) => ({
2135
2135
  id: model,
2136
2136
  object: 'model',
2137
2137
  created: Date.now(),