qwen-api-proxy 1.0.12 → 1.0.14

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.
@@ -40,7 +40,7 @@ function loadPersistedSettings() {
40
40
  activeModel = settings.activeModel;
41
41
  logInfo(`📝 Загружена активная модель: ${activeModel}`);
42
42
  } else {
43
- logInfo(`📝 Активная модель не установлена, используется модель по умолчанию`);
43
+ logInfo('📝 Активная модель не установлена, используется модель по умолчанию');
44
44
  }
45
45
  llmChatEnabled = settings.llmChatEnabled || false;
46
46
  logInfo(`📝 LLM чат: ${llmChatEnabled ? 'включен' : 'выключен'}`);
@@ -74,27 +74,27 @@ async function checkAIHealth(tokens) {
74
74
 
75
75
  try {
76
76
  logInfo('🧪 Тестирование AI нейросети (ping pong)...');
77
-
77
+
78
78
  // Импортируем функцию sendMessage для прямого запроса к Qwen
79
79
  const { sendMessage } = await import('../api/chat.js');
80
80
  const testModel = getBotSettingsModel();
81
-
81
+
82
82
  // Делаем запрос напрямую к Qwen API через наш модуль
83
83
  const startTime = Date.now();
84
84
  const result = await sendMessage('ping', testModel, null, null, null, null, null, null, 't2t', null, true, 0);
85
85
  const responseTime = ((Date.now() - startTime) / 1000).toFixed(2);
86
-
86
+
87
87
  if (result && !result.error) {
88
88
  const responseContent = result.choices?.[0]?.message?.content || '';
89
89
  const usedModel = result.model || testModel;
90
-
90
+
91
91
  // Проверяем что ответ содержит "pong" (в любом регистре)
92
92
  const hasPong = responseContent.toLowerCase().includes('pong');
93
-
93
+
94
94
  if (hasPong) {
95
95
  // Тест пройден - ответ содержит pong
96
96
  logInfo(`✅ AI тест прошел успешно: модель=${usedModel}, время=${responseTime}с, ответ="${responseContent.substring(0, 50)}"`);
97
-
97
+
98
98
  return [
99
99
  {
100
100
  name: '🧠 AI Нейросеть',
@@ -110,16 +110,16 @@ async function checkAIHealth(tokens) {
110
110
  } else {
111
111
  // Тест не пройден - ответ не содержит pong
112
112
  const fullResponse = JSON.stringify(result, null, 2);
113
-
114
- logError(`❌ AI тест не пройден: ответ не содержит "pong"`);
113
+
114
+ logError('❌ AI тест не пройден: ответ не содержит "pong"');
115
115
  logError(`Получен ответ: ${responseContent.substring(0, 200)}`);
116
116
  logDebug(`Полный JSON ответа: ${fullResponse.substring(0, 1000)}`);
117
-
117
+
118
118
  return [
119
119
  {
120
120
  name: '🧠 AI Нейросеть',
121
121
  status: false,
122
- details: `❌ Тест не пройден: ответ не содержит "pong"`
122
+ details: '❌ Тест не пройден: ответ не содержит "pong"'
123
123
  },
124
124
  {
125
125
  name: ' 📝 Ответ',
@@ -132,10 +132,10 @@ async function checkAIHealth(tokens) {
132
132
  // Ошибка от API
133
133
  const errorMsg = result.error || 'Unknown error';
134
134
  const fullResponse = JSON.stringify(result, null, 2);
135
-
135
+
136
136
  logError(`❌ AI тест не пройден: ${errorMsg}`);
137
137
  logDebug(`Полный JSON ошибки: ${fullResponse.substring(0, 1000)}`);
138
-
138
+
139
139
  return [{
140
140
  name: '🧠 AI Нейросеть',
141
141
  status: false,
@@ -145,7 +145,7 @@ async function checkAIHealth(tokens) {
145
145
  } catch (error) {
146
146
  // Ошибка подключения
147
147
  logError('❌ AI тест не пройден (ошибка подключения)', error);
148
-
148
+
149
149
  return [{
150
150
  name: '🧠 AI Нейросеть',
151
151
  status: false,
@@ -301,33 +301,33 @@ export async function checkAllSubsystems(botStarted, autoSend = true) {
301
301
  // 2. Проверяем токены
302
302
  const tokens = loadTokens();
303
303
  const now = Date.now();
304
-
304
+
305
305
  // Фильтруем только действительные токены (не invalid, не rate-limited, не истекшие, с cookies)
306
- const validTokens = tokens.filter(t => {
307
- if (t.invalid) return false;
308
- if (t.resetAt && new Date(t.resetAt).getTime() > now) return false;
309
- if (t.expiryTime && t.expiryTime <= now) return false;
306
+ const validTokens = tokens.filter((t) => {
307
+ if (t.invalid) { return false; }
308
+ if (t.resetAt && new Date(t.resetAt).getTime() > now) { return false; }
309
+ if (t.expiryTime && t.expiryTime <= now) { return false; }
310
310
  // Проверяем наличие cookies.json
311
311
  const cookiesPath = path.join(process.cwd(), SESSION_DIR, 'accounts', t.id, 'cookies.json');
312
- if (!fs.existsSync(cookiesPath)) return false;
312
+ if (!fs.existsSync(cookiesPath)) { return false; }
313
313
  return true;
314
314
  });
315
315
 
316
316
  // Проверка оставшегося времени для токенов
317
317
  if (tokens.length > 0) {
318
318
  const filteredCount = tokens.length - validTokens.length;
319
-
319
+
320
320
  const expirySummary = validTokens.reduce((acc, token) => {
321
321
  const now = Date.now();
322
-
322
+
323
323
  // Если expiryTime не установлен
324
324
  if (!token.expiryTime) {
325
325
  acc.tokens.push({ timeStr: 'Неизвестно', id: token.id, hasExpiry: false });
326
326
  return acc;
327
327
  }
328
-
328
+
329
329
  const timeLeft = token.expiryTime - now;
330
-
330
+
331
331
  // Форматируем время в удобочитаемый вид
332
332
  let timeStr;
333
333
  if (timeLeft <= 0) {
@@ -338,31 +338,31 @@ export async function checkAllSubsystems(botStarted, autoSend = true) {
338
338
  const hours = Math.floor((timeLeft % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60));
339
339
  const minutes = Math.floor((timeLeft % (1000 * 60 * 60)) / (1000 * 60));
340
340
  const seconds = Math.floor((timeLeft % (1000 * 60)) / 1000);
341
-
341
+
342
342
  const parts = [];
343
- if (days > 0) parts.push(`${days}д`);
344
- if (hours > 0) parts.push(`${hours}ч`);
345
- if (minutes > 0) parts.push(`${minutes}м`);
343
+ if (days > 0) { parts.push(`${days}д`); }
344
+ if (hours > 0) { parts.push(`${hours}ч`); }
345
+ if (minutes > 0) { parts.push(`${minutes}м`); }
346
346
  parts.push(`${seconds}с`);
347
-
347
+
348
348
  timeStr = parts.join(' ');
349
349
  }
350
-
350
+
351
351
  acc.tokens.push({ timeStr, id: token.id, hasExpiry: true });
352
352
  return acc;
353
353
  }, { expired: 0, tokens: [] });
354
-
354
+
355
355
  let tokenDetails = `✅ Доступно: ${validTokens.length}`;
356
356
  if (filteredCount > 0) {
357
357
  tokenDetails += ` (пропущено ${filteredCount} истекших)`;
358
358
  }
359
-
359
+
360
360
  checks.push({
361
361
  name: '🎫 Токены',
362
362
  status: validTokens.length > 0,
363
363
  details: tokenDetails
364
364
  });
365
-
365
+
366
366
  // Показываем только действительные токены
367
367
  if (expirySummary.tokens.length > 0) {
368
368
  expirySummary.tokens.forEach((token, index) => {
@@ -370,17 +370,17 @@ export async function checkAllSubsystems(botStarted, autoSend = true) {
370
370
  const cookiesPath = path.join(process.cwd(), SESSION_DIR, 'accounts', token.id, 'cookies.json');
371
371
  const hasCookies = fs.existsSync(cookiesPath);
372
372
  const cookieStatus = hasCookies ? '✅' : '❌';
373
-
373
+
374
374
  checks.push({
375
375
  name: ` Токен ${index + 1}`,
376
376
  status: token.hasExpiry && hasCookies,
377
- details: token.hasExpiry
377
+ details: token.hasExpiry
378
378
  ? `${cookieStatus} ${token.id}\n ⏱️ Осталось: ${token.timeStr}`
379
379
  : `${cookieStatus} ${token.id}\n ⚠️ Время истечения: ${token.timeStr}`
380
380
  });
381
381
  });
382
382
  }
383
-
383
+
384
384
  // Если все токены истекли, показываем предупреждение
385
385
  if (validTokens.length === 0) {
386
386
  checks.push({
@@ -403,7 +403,7 @@ export async function checkAllSubsystems(botStarted, autoSend = true) {
403
403
  status: botStarted,
404
404
  details: botStarted ? '✅ Работает' : '❌ Не запущен'
405
405
  });
406
-
406
+
407
407
  // 3.1. Показываем настройки бота (если загружены)
408
408
  if (botStarted) {
409
409
  const llmStatus = llmChatEnabled ? '✅ Включен' : '❌ Выключен';
@@ -468,7 +468,7 @@ export async function checkAllSubsystems(botStarted, autoSend = true) {
468
468
  logInfo('🔍 ПРОВЕРКА ПОД СИСТЕМ ПРИ ЗАПУСКЕ');
469
469
  logInfo('='.repeat(60));
470
470
 
471
- checks.forEach(check => {
471
+ checks.forEach((check) => {
472
472
  const status = check.status ? '✅' : '❌';
473
473
  logInfo(`${status} ${check.name}: ${check.details}`);
474
474
  });
@@ -489,36 +489,36 @@ export async function checkAllSubsystems(botStarted, autoSend = true) {
489
489
  const reportLines = [];
490
490
 
491
491
  // Заголовок
492
- reportLines.push(`🚀 <b>Сервис запущен!</b>\n`);
492
+ reportLines.push('🚀 <b>Сервис запущен!</b>\n');
493
493
 
494
494
  // Группа 1: Основные компоненты
495
- reportLines.push(`<b>🔑 Основные компоненты:</b>`);
496
- const mainComponents = checks.filter(c =>
497
- c.name.includes('Session') || c.name.includes('Токены') || c.name.includes('Telegram') ||
495
+ reportLines.push('<b>🔑 Основные компоненты:</b>');
496
+ const mainComponents = checks.filter((c) =>
497
+ c.name.includes('Session') || c.name.includes('Токены') || c.name.includes('Telegram') ||
498
498
  c.name.includes('Токен ') || c.name.includes('AI') || c.name.includes('Ответ')
499
499
  );
500
- mainComponents.forEach(check => {
500
+ mainComponents.forEach((check) => {
501
501
  reportLines.push(`${check.name}: ${check.details}`);
502
502
  });
503
503
 
504
504
  reportLines.push('');
505
505
 
506
506
  // Группа 2: Инфраструктура
507
- reportLines.push(`<b>🏗️ Инфраструктура:</b>`);
508
- const infrastructure = checks.filter(c =>
507
+ reportLines.push('<b>🏗️ Инфраструктура:</b>');
508
+ const infrastructure = checks.filter((c) =>
509
509
  c.name.includes('Прокси') || c.name.includes('Uploads') || c.name.includes('Логирование')
510
510
  );
511
- infrastructure.forEach(check => {
511
+ infrastructure.forEach((check) => {
512
512
  reportLines.push(`${check.name}: ${check.details}`);
513
513
  });
514
514
 
515
515
  reportLines.push('');
516
516
 
517
517
  // Группа 3: Инструменты
518
- const tools = checks.filter(c => c.name.includes('p7zip'));
518
+ const tools = checks.filter((c) => c.name.includes('p7zip'));
519
519
  if (tools.length > 0) {
520
- reportLines.push(`<b>🔧 Инструменты:</b>`);
521
- tools.forEach(check => {
520
+ reportLines.push('<b>🔧 Инструменты:</b>');
521
+ tools.forEach((check) => {
522
522
  reportLines.push(`${check.name}: ${check.details}`);
523
523
  });
524
524
  reportLines.push('');
@@ -529,29 +529,29 @@ export async function checkAllSubsystems(botStarted, autoSend = true) {
529
529
 
530
530
  // Итоговый статус
531
531
  if (hasTokens && allOk) {
532
- reportLines.push(`✅ <b>Все системы работают</b>`);
532
+ reportLines.push('✅ <b>Все системы работают</b>');
533
533
  } else if (!hasTokens) {
534
- reportLines.push(`⚠️ <b>Режим ожидания архива</b>`);
535
- reportLines.push(`📦 Отправьте архив с сессиями`);
534
+ reportLines.push('⚠️ <b>Режим ожидания архива</b>');
535
+ reportLines.push('📦 Отправьте архив с сессиями');
536
536
  } else {
537
- reportLines.push(`⚠️ <b>Есть проблемы</b>`);
537
+ reportLines.push('⚠️ <b>Есть проблемы</b>');
538
538
  }
539
539
 
540
540
  // Ссылки
541
541
  reportLines.push(`\n🌐 API: http://localhost:${process.env.PORT || 3264}`);
542
542
  reportLines.push(`📖 Docs: http://localhost:${process.env.PORT || 3264}/api`);
543
-
543
+
544
544
  // Репозиторий
545
- reportLines.push(`\n📚 <b>Репозиторий:</b>`);
546
- reportLines.push(`🔗 GitHub: https://github.com/EndyKaufman/FreeQwenApi`);
547
- reportLines.push(`⭐ Оригинал: https://github.com/y1n7sint/FreeQwenApi`);
548
- reportLines.push(`🐳 Docker: https://hub.docker.com/r/endykaufman/qwen-api-proxy`);
549
-
545
+ reportLines.push('\n📚 <b>Репозиторий:</b>');
546
+ reportLines.push('🔗 GitHub: https://github.com/EndyKaufman/FreeQwenApi');
547
+ reportLines.push('⭐ Оригинал: https://github.com/y1n7sint/FreeQwenApi');
548
+ reportLines.push('🐳 Docker: https://hub.docker.com/r/endykaufman/qwen-api-proxy');
549
+
550
550
  // Справка
551
- reportLines.push(`\n💡 <b>Справка:</b>`);
552
- reportLines.push(`📝 Используйте /help для списка команд`);
553
- reportLines.push(`🔍 Используйте /status для проверки состояния`);
554
- reportLines.push(`🤖 Используйте /chat для включения LLM режима`);
551
+ reportLines.push('\n💡 <b>Справка:</b>');
552
+ reportLines.push('📝 Используйте /help для списка команд');
553
+ reportLines.push('🔍 Используйте /status для проверки состояния');
554
+ reportLines.push('🤖 Используйте /chat для включения LLM режима');
555
555
 
556
556
  const report = reportLines.join('\n');
557
557
 
@@ -564,7 +564,7 @@ export async function checkAllSubsystems(botStarted, autoSend = true) {
564
564
  logError('❌ Не удалось отправить отчет в Telegram', e);
565
565
  }
566
566
  }
567
-
567
+
568
568
  // Возвращаем массив проверок для повторного использования
569
569
  return checks;
570
570
  }
@@ -581,7 +581,7 @@ async function fetchWithProxy(url, options = {}, skipLog = false) {
581
581
  if (proxyAgent) {
582
582
  fetchOptions.dispatcher = proxyAgent;
583
583
  if (proxyConfigured && !skipLog) {
584
- logInfo(`🌐 Запрос через прокси...`);
584
+ logInfo('🌐 Запрос через прокси...');
585
585
  }
586
586
  }
587
587
 
@@ -606,7 +606,7 @@ export async function startTelegramBot() {
606
606
 
607
607
  try {
608
608
  logInfo('🤖 Запуск Telegram бота...');
609
-
609
+
610
610
  // Загружаем сохраненные настройки
611
611
  loadPersistedSettings();
612
612
 
@@ -685,7 +685,7 @@ async function startPolling() {
685
685
  if (!response.ok) {
686
686
  const errorText = await response.text().catch(() => '');
687
687
  logError(`Ошибка получения обновлений Telegram: HTTP ${response.status}${errorText ? ` | Ответ: ${errorText.substring(0, 500)}` : ''}`);
688
- await new Promise(resolve => setTimeout(resolve, 5000));
688
+ await new Promise((resolve) => setTimeout(resolve, 5000));
689
689
  continue;
690
690
  }
691
691
 
@@ -707,7 +707,7 @@ async function startPolling() {
707
707
  }
708
708
  } catch (error) {
709
709
  logError('Ошибка в polling Telegram', error);
710
- await new Promise(resolve => setTimeout(resolve, 5000));
710
+ await new Promise((resolve) => setTimeout(resolve, 5000));
711
711
  }
712
712
  }
713
713
  }
@@ -736,10 +736,10 @@ async function processUpdate(update) {
736
736
  await handleLLMChat(chatId, message.text);
737
737
  } else {
738
738
  await sendMessage(chatId,
739
- `❌ <b>LLM чат временно недоступен</b>\n\n` +
740
- `🔒 Нет аккаунтов для обработки запросов\n` +
741
- `📦 Отправьте архив с сессиями\n` +
742
- `💡 Или используйте /chat чтобы выключить LLM режим`
739
+ '❌ <b>LLM чат временно недоступен</b>\n\n' +
740
+ '🔒 Нет аккаунтов для обработки запросов\n' +
741
+ '📦 Отправьте архив с сессиями\n' +
742
+ '💡 Или используйте /chat чтобы выключить LLM режим'
743
743
  );
744
744
  }
745
745
  return;
@@ -792,45 +792,45 @@ async function handleImageGeneration(chatId, prompt, imagePath = null) {
792
792
  if (imagePath) {
793
793
  logInfo(`📸 Режим image-to-image с файлом: ${imagePath}`);
794
794
  }
795
-
795
+
796
796
  // Отправляем сообщение о начале генерации
797
- await sendMessage(chatId,
798
- `🎨 <b>Генерация изображения...</b>\n\n` +
797
+ await sendMessage(chatId,
798
+ '🎨 <b>Генерация изображения...</b>\n\n' +
799
799
  `📝 Запрос: ${prompt}\n` +
800
- (imagePath ? `📸 Режим: Image-to-Image\n` : '') +
801
- `⏳ Пожалуйста, подождите...`
800
+ (imagePath ? '📸 Режим: Image-to-Image\n' : '') +
801
+ '⏳ Пожалуйста, подождите...'
802
802
  );
803
803
 
804
804
  // Импортируем функцию генерации
805
805
  const { generateImage } = await import('../api/imageGeneration.js');
806
806
  const { getActiveModel } = await import('./botSettings.js');
807
-
807
+
808
808
  // Используем модель для генерации изображений
809
809
  const model = 'qwen-image-plus';
810
-
810
+
811
811
  const startTime = Date.now();
812
812
  const options = {
813
813
  size: '1024*1024'
814
814
  };
815
-
815
+
816
816
  // Если есть изображение, передаем путь к файлу
817
817
  if (imagePath) {
818
818
  options.imagePath = imagePath;
819
819
  }
820
-
820
+
821
821
  const result = await generateImage(prompt, model, options);
822
822
  const generationTime = ((Date.now() - startTime) / 1000).toFixed(1);
823
823
 
824
824
  if (result.success && result.imageUrl) {
825
825
  logInfo(`✅ Изображение сгенерировано за ${generationTime}с: ${result.imageUrl}`);
826
-
826
+
827
827
  // Отправляем изображение как фото
828
828
  try {
829
829
  await sendPhoto(chatId, result.imageUrl, prompt);
830
-
830
+
831
831
  // Отправляем дополнительную информацию
832
832
  await sendMessage(chatId,
833
- `✅ <b>Изображение сгенерировано!</b>\n\n` +
833
+ '✅ <b>Изображение сгенерировано!</b>\n\n' +
834
834
  `🎨 Модель: ${result.model || model}\n` +
835
835
  `⏱️ Время: ${generationTime}с\n` +
836
836
  `📝 Prompt: ${prompt}`
@@ -839,7 +839,7 @@ async function handleImageGeneration(chatId, prompt, imagePath = null) {
839
839
  // Если не удалось отправить как фото, отправляем как ссылку
840
840
  logWarn('Не удалось отправить изображение как фото, отправляю ссылку');
841
841
  await sendMessage(chatId,
842
- `✅ <b>Изображение сгенерировано!</b>\n\n` +
842
+ '✅ <b>Изображение сгенерировано!</b>\n\n' +
843
843
  `🖼️ <a href="${result.imageUrl}">Скачать изображение</a>\n\n` +
844
844
  `🎨 Модель: ${result.model || model}\n` +
845
845
  `⏱️ Время: ${generationTime}с\n` +
@@ -848,23 +848,23 @@ async function handleImageGeneration(chatId, prompt, imagePath = null) {
848
848
  }
849
849
  } else {
850
850
  logError(`❌ Ошибка генерации изображения: ${result.error}`);
851
-
851
+
852
852
  // Проверяем, это rate limit?
853
853
  if (result.rateLimit) {
854
854
  const hours = result.rateLimitHours || 24;
855
855
  await sendMessage(chatId,
856
- `⏳ <b>Лимит генерации изображений достигнут</b>\n\n` +
857
- `⚠️ API Qwen ограничивает количество генераций в день\n` +
856
+ '⏳ <b>Лимит генерации изображений достигнут</b>\n\n' +
857
+ '⚠️ API Qwen ограничивает количество генераций в день\n' +
858
858
  `⏰ Попробуйте через ${hours}ч\n\n` +
859
- `💡 Совет: используйте другой аккаунт с токеном\n` +
860
- `📝 Или подождите сброса лимита`
859
+ '💡 Совет: используйте другой аккаунт с токеном\n' +
860
+ '📝 Или подождите сброса лимита'
861
861
  );
862
862
  return;
863
863
  }
864
-
864
+
865
865
  // Формируем детальное сообщение об ошибке
866
866
  let errorMessage = result.error || 'Неизвестная ошибка';
867
-
867
+
868
868
  // Если есть дополнительные детали в rawResponse
869
869
  if (result.rawResponse) {
870
870
  // Проверяем errorBody (JSON строка)
@@ -919,19 +919,19 @@ async function handleImageGeneration(chatId, prompt, imagePath = null) {
919
919
  }
920
920
  }
921
921
  }
922
-
922
+
923
923
  await sendMessage(chatId,
924
- `❌ <b>Ошибка генерации изображения</b>\n\n` +
924
+ '❌ <b>Ошибка генерации изображения</b>\n\n' +
925
925
  `⚠️ ${escapeHtml(errorMessage)}\n\n` +
926
- `💡 Попробуйте изменить запрос или повторите позже`
926
+ '💡 Попробуйте изменить запрос или повторите позже'
927
927
  );
928
928
  }
929
929
  } catch (error) {
930
930
  logError('❌ Ошибка в handleImageGeneration', error);
931
931
  await sendMessage(chatId,
932
- `❌ <b>Произошла ошибка</b>\n\n` +
932
+ '❌ <b>Произошла ошибка</b>\n\n' +
933
933
  `⚠️ ${error.message}\n\n` +
934
- `💡 Попробуйте позже`
934
+ '💡 Попробуйте позже'
935
935
  );
936
936
  }
937
937
  }
@@ -942,7 +942,7 @@ async function handleImageGeneration(chatId, prompt, imagePath = null) {
942
942
  async function sendPhoto(chatId, photoUrl, caption = '') {
943
943
  try {
944
944
  const url = `https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/sendPhoto`;
945
-
945
+
946
946
  const body = {
947
947
  chat_id: chatId,
948
948
  photo: photoUrl,
@@ -993,71 +993,71 @@ async function handleCommand(chatId, text) {
993
993
  }
994
994
 
995
995
  switch (command) {
996
- case '/start':
997
- case '/help':
998
- await sendHelpMessage(chatId);
999
- break;
996
+ case '/start':
997
+ case '/help':
998
+ await sendHelpMessage(chatId);
999
+ break;
1000
1000
 
1001
- case '/status':
1002
- await sendStatusMessage(chatId);
1003
- break;
1001
+ case '/status':
1002
+ await sendStatusMessage(chatId);
1003
+ break;
1004
1004
 
1005
- case '/restart':
1006
- await handleRestart(chatId);
1007
- break;
1005
+ case '/restart':
1006
+ await handleRestart(chatId);
1007
+ break;
1008
1008
 
1009
- case '/chat':
1010
- await showLLMChatStatus(chatId);
1011
- break;
1009
+ case '/chat':
1010
+ await showLLMChatStatus(chatId);
1011
+ break;
1012
1012
 
1013
- case '/togglechat':
1014
- await toggleLLMChat(chatId);
1015
- break;
1013
+ case '/togglechat':
1014
+ await toggleLLMChat(chatId);
1015
+ break;
1016
1016
 
1017
- case '/model':
1018
- await showModelInfo(chatId);
1019
- break;
1017
+ case '/model':
1018
+ await showModelInfo(chatId);
1019
+ break;
1020
1020
 
1021
- case '/setmodel':
1022
- await showModelInfo(chatId);
1023
- break;
1021
+ case '/setmodel':
1022
+ await showModelInfo(chatId);
1023
+ break;
1024
1024
 
1025
- case '/clear':
1026
- await clearChatContext(chatId);
1027
- break;
1025
+ case '/clear':
1026
+ await clearChatContext(chatId);
1027
+ break;
1028
1028
 
1029
- case '/setup':
1030
- await sendSetupMessage(chatId);
1031
- break;
1029
+ case '/setup':
1030
+ await sendSetupMessage(chatId);
1031
+ break;
1032
1032
 
1033
- case '/connect':
1034
- await sendConnectMessage(chatId);
1035
- break;
1033
+ case '/connect':
1034
+ await sendConnectMessage(chatId);
1035
+ break;
1036
1036
 
1037
- case '/about':
1038
- await sendAboutMessage(chatId);
1039
- break;
1037
+ case '/about':
1038
+ await sendAboutMessage(chatId);
1039
+ break;
1040
1040
 
1041
- case '/archive':
1042
- await sendArchiveInstructions(chatId);
1043
- break;
1041
+ case '/archive':
1042
+ await sendArchiveInstructions(chatId);
1043
+ break;
1044
1044
 
1045
- case '/extend':
1046
- // 🔧 ВРЕМЕННО ОТКЛЮЧЕНО
1047
- await sendMessage(chatId,
1048
- `🔧 <b>Команда /extend временно отключена</b>\n\n` +
1049
- `Функция продления сессий находится на техническом обслуживании.\n\n` +
1050
- `📦 <b>Что делать:</b>\n` +
1051
- `1. Создайте новую сессию: <code>npm run create-session-archive</code>\n` +
1052
- `2. Отправьте архив через бота\n\n` +
1053
- `⏳ Функция будет доступна в ближайшее время.`
1054
- );
1055
- break;
1045
+ case '/extend':
1046
+ // 🔧 ВРЕМЕННО ОТКЛЮЧЕНО
1047
+ await sendMessage(chatId,
1048
+ '🔧 <b>Команда /extend временно отключена</b>\n\n' +
1049
+ 'Функция продления сессий находится на техническом обслуживании.\n\n' +
1050
+ '📦 <b>Что делать:</b>\n' +
1051
+ '1. Создайте новую сессию: <code>npm run create-session-archive</code>\n' +
1052
+ '2. Отправьте архив через бота\n\n' +
1053
+ '⏳ Функция будет доступна в ближайшее время.'
1054
+ );
1055
+ break;
1056
1056
 
1057
- case '/image':
1058
- case '/imagine':
1059
- await sendMessage(chatId,
1060
- '🎨 <b>Генерация изображений</b>\n\n' +
1057
+ case '/image':
1058
+ case '/imagine':
1059
+ await sendMessage(chatId,
1060
+ '🎨 <b>Генерация изображений</b>\n\n' +
1061
1061
  '💬 <b>Текстовый режим:</b>\n' +
1062
1062
  '/image &lt;описание&gt; - генерация по описанию\n\n' +
1063
1063
  '📸 <b>Режим Image-to-Image:</b>\n' +
@@ -1066,21 +1066,21 @@ async function handleCommand(chatId, text) {
1066
1066
  '📝 Примеры:\n' +
1067
1067
  '• /image A beautiful sunset\n' +
1068
1068
  '• Отправьте фото с текстом "Улучши это"'
1069
- );
1070
- break;
1071
-
1072
- default:
1073
- // Проверяем, начинается ли сообщение с /image или /imagine с аргументами
1074
- if (text.startsWith('/image ') || text.startsWith('/imagine ')) {
1075
- const prompt = text.substring(text.indexOf(' ') + 1).trim();
1076
- if (prompt) {
1077
- await handleImageGeneration(chatId, prompt);
1078
- } else {
1079
- await sendMessage(chatId, '🎨 Пожалуйста, укажите описание изображения\n\nПример: /image A beautiful sunset over the ocean');
1080
- }
1069
+ );
1070
+ break;
1071
+
1072
+ default:
1073
+ // Проверяем, начинается ли сообщение с /image или /imagine с аргументами
1074
+ if (text.startsWith('/image ') || text.startsWith('/imagine ')) {
1075
+ const prompt = text.substring(text.indexOf(' ') + 1).trim();
1076
+ if (prompt) {
1077
+ await handleImageGeneration(chatId, prompt);
1081
1078
  } else {
1082
- await sendMessage(chatId, ' Неизвестная команда. Используйте /help для списка команд');
1079
+ await sendMessage(chatId, '🎨 Пожалуйста, укажите описание изображения\n\nПример: /image A beautiful sunset over the ocean');
1083
1080
  }
1081
+ } else {
1082
+ await sendMessage(chatId, '❓ Неизвестная команда. Используйте /help для списка команд');
1083
+ }
1084
1084
  }
1085
1085
  }
1086
1086
 
@@ -1090,48 +1090,48 @@ async function handleCommand(chatId, text) {
1090
1090
  async function handlePhoto(chatId, photos, caption = '') {
1091
1091
  try {
1092
1092
  logInfo(`📸 Получено фото с caption: "${caption.substring(0, 50)}${caption.length > 50 ? '...' : ''}"`);
1093
-
1093
+
1094
1094
  // Проверяем, есть ли команда в caption
1095
1095
  let prompt = caption || 'Улучши это изображение';
1096
1096
  let hasCommand = false;
1097
-
1097
+
1098
1098
  // Если caption начинается с /image или /imagine
1099
1099
  if (caption.startsWith('/image ') || caption.startsWith('/imagine ')) {
1100
1100
  hasCommand = true;
1101
1101
  prompt = caption.substring(caption.indexOf(' ') + 1).trim();
1102
1102
  }
1103
-
1103
+
1104
1104
  if (!hasCommand && !caption) {
1105
1105
  // Если просто фото без caption - не обрабатываем как image-to-image
1106
1106
  logInfo('📸 Фото без caption - пропускаем обработку');
1107
1107
  return;
1108
1108
  }
1109
-
1109
+
1110
1110
  // Telegram отправляет несколько размеров фото, берем самый большой (последний в массиве)
1111
1111
  const photo = photos[photos.length - 1];
1112
1112
  const fileId = photo.file_id;
1113
1113
  const fileSize = photo.file_size;
1114
-
1114
+
1115
1115
  logInfo(`📸 Загрузка фото из Telegram (file_id: ${fileId}, size: ${fileSize} bytes)`);
1116
-
1117
- await sendMessage(chatId,
1118
- `🎨 <b>Обработка изображения...</b>\n\n` +
1116
+
1117
+ await sendMessage(chatId,
1118
+ '🎨 <b>Обработка изображения...</b>\n\n' +
1119
1119
  `📝 Запрос: ${prompt}\n` +
1120
- `⏳ Пожалуйста, подождите...`
1120
+ '⏳ Пожалуйста, подождите...'
1121
1121
  );
1122
-
1122
+
1123
1123
  // Скачиваем фото из Telegram и сохраняем во временный файл
1124
1124
  const tempFilePath = await downloadTelegramFileToTemp(fileId);
1125
-
1125
+
1126
1126
  if (!tempFilePath) {
1127
1127
  throw new Error('Не удалось скачать фото из Telegram');
1128
1128
  }
1129
-
1129
+
1130
1130
  logInfo(`✅ Фото скачано: ${tempFilePath}`);
1131
-
1131
+
1132
1132
  // Генерируем изображение с использованием фото
1133
1133
  await handleImageGeneration(chatId, prompt, tempFilePath);
1134
-
1134
+
1135
1135
  // Удаляем временный файл
1136
1136
  try {
1137
1137
  fs.unlinkSync(tempFilePath);
@@ -1139,13 +1139,13 @@ async function handlePhoto(chatId, photos, caption = '') {
1139
1139
  } catch (e) {
1140
1140
  logWarn('Не удалось удалить временный файл', e);
1141
1141
  }
1142
-
1142
+
1143
1143
  } catch (error) {
1144
1144
  logError('❌ Ошибка в handlePhoto', error);
1145
1145
  await sendMessage(chatId,
1146
- `❌ <b>Ошибка обработки фото</b>\n\n` +
1146
+ '❌ <b>Ошибка обработки фото</b>\n\n' +
1147
1147
  `⚠️ ${error.message}\n\n` +
1148
- `💡 Попробуйте позже`
1148
+ '💡 Попробуйте позже'
1149
1149
  );
1150
1150
  }
1151
1151
  }
@@ -1160,45 +1160,45 @@ async function downloadTelegramFileToTemp(fileId) {
1160
1160
  // Получаем информацию о файле
1161
1161
  const fileUrl = `https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/getFile?file_id=${fileId}`;
1162
1162
  const fileResponse = await fetchWithProxy(fileUrl, undefined, true);
1163
-
1163
+
1164
1164
  if (!fileResponse.ok) {
1165
1165
  throw new Error(`Не удалось получить информацию о файле: HTTP ${fileResponse.status}`);
1166
1166
  }
1167
-
1167
+
1168
1168
  const fileData = await fileResponse.json();
1169
-
1169
+
1170
1170
  if (!fileData.ok) {
1171
1171
  throw new Error(`Telegram API error: ${fileData.description || 'Unknown error'}`);
1172
1172
  }
1173
-
1173
+
1174
1174
  const filePath = fileData.result.file_path;
1175
1175
  const downloadUrl = `https://api.telegram.org/file/bot${TELEGRAM_BOT_TOKEN}/${filePath}`;
1176
-
1176
+
1177
1177
  logInfo(`📥 URL для скачивания файла: ${downloadUrl}`);
1178
-
1178
+
1179
1179
  // Скачиваем файл
1180
1180
  const downloadResponse = await fetchWithProxy(downloadUrl, undefined, true);
1181
-
1181
+
1182
1182
  if (!downloadResponse.ok) {
1183
1183
  throw new Error(`Не удалось скачать файл: HTTP ${downloadResponse.status}`);
1184
1184
  }
1185
-
1185
+
1186
1186
  // Сохраняем во временный файл
1187
1187
  const tempDir = path.join(process.cwd(), 'temp');
1188
1188
  if (!fs.existsSync(tempDir)) {
1189
1189
  fs.mkdirSync(tempDir, { recursive: true });
1190
1190
  }
1191
-
1191
+
1192
1192
  const tempFileName = `telegram_${Date.now()}_${fileId}.jpg`;
1193
1193
  const tempFilePath = path.join(tempDir, tempFileName);
1194
-
1194
+
1195
1195
  const buffer = Buffer.from(await downloadResponse.arrayBuffer());
1196
1196
  fs.writeFileSync(tempFilePath, buffer);
1197
-
1197
+
1198
1198
  logInfo(`✅ Файл сохранен: ${tempFilePath} (${buffer.length} bytes)`);
1199
-
1199
+
1200
1200
  return tempFilePath;
1201
-
1201
+
1202
1202
  } catch (error) {
1203
1203
  logError('❌ Ошибка скачивания файла из Telegram', error);
1204
1204
  throw error;
@@ -1222,7 +1222,7 @@ async function handleDocument(chatId, document) {
1222
1222
  if (!allowedExtensions.includes(ext)) {
1223
1223
  await sendMessage(chatId,
1224
1224
  `❌ Неподдерживаемый формат файла: ${ext}\n` +
1225
- `📎 Поддерживаются только: .zip и .7z`
1225
+ '📎 Поддерживаются только: .zip и .7z'
1226
1226
  );
1227
1227
  return;
1228
1228
  }
@@ -1232,7 +1232,7 @@ async function handleDocument(chatId, document) {
1232
1232
  if (fileSize > maxSize) {
1233
1233
  await sendMessage(chatId,
1234
1234
  `❌ Файл слишком большой: ${(fileSize / 1024 / 1024).toFixed(2)}MB\n` +
1235
- `📏 Максимальный размер: 50MB`
1235
+ '📏 Максимальный размер: 50MB'
1236
1236
  );
1237
1237
  return;
1238
1238
  }
@@ -1246,7 +1246,7 @@ async function handleDocument(chatId, document) {
1246
1246
  logInfo(`⚠️ Архив ${fileName} уже ожидает распаковки, перезапускаем`);
1247
1247
  await sendMessage(chatId,
1248
1248
  `✅ Архив ${fileName} уже загружен\n` +
1249
- `🔄 Перезапуск для распаковки...`
1249
+ '🔄 Перезапуск для распаковки...'
1250
1250
  );
1251
1251
  // Запускаем перезапуск
1252
1252
  await gracefulRestart(chatId);
@@ -1260,7 +1260,7 @@ async function handleDocument(chatId, document) {
1260
1260
  // Проверяем существует ли уже файл в temp (по оригинальному имени)
1261
1261
  const tempDir = path.join(process.cwd(), 'temp');
1262
1262
  const existingFiles = fs.existsSync(tempDir) ? fs.readdirSync(tempDir) : [];
1263
- const existingFile = existingFiles.find(f => f.endsWith(`_${fileName}`) || f === fileName);
1263
+ const existingFile = existingFiles.find((f) => f.endsWith(`_${fileName}`) || f === fileName);
1264
1264
 
1265
1265
  if (existingFile) {
1266
1266
  const tempFilePath = path.join(tempDir, existingFile);
@@ -1274,14 +1274,14 @@ async function handleDocument(chatId, document) {
1274
1274
  ext: ext,
1275
1275
  uploadedAt: new Date().toISOString()
1276
1276
  }));
1277
- logInfo(`📝 Создан флаг для существующего архива`);
1277
+ logInfo('📝 Создан флаг для существующего архива');
1278
1278
 
1279
1279
  // Запускаем перезапуск
1280
1280
  await gracefulRestart(chatId);
1281
1281
  } else {
1282
1282
  await sendMessage(chatId,
1283
- `✅ Архив уже загружен и ожидает распаковки\n` +
1284
- `🔄 Сервис будет перезапущен...`
1283
+ '✅ Архив уже загружен и ожидает распаковки\n' +
1284
+ '🔄 Сервис будет перезапущен...'
1285
1285
  );
1286
1286
  // Запускаем перезапуск
1287
1287
  await gracefulRestart(chatId);
@@ -1313,7 +1313,7 @@ async function handleDocument(chatId, document) {
1313
1313
  logInfo(`📊 Ожидаемый размер (из Telegram): ${fileData.result.file_size || 'unknown'} bytes`);
1314
1314
 
1315
1315
  // Скачиваем файл
1316
- logInfo(`📥 Начинаем загрузку файла...`);
1316
+ logInfo('📥 Начинаем загрузку файла...');
1317
1317
  const downloadResponse = await fetchWithProxy(downloadUrl);
1318
1318
  logInfo(`📥 Статус загрузки: ${downloadResponse.status}`);
1319
1319
  logInfo(`📥 Content-Type: ${downloadResponse.headers.get('content-type')}`);
@@ -1326,7 +1326,7 @@ async function handleDocument(chatId, document) {
1326
1326
  logInfo(`📥 Загружено ${fileSize} bytes из Telegram`);
1327
1327
 
1328
1328
  if (fileSize === 0) {
1329
- logError(`❌ Загружен пустой файл!`);
1329
+ logError('❌ Загружен пустой файл!');
1330
1330
  logError(`📡 Статус ответа: ${downloadResponse.status}`);
1331
1331
  logError(`📊 Content-Length header: ${downloadResponse.headers.get('content-length')}`);
1332
1332
  logError(`🔗 URL: ${downloadUrl}`);
@@ -1348,10 +1348,10 @@ async function handleDocument(chatId, document) {
1348
1348
  const tempFilePath = path.join(tempDir, uniqueFileName);
1349
1349
 
1350
1350
  // Удаляем старые файлы с таким же именем (если есть)
1351
- const oldFiles = existingFiles.filter(f => f.endsWith(`_${fileName}`) || f === fileName);
1351
+ const oldFiles = existingFiles.filter((f) => f.endsWith(`_${fileName}`) || f === fileName);
1352
1352
  if (oldFiles.length > 0) {
1353
1353
  logInfo(`🗑️ Найдено ${oldFiles.length} старых файлов с именем ${fileName}, удаляем`);
1354
- oldFiles.forEach(oldFile => {
1354
+ oldFiles.forEach((oldFile) => {
1355
1355
  try {
1356
1356
  fs.unlinkSync(path.join(tempDir, oldFile));
1357
1357
  logInfo(`🗑️ Удален старый файл: ${oldFile}`);
@@ -1374,7 +1374,7 @@ async function handleDocument(chatId, document) {
1374
1374
  }
1375
1375
 
1376
1376
  logInfo(`✅ Файл сохранен: ${tempFilePath}`);
1377
- await sendMessage(chatId, `✅ Файл загружен. Распаковка...`);
1377
+ await sendMessage(chatId, '✅ Файл загружен. Распаковка...');
1378
1378
 
1379
1379
  // Сохраняем информацию об архиве для обработки при перезапуске
1380
1380
  fs.writeFileSync(archiveInfoPath, JSON.stringify({
@@ -1386,8 +1386,8 @@ async function handleDocument(chatId, document) {
1386
1386
 
1387
1387
  logInfo(`📝 Записан флаг ожидающего архива: ${archiveInfoPath}`);
1388
1388
  await sendMessage(chatId,
1389
- `✅ Архив сохранен\n` +
1390
- `🔄 При перезапуске будет автоматически распакован\n` +
1389
+ '✅ Архив сохранен\n' +
1390
+ '🔄 При перезапуске будет автоматически распакован\n' +
1391
1391
  `📂 Файл: ${tempFilePath}`
1392
1392
  );
1393
1393
 
@@ -1428,9 +1428,9 @@ async function createSessionBackup(sessionPath, chatId) {
1428
1428
  logWarn('⛔ session_backup отменен - СЕРВЕР НЕ БУДЕТ ПЕРЕЗАПУЩЕН');
1429
1429
  try {
1430
1430
  await sendMessage(chatId,
1431
- `❌ Ошибка session_backup: нет прав для создания папки\n` +
1432
- `⛔ Распаковка продолжена, но сервер НЕ будет перезапущен\n` +
1433
- `💡 Перезапустите сервер вручную после проверки файлов`
1431
+ '❌ Ошибка session_backup: нет прав для создания папки\n' +
1432
+ '⛔ Распаковка продолжена, но сервер НЕ будет перезапущен\n' +
1433
+ '💡 Перезапустите сервер вручную после проверки файлов'
1434
1434
  );
1435
1435
  } catch (sendError) {
1436
1436
  // Игнорируем
@@ -1448,7 +1448,7 @@ async function createSessionBackup(sessionPath, chatId) {
1448
1448
 
1449
1449
  try {
1450
1450
  await sendMessage(chatId,
1451
- `💾 Создание session_backup текущей session...\n` +
1451
+ '💾 Создание session_backup текущей session...\n' +
1452
1452
  `📁 Найдено элементов: ${items.length}`
1453
1453
  );
1454
1454
  } catch (sendError) {
@@ -1489,8 +1489,8 @@ async function createSessionBackup(sessionPath, chatId) {
1489
1489
  try {
1490
1490
  await sendMessage(chatId,
1491
1491
  `❌ Ошибка session_backup: ${error.message}\n` +
1492
- `⛔ Распаковка продолжена, но сервер НЕ будет перезапущен\n` +
1493
- `💡 Перезапустите сервер вручную после проверки файлов`
1492
+ '⛔ Распаковка продолжена, но сервер НЕ будет перезапущен\n' +
1493
+ '💡 Перезапустите сервер вручную после проверки файлов'
1494
1494
  );
1495
1495
  } catch (sendError) {
1496
1496
  // Игнорируем
@@ -1513,10 +1513,10 @@ async function extractArchive(filePath, chatId, ext) {
1513
1513
  if (!backupSuccess) {
1514
1514
  logWarn('⛔ Распаковка отменена из-за ошибки backup');
1515
1515
  await sendMessage(chatId,
1516
- `⚠️ <b>Распаковка отменена</b>\n\n` +
1517
- `❌ Не удалось создать backup текущей session\n` +
1518
- `🔒 Файлы не были изменены для безопасности\n` +
1519
- `💡 Проверьте права доступа и попробуйте снова`
1516
+ '⚠️ <b>Распаковка отменена</b>\n\n' +
1517
+ '❌ Не удалось создать backup текущей session\n' +
1518
+ '🔒 Файлы не были изменены для безопасности\n' +
1519
+ '💡 Проверьте права доступа и попробуйте снова'
1520
1520
  );
1521
1521
  return; // Выходим без перезапуска
1522
1522
  }
@@ -1530,9 +1530,9 @@ async function extractArchive(filePath, chatId, ext) {
1530
1530
  }
1531
1531
 
1532
1532
  let statusMessage =
1533
- `✅ <b>Архив успешно распакован!</b>\n\n` +
1534
- `📂 Папка session обновлена\n` +
1535
- `💾 Старая версия сохранена в session_backup\n`;
1533
+ '✅ <b>Архив успешно распакован!</b>\n\n' +
1534
+ '📂 Папка session обновлена\n' +
1535
+ '💾 Старая версия сохранена в session_backup\n';
1536
1536
 
1537
1537
  if (result && result.successCount !== 'все') {
1538
1538
  statusMessage += `📊 Распаковано: ${result.successCount} файлов\n`;
@@ -1541,12 +1541,12 @@ async function extractArchive(filePath, chatId, ext) {
1541
1541
  }
1542
1542
  }
1543
1543
 
1544
- statusMessage += `🔄 Сервис будет перезапущен...`;
1544
+ statusMessage += '🔄 Сервис будет перезапущен...';
1545
1545
 
1546
1546
  await sendMessage(chatId, statusMessage);
1547
1547
 
1548
1548
  // Ждем 2 секунды чтобы сообщение дошло
1549
- await new Promise(resolve => setTimeout(resolve, 2000));
1549
+ await new Promise((resolve) => setTimeout(resolve, 2000));
1550
1550
 
1551
1551
  // Запускаем перезапуск только если backup был успешен
1552
1552
  logInfo('✅ Backup успешен, запускаем перезапуск сервера');
@@ -1567,28 +1567,56 @@ async function extractZip(zipPath, sessionPath, chatId) {
1567
1567
  const zip = new AdmZip(zipPath);
1568
1568
  const zipEntries = zip.getEntries();
1569
1569
 
1570
+ // Нормализуем пути: заменяем обратные слеши на прямые (Windows -> Unix)
1571
+ const normalizedEntries = zipEntries.map((entry) => ({
1572
+ ...entry,
1573
+ normalizedPath: entry.entryName.replace(/\\/g, '/')
1574
+ }));
1575
+
1570
1576
  // Для отладки: показываем первые несколько записей в архиве
1571
- const firstEntries = zipEntries.slice(0, 20).map(e => e.entryName);
1572
- logInfo(`📂 Первые 20 записей в ZIP архиве:`);
1577
+ const firstEntries = normalizedEntries.slice(0, 20).map((e) => e.normalizedPath);
1578
+ logInfo('📂 Первые 20 записей в ZIP архиве:');
1573
1579
  logInfo(firstEntries.join('\n'));
1574
1580
 
1575
- // Проверяем что есть папка session
1576
- const hasSessionFolder = zipEntries.some(entry =>
1577
- entry.entryName.startsWith('session/') || entry.entryName === 'session'
1578
- );
1581
+ // Проверяем что есть папка session (с нормализованными путями)
1582
+ // Поддерживаем разные варианты:
1583
+ // 1. "session/" - папка session в корне
1584
+ // 2. "session" - ровно папка session (без слеша)
1585
+ // 3. "session/accounts/..." - содержимое session
1586
+ const hasSessionFolder = normalizedEntries.some((entry) => {
1587
+ const p = entry.normalizedPath;
1588
+ // Точное совпадение "session" или "session/"
1589
+ if (p === 'session' || p === 'session/') {return true;}
1590
+ // Начинается с "session/"
1591
+ if (p.startsWith('session/')) {return true;}
1592
+ // Для ZIP созданных на Windows может быть "session\" который уже нормализован
1593
+ return false;
1594
+ });
1595
+
1596
+ if (hasSessionFolder) {
1597
+ logInfo('✅ Найдена папка session в корне архива');
1598
+ // Логируем пример найденного пути для отладки
1599
+ const sampleEntry = normalizedEntries.find((e) => {
1600
+ const p = e.normalizedPath;
1601
+ return p === 'session' || p === 'session/' || p.startsWith('session/');
1602
+ });
1603
+ if (sampleEntry) {
1604
+ logInfo(`📂 Пример: ${sampleEntry.normalizedPath}`);
1605
+ }
1606
+ }
1579
1607
 
1580
1608
  if (!hasSessionFolder) {
1581
1609
  // Пытаемся найти session в любом месте архива
1582
- const sessionEntries = zipEntries.filter(e =>
1583
- e.entryName.includes('session') || e.entryName.includes('Session')
1610
+ const sessionEntries = normalizedEntries.filter((e) =>
1611
+ e.normalizedPath.includes('session') || e.normalizedPath.includes('Session')
1584
1612
  );
1585
1613
 
1586
1614
  if (sessionEntries.length > 0) {
1587
- logInfo(`🔍 Найдены записи содержащие 'session':`);
1588
- logInfo(sessionEntries.slice(0, 10).map(e => e.entryName).join('\n'));
1615
+ logInfo('🔍 Найдены записи содержащие \'session\':');
1616
+ logInfo(sessionEntries.slice(0, 10).map((e) => e.normalizedPath).join('\n'));
1589
1617
  reject(new Error(
1590
- `Архив содержит '${sessionEntries[0].entryName}', но не содержит 'session/' в корне. ` +
1591
- `Переместите папку session/ в корень архива`
1618
+ `Архив содержит '${sessionEntries[0].normalizedPath}', но не содержит 'session/' в корне. ` +
1619
+ 'Переместите папку session/ в корень архива'
1592
1620
  ));
1593
1621
  } else {
1594
1622
  reject(new Error('Архив не содержит папку "session". Проверите что архив содержит папку session/ в корне'));
@@ -1596,8 +1624,6 @@ async function extractZip(zipPath, sessionPath, chatId) {
1596
1624
  return;
1597
1625
  }
1598
1626
 
1599
- logInfo('✅ Найдена папка session в корне архива');
1600
-
1601
1627
  // Создаем папку session если не существует
1602
1628
  if (!fs.existsSync(sessionPath)) {
1603
1629
  fs.mkdirSync(sessionPath, { recursive: true });
@@ -1607,10 +1633,10 @@ async function extractZip(zipPath, sessionPath, chatId) {
1607
1633
  let successCount = 0;
1608
1634
  let errorCount = 0;
1609
1635
 
1610
- zipEntries.forEach(entry => {
1611
- if (entry.entryName.startsWith('session/')) {
1636
+ normalizedEntries.forEach((entry) => {
1637
+ if (entry.normalizedPath.startsWith('session/')) {
1612
1638
  try {
1613
- const relativePath = entry.entryName.substring('session/'.length);
1639
+ const relativePath = entry.normalizedPath.substring('session/'.length);
1614
1640
  if (relativePath) {
1615
1641
  const targetPath = path.join(sessionPath, relativePath);
1616
1642
 
@@ -1631,7 +1657,7 @@ async function extractZip(zipPath, sessionPath, chatId) {
1631
1657
  }
1632
1658
  } catch (err) {
1633
1659
  errorCount++;
1634
- logWarn(`⚠️ Ошибка распаковки ${entry.entryName}: ${err.message}`);
1660
+ logWarn(`⚠️ Ошибка распаковки ${entry.normalizedPath}: ${err.message}`);
1635
1661
  }
1636
1662
  }
1637
1663
  });
@@ -1753,7 +1779,7 @@ async function extract7z(sevenZPath, sessionPath, chatId) {
1753
1779
  const items = fs.readdirSync(sourceDir);
1754
1780
  logInfo(`📂 Найдено элементов для копирования: ${items.length}`);
1755
1781
 
1756
- items.forEach(item => {
1782
+ items.forEach((item) => {
1757
1783
  const sourcePath = path.join(sourceDir, item);
1758
1784
  const targetPath = path.join(targetDir, item);
1759
1785
 
@@ -1819,8 +1845,8 @@ async function gracefulRestart(chatId) {
1819
1845
  logInfo('🔄 Запуск корректного перезапуска сервиса...');
1820
1846
 
1821
1847
  await sendMessage(chatId,
1822
- `🔄 <b>Перезапуск сервиса...</b>\n\n` +
1823
- `⏱️ Сервис будет перезапущен в течение 5 секунд`
1848
+ '🔄 <b>Перезапуск сервиса...</b>\n\n' +
1849
+ '⏱️ Сервис будет перезапущен в течение 5 секунд'
1824
1850
  );
1825
1851
 
1826
1852
  // Даем Docker Compose время на перезапуск
@@ -1854,51 +1880,51 @@ async function sendHelpMessage(chatId) {
1854
1880
  const accountsExist = hasAccounts();
1855
1881
 
1856
1882
  let helpText =
1857
- `🤖 <b>FreeQwenApi Bot - Управление</b>\n\n` +
1858
- `📋 <b>Команды управления:</b>\n\n` +
1859
- `/help - Показать это сообщение\n` +
1860
- `/status - Показать статус сервиса\n` +
1861
- `/restart - Перезапустить сервис\n` +
1862
- `<s>/extend</s> - 🔧 Временно отключено\n\n`;
1883
+ '🤖 <b>FreeQwenApi Bot - Управление</b>\n\n' +
1884
+ '📋 <b>Команды управления:</b>\n\n' +
1885
+ '/help - Показать это сообщение\n' +
1886
+ '/status - Показать статус сервиса\n' +
1887
+ '/restart - Перезапустить сервис\n' +
1888
+ '<s>/extend</s> - 🔧 Временно отключено\n\n';
1863
1889
 
1864
1890
  // Команды генерации изображений
1865
1891
  helpText +=
1866
- `🎨 <b>Генерация изображений:</b>\n\n` +
1867
- `/image &lt;описание&gt; - Сгенерировать изображение\n` +
1868
- `/imagine &lt;описание&gt; - Альтернативная команда\n\n` +
1869
- `💡 Пример: /image A beautiful sunset over the ocean\n\n`;
1892
+ '🎨 <b>Генерация изображений:</b>\n\n' +
1893
+ '/image &lt;описание&gt; - Сгенерировать изображение\n' +
1894
+ '/imagine &lt;описание&gt; - Альтернативная команда\n\n' +
1895
+ '💡 Пример: /image A beautiful sunset over the ocean\n\n';
1870
1896
 
1871
1897
  // Показываем LLM команды только если есть аккаунты
1872
1898
  if (accountsExist) {
1873
1899
  helpText +=
1874
- `🤖 <b>LLM Чат (AI ассистент):</b>\n\n` +
1875
- `/chat - Показать состояние LLM чата\n` +
1876
- `/togglechat - Включить/выключить LLM чат\n` +
1877
- `/clear - Очистить контекст чата\n` +
1878
- `/model - Информация о модели\n` +
1879
- `/setmodel &lt;название&gt; - Сменить модель\n\n` +
1880
- `💡 Когда LLM чат включен, просто отправляйте сообщения!\n\n`;
1900
+ '🤖 <b>LLM Чат (AI ассистент):</b>\n\n' +
1901
+ '/chat - Показать состояние LLM чата\n' +
1902
+ '/togglechat - Включить/выключить LLM чат\n' +
1903
+ '/clear - Очистить контекст чата\n' +
1904
+ '/model - Информация о модели\n' +
1905
+ '/setmodel &lt;название&gt; - Сменить модель\n\n' +
1906
+ '💡 Когда LLM чат включен, просто отправляйте сообщения!\n\n';
1881
1907
  } else {
1882
1908
  helpText +=
1883
- `⚠️ <b>LLM Чат недоступен</b>\n\n` +
1884
- `🔒 Функции AI ассистента временно недоступны\n` +
1885
- `📦 Отправьте архив с сессиями для активации\n\n`;
1909
+ '⚠️ <b>LLM Чат недоступен</b>\n\n' +
1910
+ '🔒 Функции AI ассистента временно недоступны\n' +
1911
+ '📦 Отправьте архив с сессиями для активации\n\n';
1886
1912
  }
1887
1913
 
1888
1914
  helpText +=
1889
- `📦 <b>Загрузка сессий:</b>\n\n` +
1890
- `Отправьте ZIP или 7z архив с папкой "session" внутри.\n` +
1891
- `Бот распакует его и перезапустит сервис.\n\n` +
1892
- `/archive - Инструкция по созданию архива\n\n` +
1893
- `📏 <b>Лимиты:</b>\n` +
1894
- `• Максимальный размер файла: 50MB\n` +
1895
- `• Поддерживаемые форматы: .zip, .7z\n\n` +
1896
- `📚 <b>Дополнительные команды:</b>\n\n` +
1897
- `/setup - Инструкция по созданию сессии\n` +
1898
- `/connect - Как подключить к проекту\n` +
1899
- `/about - Информация о проекте\n\n` +
1900
- `🐳 <b>Docker:</b>\n` +
1901
- `https://hub.docker.com/r/endykaufman/qwen-api-proxy`;
1915
+ '📦 <b>Загрузка сессий:</b>\n\n' +
1916
+ 'Отправьте ZIP или 7z архив с папкой "session" внутри.\n' +
1917
+ 'Бот распакует его и перезапустит сервис.\n\n' +
1918
+ '/archive - Инструкция по созданию архива\n\n' +
1919
+ '📏 <b>Лимиты:</b>\n' +
1920
+ '• Максимальный размер файла: 50MB\n' +
1921
+ '• Поддерживаемые форматы: .zip, .7z\n\n' +
1922
+ '📚 <b>Дополнительные команды:</b>\n\n' +
1923
+ '/setup - Инструкция по созданию сессии\n' +
1924
+ '/connect - Как подключить к проекту\n' +
1925
+ '/about - Информация о проекте\n\n' +
1926
+ '🐳 <b>Docker:</b>\n' +
1927
+ 'https://hub.docker.com/r/endykaufman/qwen-api-proxy';
1902
1928
 
1903
1929
  await sendMessage(chatId, helpText);
1904
1930
  }
@@ -1908,179 +1934,179 @@ async function sendHelpMessage(chatId) {
1908
1934
  */
1909
1935
  async function sendSetupMessage(chatId) {
1910
1936
  const setupText =
1911
- `🛠️ <b>Создание сессии авторизации</b>\n\n` +
1912
- `<b>📖 Что нужно знать:</b>\n` +
1913
- `• <b>Git</b> - система управления версиями (опционально)\n` +
1914
- `• <b>Docker Compose</b> - инструмент для управления контейнерами\n` +
1915
- `• Если используете Docker Desktop - Compose уже встроен!\n\n` +
1916
- `━━━━━━━━━━━━━━━━━━━━━━━━━\n\n` +
1917
- `<b>Способ 1: Локальная установка (Node.js)</b>\n\n` +
1918
- `<b>Вариант A: С Git:</b>\n` +
1919
- `1. <code>git clone https://github.com/EndyKaufman/FreeQwenApi</code>\n` +
1920
- `2. <code>cd FreeQwenApi</code>\n` +
1921
- `3. <code>npm install</code>\n` +
1922
- `4. <code>npm start</code>\n` +
1923
- `5. Выберите <code>1</code> - добавить аккаунт\n` +
1924
- `6. Войдите в аккаунт Qwen в браузере\n` +
1925
- `7. Токен сохранится автоматически\n\n` +
1926
- `<b>Вариант B: Без Git (ZIP):</b>\n` +
1927
- `1. Скачайте ZIP: https://github.com/EndyKaufman/FreeQwenApi\n` +
1928
- `2. Нажмите <b>"<> Code"</b> → <b>"Download ZIP"</b>\n` +
1929
- `3. Распакуйте и перейдите в папку\n` +
1930
- `4. <code>npm install</code>\n` +
1931
- `5. <code>npm start</code> → выберите <code>1</code>\n\n` +
1932
- `<b>Способ 2: Docker</b>\n\n` +
1933
- `<b>Что такое Docker Compose?</b>\n` +
1934
- `• Входит в Docker Desktop (Windows/macOS)\n` +
1935
- `• Linux: <code>sudo apt install docker-compose-plugin</code>\n` +
1936
- `• Проверка: <code>docker compose version</code>\n\n` +
1937
- `<b>С Compose:</b>\n` +
1938
- `1. Сначала создайте сессию локально:\n` +
1939
- ` <code>npm run auth</code> (или <code>npm start</code> → <code>1</code>)\n` +
1940
- `2. Соберите Docker:\n` +
1941
- ` <code>docker compose build --no-cache</code>\n` +
1942
- `3. Запустите:\n` +
1943
- ` <code>docker compose up -d</code>\n\n` +
1944
- `<b>Без Compose (обычный Docker):</b>\n` +
1945
- `1. Создайте сессию локально (см. Способ 1)\n` +
1946
- `2. Соберите образ:\n` +
1947
- ` <code>docker build -t qwen-proxy .</code>\n` +
1948
- `3. Запустите:\n` +
1949
- ` <code>docker run -d --name qwen-proxy -p 3264:3264 -e SKIP_ACCOUNT_MENU=true -v $(pwd)/session:/app/session qwen-proxy</code>\n\n` +
1950
- `<b>Структура папки session:</b>\n` +
1951
- `<code>session/</code>\n` +
1952
- `├── <code>accounts/</code>\n` +
1953
- `│ ├── <code>acc_123456/</code>\n` +
1954
- `│ │ └── <code>token.txt</code>\n` +
1955
- `│ └── <code>acc_789012/</code>\n` +
1956
- `│ └── <code>token.txt</code>\n` +
1957
- `└── <code>tokens.json</code>\n\n` +
1958
- `💡 <b>Совет:</b> Используйте /archive для подробной инструкции\n\n` +
1959
- `📖 Подробнее: https://github.com/EndyKaufman/FreeQwenApi`;
1960
-
1937
+ '🛠️ <b>Создание сессии авторизации</b>\n\n' +
1938
+ '<b>📖 Что нужно знать:</b>\n' +
1939
+ '• <b>Git</b> - система управления версиями (опционально)\n' +
1940
+ '• <b>Docker Compose</b> - инструмент для управления контейнерами\n' +
1941
+ '• Если используете Docker Desktop - Compose уже встроен!\n\n' +
1942
+ '━━━━━━━━━━━━━━━━━━━━━━━━━\n\n' +
1943
+ '<b>Способ 1: Локальная установка (Node.js)</b>\n\n' +
1944
+ '<b>Вариант A: С Git:</b>\n' +
1945
+ '1. <code>git clone https://github.com/EndyKaufman/FreeQwenApi</code>\n' +
1946
+ '2. <code>cd FreeQwenApi</code>\n' +
1947
+ '3. <code>npm install</code>\n' +
1948
+ '4. <code>npm start</code>\n' +
1949
+ '5. Выберите <code>1</code> - добавить аккаунт\n' +
1950
+ '6. Войдите в аккаунт Qwen в браузере\n' +
1951
+ '7. Токен сохранится автоматически\n\n' +
1952
+ '<b>Вариант B: Без Git (ZIP):</b>\n' +
1953
+ '1. Скачайте ZIP: https://github.com/EndyKaufman/FreeQwenApi\n' +
1954
+ '2. Нажмите <b>"<> Code"</b> → <b>"Download ZIP"</b>\n' +
1955
+ '3. Распакуйте и перейдите в папку\n' +
1956
+ '4. <code>npm install</code>\n' +
1957
+ '5. <code>npm start</code> → выберите <code>1</code>\n\n' +
1958
+ '<b>Способ 2: Docker</b>\n\n' +
1959
+ '<b>Что такое Docker Compose?</b>\n' +
1960
+ '• Входит в Docker Desktop (Windows/macOS)\n' +
1961
+ '• Linux: <code>sudo apt install docker-compose-plugin</code>\n' +
1962
+ '• Проверка: <code>docker compose version</code>\n\n' +
1963
+ '<b>С Compose:</b>\n' +
1964
+ '1. Сначала создайте сессию локально:\n' +
1965
+ ' <code>npm run auth</code> (или <code>npm start</code> → <code>1</code>)\n' +
1966
+ '2. Соберите Docker:\n' +
1967
+ ' <code>docker compose build --no-cache</code>\n' +
1968
+ '3. Запустите:\n' +
1969
+ ' <code>docker compose up -d</code>\n\n' +
1970
+ '<b>Без Compose (обычный Docker):</b>\n' +
1971
+ '1. Создайте сессию локально (см. Способ 1)\n' +
1972
+ '2. Соберите образ:\n' +
1973
+ ' <code>docker build -t qwen-proxy .</code>\n' +
1974
+ '3. Запустите:\n' +
1975
+ ' <code>docker run -d --name qwen-proxy -p 3264:3264 -e SKIP_ACCOUNT_MENU=true -v $(pwd)/session:/app/session qwen-proxy</code>\n\n' +
1976
+ '<b>Структура папки session:</b>\n' +
1977
+ '<code>session/</code>\n' +
1978
+ '├── <code>accounts/</code>\n' +
1979
+ '│ ├── <code>acc_123456/</code>\n' +
1980
+ '│ │ └── <code>token.txt</code>\n' +
1981
+ '│ └── <code>acc_789012/</code>\n' +
1982
+ '│ └── <code>token.txt</code>\n' +
1983
+ '└── <code>tokens.json</code>\n\n' +
1984
+ '💡 <b>Совет:</b> Используйте /archive для подробной инструкции\n\n' +
1985
+ '📖 Подробнее: https://github.com/EndyKaufman/FreeQwenApi';
1986
+
1961
1987
  await sendMessage(chatId, setupText);
1962
1988
  }
1963
1989
 
1964
1990
  async function sendConnectMessage(chatId) {
1965
1991
  const connectText =
1966
- `🔌 <b>Подключение FreeQwenApi к проекту</b>\n\n` +
1967
- `<b>📖 Что такое Docker Compose?</b>\n` +
1968
- `Это инструмент для запуска многоконтейнерных приложений.\n` +
1969
- `Он управляет контейнерами через файл <code>docker-compose.yml</code>.\n\n` +
1970
- `<b>Установка Docker Compose:</b>\n` +
1971
- `• Входит в <b>Docker Desktop</b> (Windows/macOS)\n` +
1972
- `• Linux: <code>sudo apt install docker-compose-plugin</code>\n` +
1973
- `• Проверка: <code>docker compose version</code>\n\n` +
1974
- `💡 <i>Если Docker Desktop установлен - Compose уже есть!</i>\n\n` +
1975
- `<b>━━━━━━━━━━━━━━━━━━━━━━━━━</b>\n\n` +
1976
- `<b>Шаг 1: Запуск через Docker Compose</b>\n\n` +
1977
- `Добавьте в ваш <code>docker-compose.yml</code>:\n\n` +
1978
- `<pre>\n` +
1979
- `services:\n` +
1980
- ` qwen-proxy:\n` +
1981
- ` build: .\n` +
1982
- ` container_name: qwen-proxy\n` +
1983
- ` environment:\n` +
1984
- ` - SKIP_ACCOUNT_MENU=true\n` +
1985
- ` - PORT=3264\n` +
1986
- ` ports:\n` +
1987
- ` - "3264:3264"\n` +
1988
- ` volumes:\n` +
1989
- ` - ./session_backup:/app/session_backup\n` +
1990
- ` - ./session:/app/session\n` +
1991
- ` - ./logs:/app/logs\n` +
1992
- ` - ./uploads:/app/uploads\n` +
1993
- ` - ./temp:/app/temp\n` +
1994
- ` restart: unless-stopped\n` +
1995
- `</pre>\n\n` +
1996
- `Или используйте наш <code>docker-compose.yml</code>:\n` +
1997
- `<code>docker compose up -d</code>\n\n` +
1998
- `<b>Альтернатива: Без Docker Compose</b>\n\n` +
1999
- `Если Compose не установлен, используйте обычный Docker:\n\n` +
2000
- `<pre>\n` +
2001
- `docker build -t qwen-proxy .\n` +
2002
- `docker run -d \\\n` +
2003
- ` --name qwen-proxy \\\n` +
2004
- ` -p 3264:3264 \\\n` +
2005
- ` -e SKIP_ACCOUNT_MENU=true \\\n` +
2006
- ` -v $(pwd)/session:/app/session \\\n` +
2007
- ` -v $(pwd)/logs:/app/logs \\\n` +
2008
- ` -v $(pwd)/uploads:/app/uploads \\\n` +
2009
- ` -v $(pwd)/temp:/app/temp \\\n` +
2010
- ` qwen-proxy\n` +
2011
- `</pre>\n\n` +
2012
- `<b>━━━━━━━━━━━━━━━━━━━━━━━━━</b>\n\n` +
2013
- `<b>Шаг 2: Первый запрос через curl</b>\n\n` +
2014
- `<b>Простой запрос:</b>\n` +
2015
- `<pre>\n` +
2016
- `curl http://localhost:3264/api/chat/completions \\\n` +
2017
- ` -H "Content-Type: application/json" \\\n` +
2018
- ` -d '{"model":"qwen3.5-plus","messages":[{"role":"user","content":"Привет!"}]}'\n` +
2019
- `</pre>\n\n` +
2020
- `<b>С продолжением диалога:</b>\n` +
2021
- `<pre>\n` +
2022
- `curl -X POST http://localhost:3264/api/chat/completions \\\n` +
2023
- ` -H "Content-Type: application/json" \\\n` +
2024
- ` -d '{\n` +
2025
- ` "model": "qwen3.5-plus",\n` +
2026
- ` "messages": [{"role": "user", "content": "Сколько будет 2+2?"}]\n` +
2027
- ` }'\n` +
2028
- `</pre>\n\n` +
2029
- `<b>Шаг 3: Использование с OpenAI SDK</b>\n\n` +
2030
- `<pre>\n` +
2031
- `import OpenAI from 'openai';\n\n` +
2032
- `const client = new OpenAI({\n` +
2033
- ` baseURL: 'http://localhost:3264/api',\n` +
2034
- ` apiKey: 'any-string'\n` +
2035
- `});\n\n` +
2036
- `const response = await client.chat.completions.create({\n` +
2037
- ` model: 'qwen3.5-plus',\n` +
2038
- ` messages: [{ role: 'user', content: 'Привет!' }]\n` +
2039
- `});\n` +
2040
- `</pre>\n\n` +
2041
- `📖 API Docs: http://localhost:3264/api\n` +
2042
- `📚 GitHub: https://github.com/EndyKaufman/FreeQwenApi`;
2043
-
1992
+ '🔌 <b>Подключение FreeQwenApi к проекту</b>\n\n' +
1993
+ '<b>📖 Что такое Docker Compose?</b>\n' +
1994
+ 'Это инструмент для запуска многоконтейнерных приложений.\n' +
1995
+ 'Он управляет контейнерами через файл <code>docker-compose.yml</code>.\n\n' +
1996
+ '<b>Установка Docker Compose:</b>\n' +
1997
+ '• Входит в <b>Docker Desktop</b> (Windows/macOS)\n' +
1998
+ '• Linux: <code>sudo apt install docker-compose-plugin</code>\n' +
1999
+ '• Проверка: <code>docker compose version</code>\n\n' +
2000
+ '💡 <i>Если Docker Desktop установлен - Compose уже есть!</i>\n\n' +
2001
+ '<b>━━━━━━━━━━━━━━━━━━━━━━━━━</b>\n\n' +
2002
+ '<b>Шаг 1: Запуск через Docker Compose</b>\n\n' +
2003
+ 'Добавьте в ваш <code>docker-compose.yml</code>:\n\n' +
2004
+ '<pre>\n' +
2005
+ 'services:\n' +
2006
+ ' qwen-proxy:\n' +
2007
+ ' build: .\n' +
2008
+ ' container_name: qwen-proxy\n' +
2009
+ ' environment:\n' +
2010
+ ' - SKIP_ACCOUNT_MENU=true\n' +
2011
+ ' - PORT=3264\n' +
2012
+ ' ports:\n' +
2013
+ ' - "3264:3264"\n' +
2014
+ ' volumes:\n' +
2015
+ ' - ./session_backup:/app/session_backup\n' +
2016
+ ' - ./session:/app/session\n' +
2017
+ ' - ./logs:/app/logs\n' +
2018
+ ' - ./uploads:/app/uploads\n' +
2019
+ ' - ./temp:/app/temp\n' +
2020
+ ' restart: unless-stopped\n' +
2021
+ '</pre>\n\n' +
2022
+ 'Или используйте наш <code>docker-compose.yml</code>:\n' +
2023
+ '<code>docker compose up -d</code>\n\n' +
2024
+ '<b>Альтернатива: Без Docker Compose</b>\n\n' +
2025
+ 'Если Compose не установлен, используйте обычный Docker:\n\n' +
2026
+ '<pre>\n' +
2027
+ 'docker build -t qwen-proxy .\n' +
2028
+ 'docker run -d \\\n' +
2029
+ ' --name qwen-proxy \\\n' +
2030
+ ' -p 3264:3264 \\\n' +
2031
+ ' -e SKIP_ACCOUNT_MENU=true \\\n' +
2032
+ ' -v $(pwd)/session:/app/session \\\n' +
2033
+ ' -v $(pwd)/logs:/app/logs \\\n' +
2034
+ ' -v $(pwd)/uploads:/app/uploads \\\n' +
2035
+ ' -v $(pwd)/temp:/app/temp \\\n' +
2036
+ ' qwen-proxy\n' +
2037
+ '</pre>\n\n' +
2038
+ '<b>━━━━━━━━━━━━━━━━━━━━━━━━━</b>\n\n' +
2039
+ '<b>Шаг 2: Первый запрос через curl</b>\n\n' +
2040
+ '<b>Простой запрос:</b>\n' +
2041
+ '<pre>\n' +
2042
+ 'curl http://localhost:3264/api/chat/completions \\\n' +
2043
+ ' -H "Content-Type: application/json" \\\n' +
2044
+ ' -d \'{"model":"qwen3.5-plus","messages":[{"role":"user","content":"Привет!"}]}\'\n' +
2045
+ '</pre>\n\n' +
2046
+ '<b>С продолжением диалога:</b>\n' +
2047
+ '<pre>\n' +
2048
+ 'curl -X POST http://localhost:3264/api/chat/completions \\\n' +
2049
+ ' -H "Content-Type: application/json" \\\n' +
2050
+ ' -d \'{\n' +
2051
+ ' "model": "qwen3.5-plus",\n' +
2052
+ ' "messages": [{"role": "user", "content": "Сколько будет 2+2?"}]\n' +
2053
+ ' }\'\n' +
2054
+ '</pre>\n\n' +
2055
+ '<b>Шаг 3: Использование с OpenAI SDK</b>\n\n' +
2056
+ '<pre>\n' +
2057
+ 'import OpenAI from \'openai\';\n\n' +
2058
+ 'const client = new OpenAI({\n' +
2059
+ ' baseURL: \'http://localhost:3264/api\',\n' +
2060
+ ' apiKey: \'any-string\'\n' +
2061
+ '});\n\n' +
2062
+ 'const response = await client.chat.completions.create({\n' +
2063
+ ' model: \'qwen3.5-plus\',\n' +
2064
+ ' messages: [{ role: \'user\', content: \'Привет!\' }]\n' +
2065
+ '});\n' +
2066
+ '</pre>\n\n' +
2067
+ '📖 API Docs: http://localhost:3264/api\n' +
2068
+ '📚 GitHub: https://github.com/EndyKaufman/FreeQwenApi';
2069
+
2044
2070
  await sendMessage(chatId, connectText);
2045
2071
  }
2046
2072
 
2047
2073
  async function sendAboutMessage(chatId) {
2048
2074
  const aboutText =
2049
- `📚 <b>О проекте FreeQwenApi</b>\n\n` +
2050
- `<b>🌐 Оригинальный проект:</b>\n` +
2051
- `https://github.com/y13sint/FreeQwenApi\n\n` +
2052
- `<b>🔧 Мой форк:</b>\n` +
2053
- `https://github.com/EndyKaufman/FreeQwenApi\n\n` +
2054
- `<b>⭐ Ключевые отличия форка:</b>\n\n` +
2055
- `✅ <b>Telegram Bot интеграция</b>\n` +
2056
- ` - Управление сервисом через Telegram\n` +
2057
- ` - Загрузка сессий архивами (.zip/.7z)\n` +
2058
- ` - LLM чат с AI ассистентом\n` +
2059
- ` - Мониторинг статуса в реальном времени\n\n` +
2060
- `✅ <b>Прокси поддержка для Telegram</b>\n` +
2061
- ` - HTTP/HTTPS/SOCKS прокси\n` +
2062
- ` - Безопасное логирование (без credentials)\n\n` +
2063
- `✅ <b>Автоматическая распаковка архивов</b>\n` +
2064
- ` - Backup перед обновлением\n` +
2065
- ` - Error-tolerant extraction\n` +
2066
- ` - Health check при запуске\n\n` +
2067
- `✅ <b>Улучшенная документация</b>\n` +
2068
- ` - Подробные README на русском\n` +
2069
- ` - Telegram bot guides\n` +
2070
- `✅ <b>Production-ready features</b>\n` +
2071
- ` - Docker оптимизация\n` +
2072
- ` - Graceful restarts\n` +
2073
- ` - System health monitoring\n\n` +
2074
- `<b>📊 Общие возможности:</b>\n` +
2075
- `• 25+ моделей Qwen (включая Qwen 3.5)\n` +
2076
- `• OpenAI-совместимый API\n` +
2077
- `• Генерация изображений\n` +
2078
- `• Загрузка файлов\n` +
2079
- `• Streaming ответов (SSE)\n` +
2080
- `• Мультиаккаунт ротация\n` +
2081
- `• Бесплатный доступ к Qwen AI\n\n` +
2082
- `💡 <i>Оба проекта используют MIT лицензию</i>`;
2083
-
2075
+ '📚 <b>О проекте FreeQwenApi</b>\n\n' +
2076
+ '<b>🌐 Оригинальный проект:</b>\n' +
2077
+ 'https://github.com/y13sint/FreeQwenApi\n\n' +
2078
+ '<b>🔧 Мой форк:</b>\n' +
2079
+ 'https://github.com/EndyKaufman/FreeQwenApi\n\n' +
2080
+ '<b>⭐ Ключевые отличия форка:</b>\n\n' +
2081
+ '✅ <b>Telegram Bot интеграция</b>\n' +
2082
+ ' - Управление сервисом через Telegram\n' +
2083
+ ' - Загрузка сессий архивами (.zip/.7z)\n' +
2084
+ ' - LLM чат с AI ассистентом\n' +
2085
+ ' - Мониторинг статуса в реальном времени\n\n' +
2086
+ '✅ <b>Прокси поддержка для Telegram</b>\n' +
2087
+ ' - HTTP/HTTPS/SOCKS прокси\n' +
2088
+ ' - Безопасное логирование (без credentials)\n\n' +
2089
+ '✅ <b>Автоматическая распаковка архивов</b>\n' +
2090
+ ' - Backup перед обновлением\n' +
2091
+ ' - Error-tolerant extraction\n' +
2092
+ ' - Health check при запуске\n\n' +
2093
+ '✅ <b>Улучшенная документация</b>\n' +
2094
+ ' - Подробные README на русском\n' +
2095
+ ' - Telegram bot guides\n' +
2096
+ '✅ <b>Production-ready features</b>\n' +
2097
+ ' - Docker оптимизация\n' +
2098
+ ' - Graceful restarts\n' +
2099
+ ' - System health monitoring\n\n' +
2100
+ '<b>📊 Общие возможности:</b>\n' +
2101
+ '• 25+ моделей Qwen (включая Qwen 3.5)\n' +
2102
+ '• OpenAI-совместимый API\n' +
2103
+ '• Генерация изображений\n' +
2104
+ '• Загрузка файлов\n' +
2105
+ '• Streaming ответов (SSE)\n' +
2106
+ '• Мультиаккаунт ротация\n' +
2107
+ '• Бесплатный доступ к Qwen AI\n\n' +
2108
+ '💡 <i>Оба проекта используют MIT лицензию</i>';
2109
+
2084
2110
  await sendMessage(chatId, aboutText);
2085
2111
  }
2086
2112
 
@@ -2089,81 +2115,81 @@ async function sendAboutMessage(chatId) {
2089
2115
  */
2090
2116
  async function sendArchiveInstructions(chatId) {
2091
2117
  const archiveText =
2092
- `📦 <b>Создание архива сессии для Docker</b>\n\n` +
2093
- `Эта инструкция поможет создать архив с авторизацией\n` +
2094
- `для последующей загрузки в Telegram бота.\n\n` +
2095
- `<b>🔹 Шаг 1: Установка Node.js</b>\n\n` +
2096
- `<b>Windows:</b>\n` +
2097
- `• Скачайте с <code>nodejs.org</code>\n` +
2098
- `• Установите (галочка "Add to PATH")\n\n` +
2099
- `<b>macOS:</b>\n` +
2100
- `• <code>brew install node</code>\n` +
2101
- `• Или скачайте с <code>nodejs.org</code>\n\n` +
2102
- `<b>Linux (Ubuntu/Debian):</b>\n` +
2103
- `• <code>curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -</code>\n` +
2104
- `• <code>sudo apt install -y nodejs</code>\n\n` +
2105
- `<b>🔹 Шаг 2: Скачивание проекта</b>\n\n` +
2106
- `<b>Способ A: Git (если установлен):</b>\n` +
2107
- `<pre>\n` +
2108
- `git clone https://github.com/EndyKaufman/FreeQwenApi\n` +
2109
- `cd FreeQwenApi\n` +
2110
- `npm install\n` +
2111
- `</pre>\n\n` +
2112
- `<b>Способ B: Без Git (ZIP архив):</b>\n` +
2113
- `1. Откройте: https://github.com/EndyKaufman/FreeQwenApi\n` +
2114
- `2. Нажмите зелёную кнопку <b>"&lt;&gt; Code"</b>\n` +
2115
- `3. Выберите <b>"Download ZIP"</b>\n` +
2116
- `4. Распакуйте архив\n` +
2117
- `5. Откройте терминал в папке проекта\n` +
2118
- `6. <code>npm install</code>\n\n` +
2119
- `💡 <i>Этот способ не требует установки Git!</i>\n\n` +
2120
- `<b>🔹 Шаг 3: Создание архива сессии</b>\n\n` +
2121
- `<pre>\n` +
2122
- `npm run create-session-archive\n` +
2123
- `</pre>\n\n` +
2124
- `<b>Что произойдет:</b>\n` +
2125
- `1. Откроется браузер\n` +
2126
- `2. Войдите в Qwen (GitHub/Google/email)\n` +
2127
- `3. Нажмите ENTER в консоли\n` +
2128
- `4. Сессия сохранится\n` +
2129
- `5. Создется ZIP архив\n\n` +
2130
- `<b>🔹 Шаг 4: Отправка в Telegram бота</b>\n\n` +
2131
- `1. Откройте нашего бота\n` +
2132
- `2. Нажмите 📎 (скрепка)\n` +
2133
- `3. Выберите <b>"Файл"</b> (НЕ "Фото"!)\n` +
2134
- `4. Выберите <code>session_backup_*.zip</code>\n` +
2135
- `5. Отправьте\n` +
2136
- `6. Дождитесь: <code>✅ Архив распакован</code>\n\n` +
2137
- `<b>🔹 Альтернатива: Ручной способ</b>\n\n` +
2138
- `Если <code>npm run</code> не работает:\n\n` +
2139
- `<b>Windows PowerShell:</b>\n` +
2140
- `<pre>\n` +
2141
- `node scripts/createSessionArchive.js\n` +
2142
- `</pre>\n\n` +
2143
- `<b>Linux/macOS:</b>\n` +
2144
- `<pre>\n` +
2145
- `node scripts/createSessionArchive.js\n` +
2146
- `</pre>\n\n` +
2147
- `<b>🔹 Структура архива:</b>\n\n` +
2148
- `<pre>\n` +
2149
- `session/\n` +
2150
- `├── accounts/\n` +
2151
- `│ ├── acc_123456/\n` +
2152
- `│ │ ├── token.txt\n` +
2153
- `│ │ └── cookies.json\n` +
2154
- `│ └── acc_789012/\n` +
2155
- `│ ├── token.txt\n` +
2156
- `│ └── cookies.json\n` +
2157
- `└── tokens.json\n` +
2158
- `</pre>\n\n` +
2159
- `<b>⚠️ Важно:</b>\n` +
2160
- `• <code>cookies.json</code> обязателен!\n` +
2161
- `• Без cookies сессия не продлится\n` +
2162
- `• Архив должен содержать папку <code>session/</code>\n\n` +
2163
- `<b>🆘 Проблемы?</b>\n` +
2164
- `• GitHub: https://github.com/EndyKaufman/FreeQwenApi\n` +
2165
- `• Используйте /help для списка команд`;
2166
-
2118
+ '📦 <b>Создание архива сессии для Docker</b>\n\n' +
2119
+ 'Эта инструкция поможет создать архив с авторизацией\n' +
2120
+ 'для последующей загрузки в Telegram бота.\n\n' +
2121
+ '<b>🔹 Шаг 1: Установка Node.js</b>\n\n' +
2122
+ '<b>Windows:</b>\n' +
2123
+ '• Скачайте с <code>nodejs.org</code>\n' +
2124
+ '• Установите (галочка "Add to PATH")\n\n' +
2125
+ '<b>macOS:</b>\n' +
2126
+ '• <code>brew install node</code>\n' +
2127
+ '• Или скачайте с <code>nodejs.org</code>\n\n' +
2128
+ '<b>Linux (Ubuntu/Debian):</b>\n' +
2129
+ '• <code>curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -</code>\n' +
2130
+ '• <code>sudo apt install -y nodejs</code>\n\n' +
2131
+ '<b>🔹 Шаг 2: Скачивание проекта</b>\n\n' +
2132
+ '<b>Способ A: Git (если установлен):</b>\n' +
2133
+ '<pre>\n' +
2134
+ 'git clone https://github.com/EndyKaufman/FreeQwenApi\n' +
2135
+ 'cd FreeQwenApi\n' +
2136
+ 'npm install\n' +
2137
+ '</pre>\n\n' +
2138
+ '<b>Способ B: Без Git (ZIP архив):</b>\n' +
2139
+ '1. Откройте: https://github.com/EndyKaufman/FreeQwenApi\n' +
2140
+ '2. Нажмите зелёную кнопку <b>"&lt;&gt; Code"</b>\n' +
2141
+ '3. Выберите <b>"Download ZIP"</b>\n' +
2142
+ '4. Распакуйте архив\n' +
2143
+ '5. Откройте терминал в папке проекта\n' +
2144
+ '6. <code>npm install</code>\n\n' +
2145
+ '💡 <i>Этот способ не требует установки Git!</i>\n\n' +
2146
+ '<b>🔹 Шаг 3: Создание архива сессии</b>\n\n' +
2147
+ '<pre>\n' +
2148
+ 'npm run create-session-archive\n' +
2149
+ '</pre>\n\n' +
2150
+ '<b>Что произойдет:</b>\n' +
2151
+ '1. Откроется браузер\n' +
2152
+ '2. Войдите в Qwen (GitHub/Google/email)\n' +
2153
+ '3. Нажмите ENTER в консоли\n' +
2154
+ '4. Сессия сохранится\n' +
2155
+ '5. Создется ZIP архив\n\n' +
2156
+ '<b>🔹 Шаг 4: Отправка в Telegram бота</b>\n\n' +
2157
+ '1. Откройте нашего бота\n' +
2158
+ '2. Нажмите 📎 (скрепка)\n' +
2159
+ '3. Выберите <b>"Файл"</b> (НЕ "Фото"!)\n' +
2160
+ '4. Выберите <code>session_backup_*.zip</code>\n' +
2161
+ '5. Отправьте\n' +
2162
+ '6. Дождитесь: <code>✅ Архив распакован</code>\n\n' +
2163
+ '<b>🔹 Альтернатива: Ручной способ</b>\n\n' +
2164
+ 'Если <code>npm run</code> не работает:\n\n' +
2165
+ '<b>Windows PowerShell:</b>\n' +
2166
+ '<pre>\n' +
2167
+ 'node scripts/createSessionArchive.js\n' +
2168
+ '</pre>\n\n' +
2169
+ '<b>Linux/macOS:</b>\n' +
2170
+ '<pre>\n' +
2171
+ 'node scripts/createSessionArchive.js\n' +
2172
+ '</pre>\n\n' +
2173
+ '<b>🔹 Структура архива:</b>\n\n' +
2174
+ '<pre>\n' +
2175
+ 'session/\n' +
2176
+ '├── accounts/\n' +
2177
+ '│ ├── acc_123456/\n' +
2178
+ '│ │ ├── token.txt\n' +
2179
+ '│ │ └── cookies.json\n' +
2180
+ '│ └── acc_789012/\n' +
2181
+ '│ ├── token.txt\n' +
2182
+ '│ └── cookies.json\n' +
2183
+ '└── tokens.json\n' +
2184
+ '</pre>\n\n' +
2185
+ '<b>⚠️ Важно:</b>\n' +
2186
+ '• <code>cookies.json</code> обязателен!\n' +
2187
+ '• Без cookies сессия не продлится\n' +
2188
+ '• Архив должен содержать папку <code>session/</code>\n\n' +
2189
+ '<b>🆘 Проблемы?</b>\n' +
2190
+ '• GitHub: https://github.com/EndyKaufman/FreeQwenApi\n' +
2191
+ '• Используйте /help для списка команд';
2192
+
2167
2193
  await sendMessage(chatId, archiveText);
2168
2194
  }
2169
2195
 
@@ -2172,92 +2198,92 @@ async function sendStatusMessage(chatId, isScheduled = false) {
2172
2198
  // Получаем статус бота
2173
2199
  const telegramToken = process.env.TELEGRAM_BOT_TOKEN;
2174
2200
  const botStarted = !!telegramToken;
2175
-
2201
+
2176
2202
  // Проверяем все подсистемы (не отправляем автоматически, так как sendStatusMessage отправит сам)
2177
2203
  const checks = await checkAllSubsystems(botStarted, false);
2178
-
2204
+
2179
2205
  // Формируем сообщение с统一的格式
2180
2206
  const reportLines = [];
2181
2207
 
2182
2208
  // Заголовок
2183
2209
  if (isScheduled) {
2184
2210
  const now = new Date();
2185
- const timeStr = now.toLocaleString('ru-RU', {
2186
- day: '2-digit',
2187
- month: '2-digit',
2211
+ const timeStr = now.toLocaleString('ru-RU', {
2212
+ day: '2-digit',
2213
+ month: '2-digit',
2188
2214
  year: 'numeric',
2189
- hour: '2-digit',
2190
- minute: '2-digit'
2215
+ hour: '2-digit',
2216
+ minute: '2-digit'
2191
2217
  });
2192
2218
  reportLines.push(`⏰ <b>Плановая проверка</b> (${timeStr})\n`);
2193
2219
  } else {
2194
- reportLines.push(`🚀 <b>Сервис запущен!</b>\n`);
2220
+ reportLines.push('🚀 <b>Сервис запущен!</b>\n');
2195
2221
  }
2196
2222
 
2197
2223
  // Группа 1: Основные компоненты
2198
- reportLines.push(`<b>🔑 Основные компоненты:</b>`);
2199
- const mainComponents = checks.filter(c =>
2200
- c.name.includes('Session') || c.name.includes('Токены') || c.name.includes('Telegram') ||
2224
+ reportLines.push('<b>🔑 Основные компоненты:</b>');
2225
+ const mainComponents = checks.filter((c) =>
2226
+ c.name.includes('Session') || c.name.includes('Токены') || c.name.includes('Telegram') ||
2201
2227
  c.name.includes('Токен ') || c.name.includes('AI') || c.name.includes('Ответ')
2202
2228
  );
2203
- mainComponents.forEach(check => {
2229
+ mainComponents.forEach((check) => {
2204
2230
  reportLines.push(`${check.name}: ${check.details}`);
2205
2231
  });
2206
2232
 
2207
2233
  reportLines.push('');
2208
2234
 
2209
2235
  // Группа 2: Инфраструктура
2210
- reportLines.push(`<b>🏗️ Инфраструктура:</b>`);
2211
- const infrastructure = checks.filter(c =>
2236
+ reportLines.push('<b>🏗️ Инфраструктура:</b>');
2237
+ const infrastructure = checks.filter((c) =>
2212
2238
  c.name.includes('Прокси') || c.name.includes('Uploads') || c.name.includes('Логирование')
2213
2239
  );
2214
- infrastructure.forEach(check => {
2240
+ infrastructure.forEach((check) => {
2215
2241
  reportLines.push(`${check.name}: ${check.details}`);
2216
2242
  });
2217
2243
 
2218
2244
  reportLines.push('');
2219
2245
 
2220
2246
  // Группа 3: Инструменты
2221
- reportLines.push(`<b>🔧 Инструменты:</b>`);
2222
- const tools = checks.filter(c =>
2247
+ reportLines.push('<b>🔧 Инструменты:</b>');
2248
+ const tools = checks.filter((c) =>
2223
2249
  c.name.includes('p7zip')
2224
2250
  );
2225
- tools.forEach(check => {
2251
+ tools.forEach((check) => {
2226
2252
  reportLines.push(`${check.name}: ${check.details}`);
2227
2253
  });
2228
2254
 
2229
2255
  reportLines.push('');
2230
- reportLines.push(`━━━━━━━━━━━━━━━━━━━━━━━━━`);
2256
+ reportLines.push('━━━━━━━━━━━━━━━━━━━━━━━━━');
2231
2257
 
2232
2258
  // Итоговый статус
2233
2259
  const tokens = loadTokens();
2234
2260
  const hasTokens = tokens.length > 0;
2235
- const allOk = checks.every(c => c.status);
2236
-
2261
+ const allOk = checks.every((c) => c.status);
2262
+
2237
2263
  if (hasTokens && allOk) {
2238
- reportLines.push(`✅ <b>Все системы работают</b>`);
2264
+ reportLines.push('✅ <b>Все системы работают</b>');
2239
2265
  } else if (!hasTokens) {
2240
- reportLines.push(`⚠️ <b>Режим ожидания архива</b>`);
2241
- reportLines.push(`📦 Отправьте архив с сессиями`);
2266
+ reportLines.push('⚠️ <b>Режим ожидания архива</b>');
2267
+ reportLines.push('📦 Отправьте архив с сессиями');
2242
2268
  } else {
2243
- reportLines.push(`⚠️ <b>Есть проблемы</b>`);
2269
+ reportLines.push('⚠️ <b>Есть проблемы</b>');
2244
2270
  }
2245
2271
 
2246
2272
  // Ссылки
2247
2273
  reportLines.push(`\n🌐 API: http://localhost:${process.env.PORT || 3264}`);
2248
2274
  reportLines.push(`📖 Docs: http://localhost:${process.env.PORT || 3264}/api`);
2249
-
2275
+
2250
2276
  // Репозиторий
2251
- reportLines.push(`\n📚 <b>Репозиторий:</b>`);
2252
- reportLines.push(`🔗 GitHub: https://github.com/EndyKaufman/FreeQwenApi`);
2253
- reportLines.push(`⭐ Оригинал: https://github.com/y1n7sint/FreeQwenApi`);
2254
- reportLines.push(`🐳 Docker: https://hub.docker.com/r/endykaufman/qwen-api-proxy`);
2255
-
2277
+ reportLines.push('\n📚 <b>Репозиторий:</b>');
2278
+ reportLines.push('🔗 GitHub: https://github.com/EndyKaufman/FreeQwenApi');
2279
+ reportLines.push('⭐ Оригинал: https://github.com/y1n7sint/FreeQwenApi');
2280
+ reportLines.push('🐳 Docker: https://hub.docker.com/r/endykaufman/qwen-api-proxy');
2281
+
2256
2282
  // Справка
2257
- reportLines.push(`\n💡 <b>Справка:</b>`);
2258
- reportLines.push(`📝 Используйте /help для списка команд`);
2259
- reportLines.push(`🔍 Используйте /status для проверки состояния`);
2260
- reportLines.push(`🤖 Используйте /chat для включения LLM режима`);
2283
+ reportLines.push('\n💡 <b>Справка:</b>');
2284
+ reportLines.push('📝 Используйте /help для списка команд');
2285
+ reportLines.push('🔍 Используйте /status для проверки состояния');
2286
+ reportLines.push('🤖 Используйте /chat для включения LLM режима');
2261
2287
 
2262
2288
  const report = reportLines.join('\n');
2263
2289
  await sendMessage(chatId, report);
@@ -2272,7 +2298,7 @@ async function sendStatusMessage(chatId, isScheduled = false) {
2272
2298
  */
2273
2299
  async function handleRestart(chatId) {
2274
2300
  await sendMessage(chatId, '🔄 Перезапуск сервиса...');
2275
- await new Promise(resolve => setTimeout(resolve, 2000));
2301
+ await new Promise((resolve) => setTimeout(resolve, 2000));
2276
2302
  await gracefulRestart(chatId);
2277
2303
  }
2278
2304
 
@@ -2287,11 +2313,11 @@ async function handleExtendSession(chatId) {
2287
2313
  const { loadSession, saveAuthToken } = await import('../browser/session.js');
2288
2314
  const { loadTokens, saveTokens } = await import('../api/tokenManager.js');
2289
2315
  const { CHAT_PAGE_URL } = await import('../config.js');
2290
-
2316
+
2291
2317
  const tokens = loadTokens();
2292
-
2318
+
2293
2319
  if (tokens.length === 0) {
2294
- await sendMessage(chatId,
2320
+ await sendMessage(chatId,
2295
2321
  '⚠️ <b>Нет аккаунтов</b>\n\n' +
2296
2322
  'Сначала создайте сессию:\n' +
2297
2323
  '1. Запустите <code>npm run create-session-archive</code>\n' +
@@ -2302,21 +2328,21 @@ async function handleExtendSession(chatId) {
2302
2328
 
2303
2329
  // Фильтруем только действительные токены для продления
2304
2330
  const now = Date.now();
2305
- const validTokens = tokens.filter(t => {
2306
- if (t.invalid) return false;
2307
- if (t.resetAt && new Date(t.resetAt).getTime() > now) return false;
2308
- if (t.expiryTime && t.expiryTime <= now) return false;
2331
+ const validTokens = tokens.filter((t) => {
2332
+ if (t.invalid) { return false; }
2333
+ if (t.resetAt && new Date(t.resetAt).getTime() > now) { return false; }
2334
+ if (t.expiryTime && t.expiryTime <= now) { return false; }
2309
2335
  // Проверяем наличие cookies.json
2310
2336
  const cookiesPath = path.join(process.cwd(), SESSION_DIR, 'accounts', t.id, 'cookies.json');
2311
- if (!fs.existsSync(cookiesPath)) return false;
2337
+ if (!fs.existsSync(cookiesPath)) { return false; }
2312
2338
  return true;
2313
2339
  });
2314
2340
 
2315
2341
  const expiredCount = tokens.length - validTokens.length;
2316
2342
 
2317
2343
  if (validTokens.length === 0) {
2318
- await sendMessage(chatId,
2319
- `⚠️ <b>Нет действительных токенов</b>\n\n` +
2344
+ await sendMessage(chatId,
2345
+ '⚠️ <b>Нет действительных токенов</b>\n\n' +
2320
2346
  `Все ${tokens.length} токенов истекли.\n\n` +
2321
2347
  'Создайте новые сессии:\n' +
2322
2348
  '1. Запустите <code>npm run create-session-archive</code>\n' +
@@ -2325,15 +2351,15 @@ async function handleExtendSession(chatId) {
2325
2351
  return;
2326
2352
  }
2327
2353
 
2328
- let startMessage = `🔄 <b>Продление сессий...</b>\n\n` +
2354
+ let startMessage = '🔄 <b>Продление сессий...</b>\n\n' +
2329
2355
  `📊 Найдено аккаунтов: ${validTokens.length}`;
2330
-
2356
+
2331
2357
  if (expiredCount > 0) {
2332
2358
  startMessage += ` (пропущено ${expiredCount} истекших)`;
2333
2359
  }
2334
-
2335
- startMessage += `\n⏳ Это может занять несколько минут...\n` +
2336
- `🕐 Примерное время: ~2-4 минуты на аккаунт`;
2360
+
2361
+ startMessage += '\n⏳ Это может занять несколько минут...\n' +
2362
+ '🕐 Примерное время: ~2-4 минуты на аккаунт';
2337
2363
 
2338
2364
  await sendMessage(chatId, startMessage);
2339
2365
 
@@ -2345,7 +2371,7 @@ async function handleExtendSession(chatId) {
2345
2371
  try {
2346
2372
  // Показываем прогресс
2347
2373
  const currentNum = results.length + 1;
2348
- await sendMessage(chatId,
2374
+ await sendMessage(chatId,
2349
2375
  `🔄 Обрабатываю аккаунт ${currentNum}/${tokens.length}...\n` +
2350
2376
  `👤 ${token.id}`
2351
2377
  );
@@ -2359,7 +2385,7 @@ async function handleExtendSession(chatId) {
2359
2385
 
2360
2386
  // Загружаем cookies для аккаунта
2361
2387
  const cookiesPath = path.join(process.cwd(), SESSION_DIR, 'accounts', token.id, 'cookies.json');
2362
-
2388
+
2363
2389
  if (!fs.existsSync(cookiesPath)) {
2364
2390
  results.push(`❌ ${token.id} - нет cookies`);
2365
2391
  failCount++;
@@ -2372,26 +2398,26 @@ async function handleExtendSession(chatId) {
2372
2398
 
2373
2399
  // Открываем браузер в headless режиме
2374
2400
  const browserOk = await initBrowser(false, true);
2375
-
2401
+
2376
2402
  if (!browserOk) {
2377
2403
  throw new Error('Не удалось открыть браузер');
2378
2404
  }
2379
2405
 
2380
2406
  const ctx = getBrowserContext();
2381
-
2407
+
2382
2408
  // Загружаем cookies
2383
2409
  if (ctx && typeof ctx.setCookie === 'function') {
2384
2410
  await ctx.setCookie(...cookies);
2385
2411
  }
2386
2412
 
2387
2413
  // Переходим на Qwen для обновления сессии (3 минуты таймаут)
2388
- await ctx.goto(CHAT_PAGE_URL, {
2389
- waitUntil: 'domcontentloaded',
2414
+ await ctx.goto(CHAT_PAGE_URL, {
2415
+ waitUntil: 'domcontentloaded',
2390
2416
  timeout: 180000 // 3 минуты
2391
2417
  });
2392
2418
 
2393
2419
  // Ждем загрузки страницы (1 минута для полной загрузки)
2394
- await new Promise(resolve => setTimeout(resolve, 60000));
2420
+ await new Promise((resolve) => setTimeout(resolve, 60000));
2395
2421
 
2396
2422
  // Извлекаем новый токен
2397
2423
  const newToken = await extractAuthToken(ctx, true);
@@ -2409,7 +2435,7 @@ async function handleExtendSession(chatId) {
2409
2435
  saveAuthToken(newToken);
2410
2436
 
2411
2437
  // Обновляем tokens.json
2412
- const tokenIndex = tokens.findIndex(t => t.id === token.id);
2438
+ const tokenIndex = tokens.findIndex((t) => t.id === token.id);
2413
2439
  if (tokenIndex !== -1) {
2414
2440
  tokens[tokenIndex].token = newToken;
2415
2441
  tokens[tokenIndex].resetAt = null;
@@ -2430,14 +2456,14 @@ async function handleExtendSession(chatId) {
2430
2456
 
2431
2457
  // Небольшая задержка между аккаунтами
2432
2458
  if (successCount < tokens.length) {
2433
- await new Promise(resolve => setTimeout(resolve, 2000));
2459
+ await new Promise((resolve) => setTimeout(resolve, 2000));
2434
2460
  }
2435
2461
 
2436
2462
  } catch (error) {
2437
2463
  logError(`Ошибка продления ${token.id}`, error);
2438
2464
  results.push(`❌ ${token.id} - ${error.message}`);
2439
2465
  failCount++;
2440
-
2466
+
2441
2467
  // Убеждаемся что браузер закрыт
2442
2468
  try {
2443
2469
  await shutdownBrowser();
@@ -2448,31 +2474,31 @@ async function handleExtendSession(chatId) {
2448
2474
  }
2449
2475
 
2450
2476
  // Формируем отчет
2451
- let report = `📋 <b>Результат продления сессий</b>\n\n`;
2477
+ let report = '📋 <b>Результат продления сессий</b>\n\n';
2452
2478
  report += `✅ Успешно: ${successCount}\n`;
2453
2479
  report += `❌ Ошибки: ${failCount}\n\n`;
2454
- report += `<b>Детали:</b>\n`;
2480
+ report += '<b>Детали:</b>\n';
2455
2481
  report += results.join('\n');
2456
2482
 
2457
2483
  if (successCount > 0) {
2458
- report += `\n\n🎉 Сессии продлены!`;
2484
+ report += '\n\n🎉 Сессии продлены!';
2459
2485
  }
2460
2486
 
2461
2487
  if (failCount > 0 && successCount === 0) {
2462
- report += `\n\n⚠️ Все сессии не удалось продлить.\n`;
2463
-
2488
+ report += '\n\n⚠️ Все сессии не удалось продлить.\n';
2489
+
2464
2490
  // Check if the issue is missing cookies
2465
- const missingCookiesCount = results.filter(r => r.includes('нет cookies')).length;
2466
-
2491
+ const missingCookiesCount = results.filter((r) => r.includes('нет cookies')).length;
2492
+
2467
2493
  if (missingCookiesCount > 0) {
2468
- report += `\n📦 <b>Причина: отсутствуют cookies.json</b>\n`;
2469
- report += `\nДля создания новой сессии с cookies:\n`;
2470
- report += `1. Запустите: <code>npm run create-session-archive</code>\n`;
2471
- report += `2. Войдите в систему в браузере\n`;
2472
- report += `3. Бот автоматически сохранит cookies и токен\n`;
2473
- report += `\n💡 Или отправьте архив с сессиями через бота`;
2494
+ report += '\n📦 <b>Причина: отсутствуют cookies.json</b>\n';
2495
+ report += '\nДля создания новой сессии с cookies:\n';
2496
+ report += '1. Запустите: <code>npm run create-session-archive</code>\n';
2497
+ report += '2. Войдите в систему в браузере\n';
2498
+ report += '3. Бот автоматически сохранит cookies и токен\n';
2499
+ report += '\n💡 Или отправьте архив с сессиями через бота';
2474
2500
  } else {
2475
- report += `\nВыполните: <code>npm run create-session-archive</code>`;
2501
+ report += '\nВыполните: <code>npm run create-session-archive</code>';
2476
2502
  }
2477
2503
  }
2478
2504
 
@@ -2480,12 +2506,12 @@ async function handleExtendSession(chatId) {
2480
2506
 
2481
2507
  } catch (error) {
2482
2508
  logError('Ошибка при продлении сессий', error);
2483
- await sendMessage(chatId,
2484
- `❌ <b>Ошибка продления сессий</b>\n\n` +
2509
+ await sendMessage(chatId,
2510
+ '❌ <b>Ошибка продления сессий</b>\n\n' +
2485
2511
  `Ошибка: ${error.message}\n\n` +
2486
- `Попробуйте:\n` +
2487
- `1. <code>npm run create-session-archive</code>\n` +
2488
- `2. Или отправьте архив с сессиями`
2512
+ 'Попробуйте:\n' +
2513
+ '1. <code>npm run create-session-archive</code>\n' +
2514
+ '2. Или отправьте архив с сессиями'
2489
2515
  );
2490
2516
  }
2491
2517
  }
@@ -2501,7 +2527,7 @@ async function sendScheduledStatusToAdmins() {
2501
2527
  }
2502
2528
 
2503
2529
  const adminUserIds = TELEGRAM_USER_IDS;
2504
-
2530
+
2505
2531
  if (adminUserIds.length === 0) {
2506
2532
  logInfo('Нет админов для отправки плановой проверки');
2507
2533
  return;
@@ -2527,9 +2553,9 @@ async function sendScheduledStatusToAdmins() {
2527
2553
  */
2528
2554
  export function startPeriodicHealthCheck() {
2529
2555
  const FOUR_HOURS = 4 * 60 * 60 * 1000; // 4 часа в миллисекундах
2530
-
2531
- logInfo(`Запуск периодической проверки здоровья каждые 4 часа`);
2532
-
2556
+
2557
+ logInfo('Запуск периодической проверки здоровья каждые 4 часа');
2558
+
2533
2559
  setInterval(async () => {
2534
2560
  logInfo('Выполняется плановая проверка здоровья...');
2535
2561
  await sendScheduledStatusToAdmins();
@@ -2580,6 +2606,7 @@ export async function notifyAllUsers(message) {
2580
2606
  * Обработчик LLM чата - отправляет сообщение в Qwen API
2581
2607
  */
2582
2608
  async function handleLLMChat(chatId, userMessage) {
2609
+ let context;
2583
2610
  try {
2584
2611
  // Показываем индикатор набора текста
2585
2612
  await sendChatAction(chatId, 'typing');
@@ -2588,7 +2615,8 @@ async function handleLLMChat(chatId, userMessage) {
2588
2615
  if (!chatContexts.has(chatId)) {
2589
2616
  chatContexts.set(chatId, []);
2590
2617
  }
2591
- const context = chatContexts.get(chatId);
2618
+
2619
+ context = chatContexts.get(chatId);
2592
2620
 
2593
2621
  // Добавляем сообщение пользователя в контекст
2594
2622
  context.push({ role: 'user', content: userMessage });
@@ -2624,17 +2652,17 @@ async function handleLLMChat(chatId, userMessage) {
2624
2652
  method: 'POST',
2625
2653
  headers: {
2626
2654
  'Content-Type': 'application/json',
2627
- 'x-chat-id': `telegram-${chatId}` // Уникальный ID для каждого Telegram чата
2655
+ 'x-chat-id': `telegram-${chatId}` // Уникальный ID для каждого Telegram чата
2628
2656
  },
2629
2657
  body: JSON.stringify(requestBody)
2630
2658
  });
2631
2659
 
2632
2660
  if (!response.ok) {
2633
2661
  const errorText = await response.text();
2634
-
2662
+
2635
2663
  // Логируем полную ошибку
2636
2664
  logError(`❌ LLM Chat: Qwen API error ${response.status}`, errorText);
2637
-
2665
+
2638
2666
  // Пытаемся распарсить JSON для лучшего форматирования
2639
2667
  let errorJson;
2640
2668
  try {
@@ -2642,16 +2670,16 @@ async function handleLLMChat(chatId, userMessage) {
2642
2670
  } catch {
2643
2671
  errorJson = { error: errorText };
2644
2672
  }
2645
-
2673
+
2646
2674
  // Формируем сообщение об ошибке с полным JSON
2647
2675
  const escapedJson = escapeHtmlForCode(JSON.stringify(errorJson, null, 2));
2648
- const errorMessage =
2649
- `❌ <b>Ошибка Qwen API</b>\n\n` +
2676
+ const errorMessage =
2677
+ '❌ <b>Ошибка Qwen API</b>\n\n' +
2650
2678
  `Статус: <code>${response.status}</code>\n\n` +
2651
- `<b>Полный ответ:</b>\n` +
2679
+ '<b>Полный ответ:</b>\n' +
2652
2680
  `<pre>${escapedJson}</pre>\n\n` +
2653
- `💡 Попробуйте еще раз или используйте /clear`;
2654
-
2681
+ '💡 Попробуйте еще раз или используйте /clear';
2682
+
2655
2683
  // Отправляем ошибку (разбиваем если длинная)
2656
2684
  if (errorMessage.length > 4000) {
2657
2685
  const chunks = splitMessage(errorMessage, 4000);
@@ -2661,12 +2689,12 @@ async function handleLLMChat(chatId, userMessage) {
2661
2689
  } else {
2662
2690
  await sendMessage(chatId, errorMessage);
2663
2691
  }
2664
-
2692
+
2665
2693
  // Удаляем последнее сообщение пользователя из контекста (оно не было обработано)
2666
2694
  if (context.length > 0) {
2667
2695
  context.pop();
2668
2696
  }
2669
-
2697
+
2670
2698
  return; // Выходим, не выбрасывая ошибку
2671
2699
  }
2672
2700
 
@@ -2693,17 +2721,17 @@ async function handleLLMChat(chatId, userMessage) {
2693
2721
 
2694
2722
  } catch (error) {
2695
2723
  logError('❌ LLM Chat: Ошибка', error);
2696
-
2724
+
2697
2725
  // Формируем сообщение об ошибке с деталями
2698
2726
  const escapedStack = escapeHtmlForCode(error.stack || 'Stack trace unavailable');
2699
- const errorMessage =
2700
- `❌ <b>Ошибка при обработке запроса</b>\n\n` +
2727
+ const errorMessage =
2728
+ '❌ <b>Ошибка при обработке запроса</b>\n\n' +
2701
2729
  `<b>Тип:</b> ${error.name || 'Unknown'}\n` +
2702
2730
  `<b>Сообщение:</b> ${escapeHtml(error.message)}\n\n` +
2703
- `<b>Стек:</b>\n` +
2731
+ '<b>Стек:</b>\n' +
2704
2732
  `<pre>${escapedStack}</pre>\n\n` +
2705
- `💡 Попробуйте еще раз или используйте /clear для очистки контекста.`;
2706
-
2733
+ '💡 Попробуйте еще раз или используйте /clear для очистки контекста.';
2734
+
2707
2735
  // Отправляем ошибку (разбиваем если длинная)
2708
2736
  if (errorMessage.length > 4000) {
2709
2737
  const chunks = splitMessage(errorMessage, 4000);
@@ -2713,7 +2741,7 @@ async function handleLLMChat(chatId, userMessage) {
2713
2741
  } else {
2714
2742
  await sendMessage(chatId, errorMessage);
2715
2743
  }
2716
-
2744
+
2717
2745
  // Удаляем последнее сообщение пользователя из контекста
2718
2746
  if (context && context.length > 0) {
2719
2747
  context.pop();
@@ -2728,18 +2756,18 @@ async function toggleLLMChat(chatId) {
2728
2756
  // Проверяем есть ли аккаунты
2729
2757
  if (!hasAccounts()) {
2730
2758
  await sendMessage(chatId,
2731
- `❌ <b>LLM чат недоступен</b>\n\n` +
2732
- `🔒 Для работы AI ассистента нужны аккаунты\n` +
2733
- `📦 Отправьте архив с сессиями через бота\n` +
2734
- `💡 После загрузки аккаунтов функции будут доступны`
2759
+ '❌ <b>LLM чат недоступен</b>\n\n' +
2760
+ '🔒 Для работы AI ассистента нужны аккаунты\n' +
2761
+ '📦 Отправьте архив с сессиями через бота\n' +
2762
+ '💡 После загрузки аккаунтов функции будут доступны'
2735
2763
  );
2736
- logInfo(`❌ LLM Chat запрошен, но аккаунты отсутствуют`);
2764
+ logInfo('❌ LLM Chat запрошен, но аккаунты отсутствуют');
2737
2765
  return;
2738
2766
  }
2739
2767
 
2740
2768
  // Если LLM уже включен - выключаем, и наоборот
2741
2769
  llmChatEnabled = !llmChatEnabled;
2742
-
2770
+
2743
2771
  // Сохраняем состояние LLM чата
2744
2772
  saveBotSettings({
2745
2773
  activeModel: activeModel,
@@ -2753,26 +2781,26 @@ async function toggleLLMChat(chatId) {
2753
2781
  }
2754
2782
 
2755
2783
  await sendMessage(chatId,
2756
- `✅ <b>LLM чат включен!</b>\n\n` +
2757
- `🤖 Теперь я отвечаю как AI ассистент.\n` +
2784
+ '✅ <b>LLM чат включен!</b>\n\n' +
2785
+ '🤖 Теперь я отвечаю как AI ассистент.\n' +
2758
2786
  `📝 Модель: ${getModelForChat(chatId)}\n` +
2759
- `💬 Просто отправляйте сообщения.\n` +
2760
- `💾 Настройка сохранена\n\n` +
2761
- `<b>Команды:</b>\n` +
2762
- `/togglechat - Выключить LLM чат\n` +
2763
- `/clear - Очистить контекст\n` +
2764
- `/model - Информация о модели\n` +
2765
- `/setmodel &lt;название&gt; - Сменить модель\n` +
2766
- `/help - Все команды бота`
2787
+ '💬 Просто отправляйте сообщения.\n' +
2788
+ '💾 Настройка сохранена\n\n' +
2789
+ '<b>Команды:</b>\n' +
2790
+ '/togglechat - Выключить LLM чат\n' +
2791
+ '/clear - Очистить контекст\n' +
2792
+ '/model - Информация о модели\n' +
2793
+ '/setmodel &lt;название&gt; - Сменить модель\n' +
2794
+ '/help - Все команды бота'
2767
2795
  );
2768
2796
 
2769
2797
  logInfo(`✅ LLM Chat включен для пользователя ${chatId} (сохранено)`);
2770
2798
  } else {
2771
2799
  await sendMessage(chatId,
2772
- `❌ <b>LLM чат выключен</b>\n\n` +
2773
- `🔧 Возвращен в режим управления ботом.\n` +
2774
- `💾 Настройка сохранена\n` +
2775
- `Используйте /togglechat чтобы включить снова.`
2800
+ '❌ <b>LLM чат выключен</b>\n\n' +
2801
+ '🔧 Возвращен в режим управления ботом.\n' +
2802
+ '💾 Настройка сохранена\n' +
2803
+ 'Используйте /togglechat чтобы включить снова.'
2776
2804
  );
2777
2805
 
2778
2806
  logInfo(`❌ LLM Chat выключен для пользователя ${chatId} (сохранено)`);
@@ -2786,15 +2814,15 @@ async function showLLMChatStatus(chatId) {
2786
2814
  const status = llmChatEnabled ? '✅ Включен' : '❌ Выключен';
2787
2815
  const model = getModelForChat(chatId);
2788
2816
  const context = chatContexts.get(chatId) || [];
2789
-
2817
+
2790
2818
  await sendMessage(chatId,
2791
- `📊 <b>Состояние LLM чата</b>\n\n` +
2819
+ '📊 <b>Состояние LLM чата</b>\n\n' +
2792
2820
  `🔧 Статус: ${status}\n` +
2793
2821
  `🤖 Модель: <code>${model}</code>\n` +
2794
2822
  `💬 Сообщений в контексте: ${context.length}\n\n` +
2795
2823
  `💡 Используйте /togglechat чтобы ${llmChatEnabled ? 'выключить' : 'включить'} LLM чат`
2796
2824
  );
2797
-
2825
+
2798
2826
  logInfo(`📊 Проверка статуса LLM чата: ${llmChatEnabled ? 'включен' : 'выключен'}`);
2799
2827
  }
2800
2828
 
@@ -2803,44 +2831,44 @@ async function showLLMChatStatus(chatId) {
2803
2831
  */
2804
2832
  async function handleSetModel(chatId, text) {
2805
2833
  const parts = text.trim().split(/\s+/);
2806
-
2834
+
2807
2835
  // Если нет аргумента - показываем текущую модель
2808
2836
  if (parts.length < 2) {
2809
2837
  await showModelInfo(chatId);
2810
2838
  return;
2811
2839
  }
2812
-
2840
+
2813
2841
  const requestedModel = parts[1].trim();
2814
2842
  const { getAvailableModelsFromFile } = await import('../api/chat.js');
2815
2843
  const availableModels = getAvailableModelsFromFile();
2816
-
2844
+
2817
2845
  // Проверяем что модель существует
2818
2846
  if (!availableModels.includes(requestedModel)) {
2819
2847
  await sendMessage(chatId,
2820
- `❌ <b>Модель не найдена</b>\n\n` +
2848
+ '❌ <b>Модель не найдена</b>\n\n' +
2821
2849
  `Модель <code>${requestedModel}</code> не найдена в списке доступных.\n\n` +
2822
- `<b>Используйте /model для списка доступных моделей</b>`
2850
+ '<b>Используйте /model для списка доступных моделей</b>'
2823
2851
  );
2824
2852
  logWarn(`❌ Пользователь ${chatId} попытался установить несуществующую модель: ${requestedModel}`);
2825
2853
  return;
2826
2854
  }
2827
-
2855
+
2828
2856
  // Устанавливаем глобальную активную модель
2829
2857
  activeModel = requestedModel;
2830
-
2858
+
2831
2859
  // Сохраняем в файл
2832
2860
  const settings = loadBotSettings();
2833
2861
  settings.activeModel = requestedModel;
2834
2862
  saveBotSettings(settings);
2835
-
2863
+
2836
2864
  await sendMessage(chatId,
2837
- `✅ <b>Модель изменена!</b>\n\n` +
2865
+ '✅ <b>Модель изменена!</b>\n\n' +
2838
2866
  `🤖 Новая модель: <code>${requestedModel}</code>\n` +
2839
- `💬 Будет использоваться во всех чатах\n` +
2840
- `💾 Настройка сохранена\n\n` +
2841
- `💡 Для сброса используйте /clear`
2867
+ '💬 Будет использоваться во всех чатах\n' +
2868
+ '💾 Настройка сохранена\n\n' +
2869
+ '💡 Для сброса используйте /clear'
2842
2870
  );
2843
-
2871
+
2844
2872
  logInfo(`✅ Установлена глобальная модель: ${requestedModel} (сохранено)`);
2845
2873
  }
2846
2874
 
@@ -2853,7 +2881,7 @@ function getModelForChat(chatId) {
2853
2881
  if (activeModel) {
2854
2882
  return activeModel;
2855
2883
  }
2856
-
2884
+
2857
2885
  // Возвращаем модель из настроек бота
2858
2886
  return getBotSettingsModel();
2859
2887
  }
@@ -2877,15 +2905,15 @@ async function showModelInfo(chatId) {
2877
2905
  const availableModels = getAvailableModelsFromFile();
2878
2906
 
2879
2907
  const message =
2880
- `📊 <b>Информация о модели</b>\n\n` +
2908
+ '📊 <b>Информация о модели</b>\n\n' +
2881
2909
  `🤖 Активная модель: <code>${currentModel}</code>\n` +
2882
2910
  `💬 Сообщений в контексте: ${context.length}\n` +
2883
2911
  `🔧 LLM чат: ${llmChatEnabled ? '✅ Включен' : '❌ Выключен'}\n\n` +
2884
- `<b>Доступные модели:</b>\n` +
2885
- availableModels.map(m => `<code>${m}</code>`).join(', ') +
2886
- `\n\n💡 Для смены модели используйте:\n` +
2887
- `/setmodel &lt;название_модели&gt;\n` +
2888
- `Например: /setmodel qwen3-max`;
2912
+ '<b>Доступные модели:</b>\n' +
2913
+ availableModels.map((m) => `<code>${m}</code>`).join(', ') +
2914
+ '\n\n💡 Для смены модели используйте:\n' +
2915
+ '/setmodel &lt;название_модели&gt;\n' +
2916
+ 'Например: /setmodel qwen3-max';
2889
2917
 
2890
2918
  await sendMessage(chatId, message);
2891
2919
  }
@@ -2897,8 +2925,8 @@ async function clearChatContext(chatId) {
2897
2925
  chatContexts.set(chatId, []);
2898
2926
 
2899
2927
  await sendMessage(chatId,
2900
- `🗑️ <b>Контекст чата очищен</b>\n\n` +
2901
- `💬 Новая история чата начата.\n` +
2928
+ '🗑️ <b>Контекст чата очищен</b>\n\n' +
2929
+ '💬 Новая история чата начата.\n' +
2902
2930
  `🤖 LLM чат: ${llmChatEnabled ? '✅ Включен' : '❌ Выключен'}`
2903
2931
  );
2904
2932
 
@@ -2958,7 +2986,7 @@ function splitMessage(text, maxLength) {
2958
2986
  * Экранирует HTML специальные символы
2959
2987
  */
2960
2988
  function escapeHtml(text) {
2961
- if (!text) return '';
2989
+ if (!text) { return ''; }
2962
2990
  return String(text)
2963
2991
  .replace(/&/g, '&amp;')
2964
2992
  .replace(/</g, '&lt;')
@@ -2969,7 +2997,7 @@ function escapeHtml(text) {
2969
2997
  * Экранирует текст для вставки в <pre> тег (только < и &)
2970
2998
  */
2971
2999
  function escapeHtmlForCode(text) {
2972
- if (!text) return '';
3000
+ if (!text) { return ''; }
2973
3001
  return String(text)
2974
3002
  .replace(/&/g, '&amp;')
2975
3003
  .replace(/</g, '&lt;')