qwen-api-proxy 1.0.10

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.
@@ -0,0 +1,2977 @@
1
+ import { logInfo, logError, logWarn, logDebug } from '../logger/index.js';
2
+ import { TELEGRAM_BOT_TOKEN, TELEGRAM_USER_IDS, SESSION_DIR, DEFAULT_MODEL, TELEGRAM_PROXY, TELEGRAM_PROXY_URL } from '../config.js';
3
+ import { getActiveModel as getBotSettingsModel } from './botSettings.js';
4
+ import { loadBotSettings, saveBotSettings, loadChatModels, setChatModel, getChatModel } from './botSettings.js';
5
+ import { fetchWithQwenProxy } from './proxy.js';
6
+ import fs from 'fs';
7
+ import path from 'path';
8
+ import { exec } from 'child_process';
9
+ import { promisify } from 'util';
10
+ import AdmZip from 'adm-zip';
11
+ import { ProxyAgent } from 'undici';
12
+ import { loadTokens } from '../api/tokenManager.js';
13
+
14
+ // Use global fetch (available in Node 18+) instead of undici's fetch
15
+ const globalFetch = globalThis.fetch;
16
+
17
+
18
+ const execAsync = promisify(exec);
19
+
20
+ let botServer = null;
21
+ let isBotRunning = false;
22
+
23
+ // Хранилище контекста чатов для LLM
24
+ const chatContexts = new Map();
25
+
26
+ // Глобальная активная модель (загружается из файла)
27
+ let activeModel = null;
28
+
29
+ // Флаг включения LLM чата (загружается из файла)
30
+ let llmChatEnabled = false;
31
+
32
+ /**
33
+ * Загружает сохраненные настройки при старте
34
+ */
35
+ function loadPersistedSettings() {
36
+ try {
37
+ // Загружаем глобальные настройки
38
+ const settings = loadBotSettings();
39
+ if (settings.activeModel) {
40
+ activeModel = settings.activeModel;
41
+ logInfo(`📝 Загружена активная модель: ${activeModel}`);
42
+ } else {
43
+ logInfo(`📝 Активная модель не установлена, используется модель по умолчанию`);
44
+ }
45
+ llmChatEnabled = settings.llmChatEnabled || false;
46
+ logInfo(`📝 LLM чат: ${llmChatEnabled ? 'включен' : 'выключен'}`);
47
+ } catch (error) {
48
+ logError('❌ Ошибка загрузки сохраненных настроек', error);
49
+ }
50
+ }
51
+
52
+ /**
53
+ * Проверяет есть ли аккаунты с токенами
54
+ */
55
+ function hasAccounts() {
56
+ const tokens = loadTokens();
57
+ return tokens.length > 0;
58
+ }
59
+
60
+ /**
61
+ * Проверяет работоспособность AI нейросети (единственный источник истины)
62
+ * @param {Array} tokens - массив токенов
63
+ * @returns {Promise<Array>} массив результатов проверки
64
+ */
65
+ async function checkAIHealth(tokens) {
66
+ // Если нет токенов - пропускаем
67
+ if (!tokens || tokens.length === 0) {
68
+ return [{
69
+ name: '🧠 AI Нейросеть',
70
+ status: false,
71
+ details: '⏸️ Пропущено (нет токенов)'
72
+ }];
73
+ }
74
+
75
+ try {
76
+ logInfo('🧪 Тестирование AI нейросети (ping pong)...');
77
+
78
+ // Импортируем функцию sendMessage для прямого запроса к Qwen
79
+ const { sendMessage } = await import('../api/chat.js');
80
+ const testModel = getBotSettingsModel();
81
+
82
+ // Делаем запрос напрямую к Qwen API через наш модуль
83
+ const startTime = Date.now();
84
+ const result = await sendMessage('ping', testModel, null, null, null, null, null, null, 't2t', null, true, 0);
85
+ const responseTime = ((Date.now() - startTime) / 1000).toFixed(2);
86
+
87
+ if (result && !result.error) {
88
+ const responseContent = result.choices?.[0]?.message?.content || '';
89
+ const usedModel = result.model || testModel;
90
+
91
+ // Проверяем что ответ содержит "pong" (в любом регистре)
92
+ const hasPong = responseContent.toLowerCase().includes('pong');
93
+
94
+ if (hasPong) {
95
+ // Тест пройден - ответ содержит pong
96
+ logInfo(`✅ AI тест прошел успешно: модель=${usedModel}, время=${responseTime}с, ответ="${responseContent.substring(0, 50)}"`);
97
+
98
+ return [
99
+ {
100
+ name: '🧠 AI Нейросеть',
101
+ status: true,
102
+ details: `✅ Работает (модель: ${usedModel}, время: ${responseTime}с)`
103
+ },
104
+ {
105
+ name: ' 📝 Ответ',
106
+ status: true,
107
+ details: `💬 "${responseContent.substring(0, 50)}${responseContent.length > 50 ? '...' : ''}"`
108
+ }
109
+ ];
110
+ } else {
111
+ // Тест не пройден - ответ не содержит pong
112
+ const fullResponse = JSON.stringify(result, null, 2);
113
+
114
+ logError(`❌ AI тест не пройден: ответ не содержит "pong"`);
115
+ logError(`Получен ответ: ${responseContent.substring(0, 200)}`);
116
+ logDebug(`Полный JSON ответа: ${fullResponse.substring(0, 1000)}`);
117
+
118
+ return [
119
+ {
120
+ name: '🧠 AI Нейросеть',
121
+ status: false,
122
+ details: `❌ Тест не пройден: ответ не содержит "pong"`
123
+ },
124
+ {
125
+ name: ' 📝 Ответ',
126
+ status: false,
127
+ details: `⚠️ Получен ответ без "pong": "${responseContent.substring(0, 50)}${responseContent.length > 50 ? '...' : ''}"`
128
+ }
129
+ ];
130
+ }
131
+ } else {
132
+ // Ошибка от API
133
+ const errorMsg = result.error || 'Unknown error';
134
+ const fullResponse = JSON.stringify(result, null, 2);
135
+
136
+ logError(`❌ AI тест не пройден: ${errorMsg}`);
137
+ logDebug(`Полный JSON ошибки: ${fullResponse.substring(0, 1000)}`);
138
+
139
+ return [{
140
+ name: '🧠 AI Нейросеть',
141
+ status: false,
142
+ details: `❌ Ошибка: ${errorMsg.substring(0, 80)}`
143
+ }];
144
+ }
145
+ } catch (error) {
146
+ // Ошибка подключения
147
+ logError('❌ AI тест не пройден (ошибка подключения)', error);
148
+
149
+ return [{
150
+ name: '🧠 AI Нейросеть',
151
+ status: false,
152
+ details: `❌ Ошибка подключения: ${error.message.substring(0, 80)}`
153
+ }];
154
+ }
155
+ }
156
+
157
+ // Создаем агент для прокси если настроен
158
+ let proxyAgent = null;
159
+ let proxyConfigured = false;
160
+
161
+ export async function configureProxy() {
162
+ if (TELEGRAM_PROXY || TELEGRAM_PROXY_URL) {
163
+ const proxyUrl = TELEGRAM_PROXY_URL || TELEGRAM_PROXY;
164
+ proxyConfigured = true;
165
+ logInfo('🔧 Telegram прокси настроен');
166
+ logInfo(`📍 Прокси URL: ${proxyUrl.replace(/\/\/([^:]+):([^@]+)@/, '//***:***@')}`); // СкрываемCredentials
167
+ try {
168
+ proxyAgent = new ProxyAgent(proxyUrl);
169
+ logInfo('✅ Прокси агент создан успешно');
170
+
171
+ // Тестируем соединение с прокси
172
+ logInfo('🔍 Тестирование соединения с прокси...');
173
+ const testUrl = 'https://api.telegram.org/bot';
174
+ await globalFetch(testUrl, {
175
+ dispatcher: proxyAgent,
176
+ signal: AbortSignal.timeout(10000)
177
+ });
178
+ logInfo('✅ Соединение с прокси установлено');
179
+ } catch (error) {
180
+ logError('❌ Ошибка создания прокси агента', error);
181
+ }
182
+ }
183
+ }
184
+ /**
185
+ * Обрабатывает ожидающий архив при запуске
186
+ * @returns {Promise<boolean>} true если архив был обработан
187
+ */
188
+ export async function processPendingArchive() {
189
+ const archiveInfoPath = path.join(process.cwd(), '.pending_archive');
190
+
191
+ // Проверяем есть ли ожидающий архив
192
+ if (!fs.existsSync(archiveInfoPath)) {
193
+ return false;
194
+ }
195
+
196
+ try {
197
+ // Читаем информацию об архиве
198
+ const archiveInfo = JSON.parse(fs.readFileSync(archiveInfoPath, 'utf8'));
199
+ const { archivePath, fileName, ext, uploadedAt } = archiveInfo;
200
+
201
+ logInfo('🔄 Обнаружен ожидающий архив для распаковки');
202
+ logInfo(`📂 Архив: ${fileName}`);
203
+ logInfo(`📍 Путь: ${archivePath}`);
204
+ logInfo(`🕐 Загружен: ${uploadedAt}`);
205
+
206
+ // Проверяем что архив существует
207
+ if (!fs.existsSync(archivePath)) {
208
+ logWarn(`⚠️ Архив не найден: ${archivePath}`);
209
+ logWarn('🗑️ Возможно temp/ директория не сохранена в Docker volumes');
210
+ fs.unlinkSync(archiveInfoPath);
211
+ return false;
212
+ }
213
+
214
+ // Проверяем размер архива
215
+ const archiveSize = fs.statSync(archivePath).size;
216
+ logInfo(`📊 Размер архива: ${archiveSize} bytes (${(archiveSize / 1024).toFixed(2)} KB)`);
217
+
218
+ if (archiveSize === 0) {
219
+ logError('❌ Архив пустой (0 bytes)!');
220
+ logError('🔄 Файл не был загружен из Telegram или был поврежден');
221
+ logError('💡 Попробуйте отправить архив снова');
222
+ fs.unlinkSync(archiveInfoPath);
223
+ try {
224
+ fs.unlinkSync(archivePath);
225
+ logInfo('🗑️ Удален пустой архив');
226
+ } catch (e) {
227
+ // Игнорируем
228
+ }
229
+ return false;
230
+ }
231
+
232
+ if (archiveSize < 100) {
233
+ logWarn(`⚠️ Архив очень маленький (${archiveSize} bytes). Возможно это не настоящий архив`);
234
+ }
235
+
236
+ // Распаковываем архив (без backup - это первый запуск)
237
+ const sessionPath = path.join(process.cwd(), SESSION_DIR);
238
+
239
+ logInfo('📦 Распаковка архива...');
240
+
241
+ if (ext === '.zip') {
242
+ await extractZip(archivePath, sessionPath, null);
243
+ } else if (ext === '.7z') {
244
+ await extract7z(archivePath, sessionPath, null);
245
+ }
246
+
247
+ logInfo('✅ Архив успешно распакован');
248
+
249
+ // Удаляем флаг и архив
250
+ try {
251
+ fs.unlinkSync(archiveInfoPath);
252
+ logInfo('🗑️ Удален флаг .pending_archive');
253
+ } catch (e) {
254
+ logWarn('Не удалось удалить флаг .pending_archive', e);
255
+ }
256
+
257
+ try {
258
+ fs.unlinkSync(archivePath);
259
+ logInfo('🗑️ Удален временный архив');
260
+ } catch (e) {
261
+ logWarn('Не удалось удалить временный архив', e);
262
+ }
263
+
264
+ return true;
265
+
266
+ } catch (error) {
267
+ logError('❌ Ошибка при обработке ожидающего архива', error);
268
+ // Удаляем флаг чтобы не блокировать запуск
269
+ try {
270
+ fs.unlinkSync(archiveInfoPath);
271
+ } catch (e) {
272
+ // Игнорируем
273
+ }
274
+ return false;
275
+ }
276
+ }
277
+
278
+ /**
279
+ * Проверяет все подсистемы и отправляет отчет
280
+ * @param {boolean} botStarted - запущен ли бот
281
+ * @param {boolean} autoSend - автоматически отправлять отчет (по умолчанию true)
282
+ * @returns {Promise<Array>} массив проверок
283
+ */
284
+ export async function checkAllSubsystems(botStarted, autoSend = true) {
285
+ const checks = [];
286
+ let allOk = true;
287
+
288
+ // 1. Проверяем папку session
289
+ const sessionPath = path.join(process.cwd(), SESSION_DIR);
290
+ const sessionExists = fs.existsSync(sessionPath);
291
+ const sessionAccounts = sessionExists && fs.existsSync(path.join(sessionPath, 'accounts'))
292
+ ? fs.readdirSync(path.join(sessionPath, 'accounts')).length
293
+ : 0;
294
+
295
+ checks.push({
296
+ name: '📂 Session директория',
297
+ status: sessionExists,
298
+ details: sessionExists ? `✅ Найдено аккаунтов: ${sessionAccounts}` : '❌ Не найдена'
299
+ });
300
+
301
+ // 2. Проверяем токены
302
+ const tokens = loadTokens();
303
+ const now = Date.now();
304
+
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;
310
+ // Проверяем наличие cookies.json
311
+ const cookiesPath = path.join(process.cwd(), SESSION_DIR, 'accounts', t.id, 'cookies.json');
312
+ if (!fs.existsSync(cookiesPath)) return false;
313
+ return true;
314
+ });
315
+
316
+ // Проверка оставшегося времени для токенов
317
+ if (tokens.length > 0) {
318
+ const filteredCount = tokens.length - validTokens.length;
319
+
320
+ const expirySummary = validTokens.reduce((acc, token) => {
321
+ const now = Date.now();
322
+
323
+ // Если expiryTime не установлен
324
+ if (!token.expiryTime) {
325
+ acc.tokens.push({ timeStr: 'Неизвестно', id: token.id, hasExpiry: false });
326
+ return acc;
327
+ }
328
+
329
+ const timeLeft = token.expiryTime - now;
330
+
331
+ // Форматируем время в удобочитаемый вид
332
+ let timeStr;
333
+ if (timeLeft <= 0) {
334
+ acc.expired++;
335
+ timeStr = 'Протух';
336
+ } else {
337
+ const days = Math.floor(timeLeft / (1000 * 60 * 60 * 24));
338
+ const hours = Math.floor((timeLeft % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60));
339
+ const minutes = Math.floor((timeLeft % (1000 * 60 * 60)) / (1000 * 60));
340
+ const seconds = Math.floor((timeLeft % (1000 * 60)) / 1000);
341
+
342
+ const parts = [];
343
+ if (days > 0) parts.push(`${days}д`);
344
+ if (hours > 0) parts.push(`${hours}ч`);
345
+ if (minutes > 0) parts.push(`${minutes}м`);
346
+ parts.push(`${seconds}с`);
347
+
348
+ timeStr = parts.join(' ');
349
+ }
350
+
351
+ acc.tokens.push({ timeStr, id: token.id, hasExpiry: true });
352
+ return acc;
353
+ }, { expired: 0, tokens: [] });
354
+
355
+ let tokenDetails = `✅ Доступно: ${validTokens.length}`;
356
+ if (filteredCount > 0) {
357
+ tokenDetails += ` (пропущено ${filteredCount} истекших)`;
358
+ }
359
+
360
+ checks.push({
361
+ name: '🎫 Токены',
362
+ status: validTokens.length > 0,
363
+ details: tokenDetails
364
+ });
365
+
366
+ // Показываем только действительные токены
367
+ if (expirySummary.tokens.length > 0) {
368
+ expirySummary.tokens.forEach((token, index) => {
369
+ // Проверяем наличие cookies
370
+ const cookiesPath = path.join(process.cwd(), SESSION_DIR, 'accounts', token.id, 'cookies.json');
371
+ const hasCookies = fs.existsSync(cookiesPath);
372
+ const cookieStatus = hasCookies ? '✅' : '❌';
373
+
374
+ checks.push({
375
+ name: ` Токен ${index + 1}`,
376
+ status: token.hasExpiry && hasCookies,
377
+ details: token.hasExpiry
378
+ ? `${cookieStatus} ${token.id}\n ⏱️ Осталось: ${token.timeStr}`
379
+ : `${cookieStatus} ${token.id}\n ⚠️ Время истечения: ${token.timeStr}`
380
+ });
381
+ });
382
+ }
383
+
384
+ // Если все токены истекли, показываем предупреждение
385
+ if (validTokens.length === 0) {
386
+ checks.push({
387
+ name: '⚠️ Внимание',
388
+ status: false,
389
+ details: `Все ${tokens.length} токенов истекли. Создайте новые сессии.`
390
+ });
391
+ }
392
+ } else {
393
+ checks.push({
394
+ name: '🎫 Токены',
395
+ status: false,
396
+ details: '❌ Нет токенов'
397
+ });
398
+ }
399
+
400
+ // 3. Проверяем Telegram бота
401
+ checks.push({
402
+ name: '🤖 Telegram бот',
403
+ status: botStarted,
404
+ details: botStarted ? '✅ Работает' : '❌ Не запущен'
405
+ });
406
+
407
+ // 3.1. Показываем настройки бота (если загружены)
408
+ if (botStarted) {
409
+ const llmStatus = llmChatEnabled ? '✅ Включен' : '❌ Выключен';
410
+ const modelUsed = activeModel || getBotSettingsModel();
411
+ checks.push({
412
+ name: ' ⚙️ LLM режим',
413
+ status: llmChatEnabled,
414
+ details: `${llmStatus} (модель: ${modelUsed})`
415
+ });
416
+ }
417
+
418
+ // 4. Проверяем прокси
419
+ checks.push({
420
+ name: '🌐 Прокси',
421
+ status: true,
422
+ details: proxyConfigured
423
+ ? '✅ Настроен'
424
+ : 'Не используется'
425
+ });
426
+
427
+ // 5. Проверяем папку uploads
428
+ const uploadsPath = path.join(process.cwd(), 'uploads');
429
+ const uploadsExists = fs.existsSync(uploadsPath);
430
+ checks.push({
431
+ name: '📤 Uploads директория',
432
+ status: uploadsExists,
433
+ details: uploadsExists ? '✅ Доступна' : '❌ Не найдена'
434
+ });
435
+
436
+ // 6. Проверяем логи
437
+ const logsPath = path.join(process.cwd(), 'logs');
438
+ const logsExists = fs.existsSync(logsPath);
439
+ checks.push({
440
+ name: '📝 Логирование',
441
+ status: logsExists,
442
+ details: logsExists ? '✅ Работает' : '❌ Не настроено'
443
+ });
444
+
445
+ // 7. Проверяем p7zip
446
+ try {
447
+ await execAsync('which 7z');
448
+ checks.push({
449
+ name: '📦 p7zip',
450
+ status: true,
451
+ details: '✅ Установлен'
452
+ });
453
+ } catch (e) {
454
+ checks.push({
455
+ name: '📦 p7zip',
456
+ status: false,
457
+ details: '❌ Не установлен (7z архивы не будут работать)'
458
+ });
459
+ allOk = false;
460
+ }
461
+
462
+ // 8. Проверяем работу AI (только если есть токены)
463
+ const aiCheckResult = await checkAIHealth(tokens);
464
+ checks.push(...aiCheckResult);
465
+
466
+ // Формируем отчет для логов
467
+ logInfo('='.repeat(60));
468
+ logInfo('🔍 ПРОВЕРКА ПОД СИСТЕМ ПРИ ЗАПУСКЕ');
469
+ logInfo('='.repeat(60));
470
+
471
+ checks.forEach(check => {
472
+ const status = check.status ? '✅' : '❌';
473
+ logInfo(`${status} ${check.name}: ${check.details}`);
474
+ });
475
+
476
+ logInfo('='.repeat(60));
477
+
478
+ const hasTokens = tokens.length > 0;
479
+ if (hasTokens && allOk) {
480
+ logInfo('✅ ВСЕ ПОД СИСТЕМЫ РАБОТАЮТ');
481
+ } else if (!hasTokens) {
482
+ logWarn('⚠️ ТОКЕНЫ ОТСУТСТВУЮТ - РЕЖИМ ОЖИДАНИЯ АРХИВА');
483
+ } else {
484
+ logWarn('⚠️ НЕКОТОРЫЕ ПОД СИСТЕМЫ НЕ РАБОТАЮТ');
485
+ }
486
+ logInfo('='.repeat(60));
487
+
488
+ // Формируем отчет для Telegram с группировкой
489
+ const reportLines = [];
490
+
491
+ // Заголовок
492
+ reportLines.push(`🚀 <b>Сервис запущен!</b>\n`);
493
+
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') ||
498
+ c.name.includes('Токен ') || c.name.includes('AI') || c.name.includes('Ответ')
499
+ );
500
+ mainComponents.forEach(check => {
501
+ reportLines.push(`${check.name}: ${check.details}`);
502
+ });
503
+
504
+ reportLines.push('');
505
+
506
+ // Группа 2: Инфраструктура
507
+ reportLines.push(`<b>🏗️ Инфраструктура:</b>`);
508
+ const infrastructure = checks.filter(c =>
509
+ c.name.includes('Прокси') || c.name.includes('Uploads') || c.name.includes('Логирование')
510
+ );
511
+ infrastructure.forEach(check => {
512
+ reportLines.push(`${check.name}: ${check.details}`);
513
+ });
514
+
515
+ reportLines.push('');
516
+
517
+ // Группа 3: Инструменты
518
+ const tools = checks.filter(c => c.name.includes('p7zip'));
519
+ if (tools.length > 0) {
520
+ reportLines.push(`<b>🔧 Инструменты:</b>`);
521
+ tools.forEach(check => {
522
+ reportLines.push(`${check.name}: ${check.details}`);
523
+ });
524
+ reportLines.push('');
525
+ }
526
+
527
+ // Разделитель
528
+ reportLines.push(`${'━'.repeat(25)}`);
529
+
530
+ // Итоговый статус
531
+ if (hasTokens && allOk) {
532
+ reportLines.push(`✅ <b>Все системы работают</b>`);
533
+ } else if (!hasTokens) {
534
+ reportLines.push(`⚠️ <b>Режим ожидания архива</b>`);
535
+ reportLines.push(`📦 Отправьте архив с сессиями`);
536
+ } else {
537
+ reportLines.push(`⚠️ <b>Есть проблемы</b>`);
538
+ }
539
+
540
+ // Ссылки
541
+ reportLines.push(`\n🌐 API: http://localhost:${process.env.PORT || 3264}`);
542
+ reportLines.push(`📖 Docs: http://localhost:${process.env.PORT || 3264}/api`);
543
+
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
+
550
+ // Справка
551
+ reportLines.push(`\n💡 <b>Справка:</b>`);
552
+ reportLines.push(`📝 Используйте /help для списка команд`);
553
+ reportLines.push(`🔍 Используйте /status для проверки состояния`);
554
+ reportLines.push(`🤖 Используйте /chat для включения LLM режима`);
555
+
556
+ const report = reportLines.join('\n');
557
+
558
+ // Отправляем всем пользователям (только если autoSend = true)
559
+ if (autoSend && botStarted) {
560
+ try {
561
+ await notifyAllUsers(report);
562
+ logInfo('📤 Отчет о запуске отправлен в Telegram');
563
+ } catch (e) {
564
+ logError('❌ Не удалось отправить отчет в Telegram', e);
565
+ }
566
+ }
567
+
568
+ // Возвращаем массив проверок для повторного использования
569
+ return checks;
570
+ }
571
+
572
+ /**
573
+ * Выполняет fetch запрос с учетом прокси
574
+ */
575
+ async function fetchWithProxy(url, options = {}, skipLog = false) {
576
+ const fetchOptions = {
577
+ ...options
578
+ };
579
+
580
+ // Добавляем прокси агент только если он существует
581
+ if (proxyAgent) {
582
+ fetchOptions.dispatcher = proxyAgent;
583
+ if (proxyConfigured && !skipLog) {
584
+ logInfo(`🌐 Запрос через прокси...`);
585
+ }
586
+ }
587
+
588
+ fetchOptions.timeout = 30000; // 30 секунд таймаут
589
+
590
+ return fetch(url, fetchOptions);
591
+ }
592
+
593
+ /**
594
+ * Запускает Telegram бота для получения команд и файлов
595
+ */
596
+ export async function startTelegramBot() {
597
+ if (!TELEGRAM_BOT_TOKEN) {
598
+ logWarn('Telegram бот не запущен: отсутствует TELEGRAM_BOT_TOKEN');
599
+ return false;
600
+ }
601
+
602
+ if (isBotRunning) {
603
+ logWarn('Telegram бот уже запущен');
604
+ return true;
605
+ }
606
+
607
+ try {
608
+ logInfo('🤖 Запуск Telegram бота...');
609
+
610
+ // Загружаем сохраненные настройки
611
+ loadPersistedSettings();
612
+
613
+ // Проверяем доступность API Telegram
614
+ const testUrl = `https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/getMe`;
615
+ const response = await fetchWithProxy(testUrl);
616
+
617
+ if (!response.ok) {
618
+ logError(`❌ Не удалось подключиться к Telegram API: HTTP ${response.status}`);
619
+ return false;
620
+ }
621
+
622
+ const botInfo = await response.json();
623
+ logInfo(`✅ Telegram бот запущен: @${botInfo.result.username}`);
624
+
625
+ isBotRunning = true;
626
+
627
+ // Запускаем polling для получения обновлений
628
+ startPolling();
629
+
630
+ return true;
631
+ } catch (error) {
632
+ // Определяем тип ошибки для лучшего сообщения
633
+ let errorMessage = 'Ошибка при запуске Telegram бота';
634
+
635
+ if (error.message?.includes('fetch failed') || error.code === 'ECONNREFUSED') {
636
+ if (proxyAgent) {
637
+ errorMessage = '❌ Не удалось подключиться к Telegram API через прокси. Проверьте настройки прокси.';
638
+ } else {
639
+ errorMessage = '❌ Не удалось подключиться к Telegram API. Если API заблокирован, настройте прокси через TELEGRAM_PROXY.';
640
+ }
641
+ } else if (error.message?.includes('ENOTFOUND') || error.code === 'ENOTFOUND') {
642
+ errorMessage = '❌ Ошибка DNS. Проверьте интернет-соединение.';
643
+ } else if (error.message?.includes('ETIMEDOUT') || error.code === 'ETIMEDOUT') {
644
+ errorMessage = '❌ Таймаут подключения. Проверьте соединение и настройки прокси.';
645
+ }
646
+
647
+ logError(errorMessage, error);
648
+ return false;
649
+ }
650
+ }
651
+
652
+ /**
653
+ * Останавливает Telegram бота
654
+ */
655
+ export function stopTelegramBot() {
656
+ if (botServer) {
657
+ isBotRunning = false;
658
+ logInfo('🛑 Telegram бот остановлен');
659
+ }
660
+ }
661
+
662
+ /**
663
+ * Запускает polling для получения обновлений от Telegram
664
+ */
665
+ async function startPolling() {
666
+ // Загружаем последний обработанный update_id
667
+ const lastUpdatePath = path.join(process.cwd(), '.last_telegram_update');
668
+ let offset = 0;
669
+
670
+ if (fs.existsSync(lastUpdatePath)) {
671
+ try {
672
+ const lastUpdateId = parseInt(fs.readFileSync(lastUpdatePath, 'utf8'));
673
+ offset = lastUpdateId + 1;
674
+ logInfo(`📡 Продолжаем polling с update_id: ${offset}`);
675
+ } catch (e) {
676
+ logWarn('Не удалось прочитать .last_telegram_update, начинаем с 0');
677
+ }
678
+ }
679
+
680
+ while (isBotRunning) {
681
+ try {
682
+ const updatesUrl = `https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/getUpdates?offset=${offset}&limit=10&timeout=30`;
683
+ const response = await fetchWithProxy(updatesUrl, undefined, true);
684
+
685
+ if (!response.ok) {
686
+ const errorText = await response.text().catch(() => '');
687
+ logError(`Ошибка получения обновлений Telegram: HTTP ${response.status}${errorText ? ` | Ответ: ${errorText.substring(0, 500)}` : ''}`);
688
+ await new Promise(resolve => setTimeout(resolve, 5000));
689
+ continue;
690
+ }
691
+
692
+ const data = await response.json();
693
+
694
+ if (data.ok && data.result.length > 0) {
695
+ for (const update of data.result) {
696
+ offset = update.update_id + 1;
697
+
698
+ // Сохраняем последний обработанный update_id
699
+ try {
700
+ fs.writeFileSync(lastUpdatePath, String(update.update_id));
701
+ } catch (e) {
702
+ // Игнорируем ошибки записи
703
+ }
704
+
705
+ await processUpdate(update);
706
+ }
707
+ }
708
+ } catch (error) {
709
+ logError('Ошибка в polling Telegram', error);
710
+ await new Promise(resolve => setTimeout(resolve, 5000));
711
+ }
712
+ }
713
+ }
714
+
715
+ /**
716
+ * Обрабатывает обновление от Telegram
717
+ */
718
+ async function processUpdate(update) {
719
+ // Обработка сообщений
720
+ if (update.message) {
721
+ const message = update.message;
722
+ const chatId = message.chat.id;
723
+
724
+ // Проверяем, что пользователь авторизован
725
+ if (!TELEGRAM_USER_IDS.includes(String(chatId))) {
726
+ await sendMessage(chatId, '❌ У вас нет доступа к этому боту');
727
+ return;
728
+ }
729
+
730
+ // Обработка команд
731
+ if (message.text) {
732
+ // Если включен LLM чат и это не команда
733
+ if (llmChatEnabled && !message.text.startsWith('/')) {
734
+ // Проверяем есть ли аккаунты для LLM
735
+ if (hasAccounts()) {
736
+ await handleLLMChat(chatId, message.text);
737
+ } else {
738
+ await sendMessage(chatId,
739
+ `❌ <b>LLM чат временно недоступен</b>\n\n` +
740
+ `🔒 Нет аккаунтов для обработки запросов\n` +
741
+ `📦 Отправьте архив с сессиями\n` +
742
+ `💡 Или используйте /chat чтобы выключить LLM режим`
743
+ );
744
+ }
745
+ return;
746
+ }
747
+
748
+ await handleCommand(chatId, message.text);
749
+ }
750
+
751
+ // Обработка файлов (документов)
752
+ if (message.document) {
753
+ try {
754
+ await handleDocument(chatId, message.document);
755
+ } catch (error) {
756
+ logError('❌ Ошибка обработки документа', error);
757
+ try {
758
+ await sendMessage(chatId, `❌ Ошибка обработки файла: ${error.message}`);
759
+ } catch (sendError) {
760
+ // Игнорируем ошибки отправки
761
+ }
762
+ }
763
+ }
764
+
765
+ // Обработка фотографий (для генерации изображений)
766
+ if (message.photo) {
767
+ try {
768
+ // Получаем текст из caption
769
+ const caption = message.caption || '';
770
+ await handlePhoto(chatId, message.photo, caption);
771
+ } catch (error) {
772
+ logError('❌ Ошибка обработки фото', error);
773
+ try {
774
+ await sendMessage(chatId, `❌ Ошибка обработки фото: ${error.message}`);
775
+ } catch (sendError) {
776
+ // Игнорируем ошибки отправки
777
+ }
778
+ }
779
+ }
780
+ }
781
+ }
782
+
783
+ /**
784
+ * Обрабатывает генерацию изображений
785
+ * @param {string} chatId - ID чата
786
+ * @param {string} prompt - Текст запроса
787
+ * @param {string} imagePath - Путь к файлу изображения (опционально, для image-to-image)
788
+ */
789
+ async function handleImageGeneration(chatId, prompt, imagePath = null) {
790
+ try {
791
+ logInfo(`🎨 Telegram: запрошена генерация изображения: ${prompt.substring(0, 100)}...`);
792
+ if (imagePath) {
793
+ logInfo(`📸 Режим image-to-image с файлом: ${imagePath}`);
794
+ }
795
+
796
+ // Отправляем сообщение о начале генерации
797
+ await sendMessage(chatId,
798
+ `🎨 <b>Генерация изображения...</b>\n\n` +
799
+ `📝 Запрос: ${prompt}\n` +
800
+ (imagePath ? `📸 Режим: Image-to-Image\n` : '') +
801
+ `⏳ Пожалуйста, подождите...`
802
+ );
803
+
804
+ // Импортируем функцию генерации
805
+ const { generateImage } = await import('../api/imageGeneration.js');
806
+ const { getActiveModel } = await import('./botSettings.js');
807
+
808
+ // Используем модель для генерации изображений
809
+ const model = 'qwen-image-plus';
810
+
811
+ const startTime = Date.now();
812
+ const options = {
813
+ size: '1024*1024'
814
+ };
815
+
816
+ // Если есть изображение, передаем путь к файлу
817
+ if (imagePath) {
818
+ options.imagePath = imagePath;
819
+ }
820
+
821
+ const result = await generateImage(prompt, model, options);
822
+ const generationTime = ((Date.now() - startTime) / 1000).toFixed(1);
823
+
824
+ if (result.success && result.imageUrl) {
825
+ logInfo(`✅ Изображение сгенерировано за ${generationTime}с: ${result.imageUrl}`);
826
+
827
+ // Отправляем изображение как фото
828
+ try {
829
+ await sendPhoto(chatId, result.imageUrl, prompt);
830
+
831
+ // Отправляем дополнительную информацию
832
+ await sendMessage(chatId,
833
+ `✅ <b>Изображение сгенерировано!</b>\n\n` +
834
+ `🎨 Модель: ${result.model || model}\n` +
835
+ `⏱️ Время: ${generationTime}с\n` +
836
+ `📝 Prompt: ${prompt}`
837
+ );
838
+ } catch (photoError) {
839
+ // Если не удалось отправить как фото, отправляем как ссылку
840
+ logWarn('Не удалось отправить изображение как фото, отправляю ссылку');
841
+ await sendMessage(chatId,
842
+ `✅ <b>Изображение сгенерировано!</b>\n\n` +
843
+ `🖼️ <a href="${result.imageUrl}">Скачать изображение</a>\n\n` +
844
+ `🎨 Модель: ${result.model || model}\n` +
845
+ `⏱️ Время: ${generationTime}с\n` +
846
+ `📝 Prompt: ${prompt}`
847
+ );
848
+ }
849
+ } else {
850
+ logError(`❌ Ошибка генерации изображения: ${result.error}`);
851
+
852
+ // Проверяем, это rate limit?
853
+ if (result.rateLimit) {
854
+ const hours = result.rateLimitHours || 24;
855
+ await sendMessage(chatId,
856
+ `⏳ <b>Лимит генерации изображений достигнут</b>\n\n` +
857
+ `⚠️ API Qwen ограничивает количество генераций в день\n` +
858
+ `⏰ Попробуйте через ${hours}ч\n\n` +
859
+ `💡 Совет: используйте другой аккаунт с токеном\n` +
860
+ `📝 Или подождите сброса лимита`
861
+ );
862
+ return;
863
+ }
864
+
865
+ // Формируем детальное сообщение об ошибке
866
+ let errorMessage = result.error || 'Неизвестная ошибка';
867
+
868
+ // Если есть дополнительные детали в rawResponse
869
+ if (result.rawResponse) {
870
+ // Проверяем errorBody (JSON строка)
871
+ if (result.rawResponse.errorBody) {
872
+ try {
873
+ const errorData = JSON.parse(result.rawResponse.errorBody);
874
+ if (errorData.error && errorData.error.details) {
875
+ errorMessage += `\n\n📋 Детали: ${errorData.error.details}`;
876
+ if (errorData.error.modality) {
877
+ errorMessage += `\n🔍 Проверка: ${errorData.error.modality.join(', ')}`;
878
+ }
879
+ if (errorData.error.stage) {
880
+ errorMessage += `\n📍 Этап: ${errorData.error.stage}`;
881
+ }
882
+ } else if (errorData.details || errorData.detail) {
883
+ errorMessage += `\n\n📋 Детали: ${errorData.details || errorData.detail}`;
884
+ }
885
+ } catch {
886
+ // Не JSON, используем как есть
887
+ if (result.rawResponse.errorBody.length < 200) {
888
+ errorMessage += `\n\n📋 ${result.rawResponse.errorBody}`;
889
+ }
890
+ }
891
+ }
892
+ // Проверяем details (JSON строка из handleApiError)
893
+ else if (result.rawResponse.details && typeof result.rawResponse.details === 'string') {
894
+ try {
895
+ const errorData = JSON.parse(result.rawResponse.details);
896
+ if (errorData.error && errorData.error.details) {
897
+ errorMessage += `\n\n📋 Детали: ${errorData.error.details}`;
898
+ if (errorData.error.modality) {
899
+ errorMessage += `\n🔍 Проверка: ${errorData.error.modality.join(', ')}`;
900
+ }
901
+ if (errorData.error.stage) {
902
+ errorMessage += `\n📍 Этап: ${errorData.error.stage}`;
903
+ }
904
+ } else if (errorData.details || errorData.detail) {
905
+ errorMessage += `\n\n📋 Детали: ${errorData.details || errorData.detail}`;
906
+ }
907
+ } catch {
908
+ // Не JSON, используем как есть
909
+ if (result.rawResponse.details.length < 200) {
910
+ errorMessage += `\n\n📋 ${result.rawResponse.details}`;
911
+ }
912
+ }
913
+ }
914
+ // Проверяем прямое поле error
915
+ else if (result.rawResponse.error && result.rawResponse.error.details) {
916
+ errorMessage += `\n\n📋 Детали: ${result.rawResponse.error.details}`;
917
+ if (result.rawResponse.error.modality) {
918
+ errorMessage += `\n🔍 Проверка: ${result.rawResponse.error.modality.join(', ')}`;
919
+ }
920
+ }
921
+ }
922
+
923
+ await sendMessage(chatId,
924
+ `❌ <b>Ошибка генерации изображения</b>\n\n` +
925
+ `⚠️ ${escapeHtml(errorMessage)}\n\n` +
926
+ `💡 Попробуйте изменить запрос или повторите позже`
927
+ );
928
+ }
929
+ } catch (error) {
930
+ logError('❌ Ошибка в handleImageGeneration', error);
931
+ await sendMessage(chatId,
932
+ `❌ <b>Произошла ошибка</b>\n\n` +
933
+ `⚠️ ${error.message}\n\n` +
934
+ `💡 Попробуйте позже`
935
+ );
936
+ }
937
+ }
938
+
939
+ /**
940
+ * Отправляет фото в Telegram
941
+ */
942
+ async function sendPhoto(chatId, photoUrl, caption = '') {
943
+ try {
944
+ const url = `https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/sendPhoto`;
945
+
946
+ const body = {
947
+ chat_id: chatId,
948
+ photo: photoUrl,
949
+ caption: caption.substring(0, 1024) // Telegram limit for captions
950
+ };
951
+
952
+ const response = await fetchWithProxy(url, {
953
+ method: 'POST',
954
+ headers: {
955
+ 'Content-Type': 'application/json'
956
+ },
957
+ body: JSON.stringify(body)
958
+ }, true);
959
+
960
+ if (!response.ok) {
961
+ const errorText = await response.text().catch(() => '');
962
+ let errorDescription = `HTTP ${response.status}`;
963
+ try {
964
+ const errorData = JSON.parse(errorText);
965
+ errorDescription = errorData.description || errorDescription;
966
+ } catch {
967
+ // Если не JSON, используем текст ответа
968
+ if (errorText) {
969
+ errorDescription = errorText.substring(0, 500);
970
+ }
971
+ }
972
+ throw new Error(errorDescription);
973
+ }
974
+
975
+ logDebug(`📸 Фото отправлено: ${photoUrl}`);
976
+ return true;
977
+ } catch (error) {
978
+ logError('Ошибка при отправке фото', error);
979
+ throw error;
980
+ }
981
+ }
982
+
983
+ /**
984
+ * Обрабатывает команды
985
+ */
986
+ async function handleCommand(chatId, text) {
987
+ const command = text.trim().toLowerCase();
988
+
989
+ // Обработка команды /setmodel с аргументом
990
+ if (command.startsWith('/setmodel')) {
991
+ await handleSetModel(chatId, text);
992
+ return;
993
+ }
994
+
995
+ switch (command) {
996
+ case '/start':
997
+ case '/help':
998
+ await sendHelpMessage(chatId);
999
+ break;
1000
+
1001
+ case '/status':
1002
+ await sendStatusMessage(chatId);
1003
+ break;
1004
+
1005
+ case '/restart':
1006
+ await handleRestart(chatId);
1007
+ break;
1008
+
1009
+ case '/chat':
1010
+ await showLLMChatStatus(chatId);
1011
+ break;
1012
+
1013
+ case '/togglechat':
1014
+ await toggleLLMChat(chatId);
1015
+ break;
1016
+
1017
+ case '/model':
1018
+ await showModelInfo(chatId);
1019
+ break;
1020
+
1021
+ case '/setmodel':
1022
+ await showModelInfo(chatId);
1023
+ break;
1024
+
1025
+ case '/clear':
1026
+ await clearChatContext(chatId);
1027
+ break;
1028
+
1029
+ case '/setup':
1030
+ await sendSetupMessage(chatId);
1031
+ break;
1032
+
1033
+ case '/connect':
1034
+ await sendConnectMessage(chatId);
1035
+ break;
1036
+
1037
+ case '/about':
1038
+ await sendAboutMessage(chatId);
1039
+ break;
1040
+
1041
+ case '/archive':
1042
+ await sendArchiveInstructions(chatId);
1043
+ break;
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;
1056
+
1057
+ case '/image':
1058
+ case '/imagine':
1059
+ await sendMessage(chatId,
1060
+ '🎨 <b>Генерация изображений</b>\n\n' +
1061
+ '💬 <b>Текстовый режим:</b>\n' +
1062
+ '/image &lt;описание&gt; - генерация по описанию\n\n' +
1063
+ '📸 <b>Режим Image-to-Image:</b>\n' +
1064
+ 'Отправьте фото с подписью (caption)\n' +
1065
+ 'Или используйте /image &lt;описание&gt; с фото\n\n' +
1066
+ '📝 Примеры:\n' +
1067
+ '• /image A beautiful sunset\n' +
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
+ }
1081
+ } else {
1082
+ await sendMessage(chatId, '❓ Неизвестная команда. Используйте /help для списка команд');
1083
+ }
1084
+ }
1085
+ }
1086
+
1087
+ /**
1088
+ * Обрабатывает фотографии для генерации изображений (image-to-image)
1089
+ */
1090
+ async function handlePhoto(chatId, photos, caption = '') {
1091
+ try {
1092
+ logInfo(`📸 Получено фото с caption: "${caption.substring(0, 50)}${caption.length > 50 ? '...' : ''}"`);
1093
+
1094
+ // Проверяем, есть ли команда в caption
1095
+ let prompt = caption || 'Улучши это изображение';
1096
+ let hasCommand = false;
1097
+
1098
+ // Если caption начинается с /image или /imagine
1099
+ if (caption.startsWith('/image ') || caption.startsWith('/imagine ')) {
1100
+ hasCommand = true;
1101
+ prompt = caption.substring(caption.indexOf(' ') + 1).trim();
1102
+ }
1103
+
1104
+ if (!hasCommand && !caption) {
1105
+ // Если просто фото без caption - не обрабатываем как image-to-image
1106
+ logInfo('📸 Фото без caption - пропускаем обработку');
1107
+ return;
1108
+ }
1109
+
1110
+ // Telegram отправляет несколько размеров фото, берем самый большой (последний в массиве)
1111
+ const photo = photos[photos.length - 1];
1112
+ const fileId = photo.file_id;
1113
+ const fileSize = photo.file_size;
1114
+
1115
+ logInfo(`📸 Загрузка фото из Telegram (file_id: ${fileId}, size: ${fileSize} bytes)`);
1116
+
1117
+ await sendMessage(chatId,
1118
+ `🎨 <b>Обработка изображения...</b>\n\n` +
1119
+ `📝 Запрос: ${prompt}\n` +
1120
+ `⏳ Пожалуйста, подождите...`
1121
+ );
1122
+
1123
+ // Скачиваем фото из Telegram и сохраняем во временный файл
1124
+ const tempFilePath = await downloadTelegramFileToTemp(fileId);
1125
+
1126
+ if (!tempFilePath) {
1127
+ throw new Error('Не удалось скачать фото из Telegram');
1128
+ }
1129
+
1130
+ logInfo(`✅ Фото скачано: ${tempFilePath}`);
1131
+
1132
+ // Генерируем изображение с использованием фото
1133
+ await handleImageGeneration(chatId, prompt, tempFilePath);
1134
+
1135
+ // Удаляем временный файл
1136
+ try {
1137
+ fs.unlinkSync(tempFilePath);
1138
+ logInfo('🗑️ Временный файл удален');
1139
+ } catch (e) {
1140
+ logWarn('Не удалось удалить временный файл', e);
1141
+ }
1142
+
1143
+ } catch (error) {
1144
+ logError('❌ Ошибка в handlePhoto', error);
1145
+ await sendMessage(chatId,
1146
+ `❌ <b>Ошибка обработки фото</b>\n\n` +
1147
+ `⚠️ ${error.message}\n\n` +
1148
+ `💡 Попробуйте позже`
1149
+ );
1150
+ }
1151
+ }
1152
+
1153
+ /**
1154
+ * Скачивает файл из Telegram и сохраняет во временный файл
1155
+ * @param {string} fileId - ID файла в Telegram
1156
+ * @returns {Promise<string>} - Путь к временному файлу
1157
+ */
1158
+ async function downloadTelegramFileToTemp(fileId) {
1159
+ try {
1160
+ // Получаем информацию о файле
1161
+ const fileUrl = `https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/getFile?file_id=${fileId}`;
1162
+ const fileResponse = await fetchWithProxy(fileUrl, undefined, true);
1163
+
1164
+ if (!fileResponse.ok) {
1165
+ throw new Error(`Не удалось получить информацию о файле: HTTP ${fileResponse.status}`);
1166
+ }
1167
+
1168
+ const fileData = await fileResponse.json();
1169
+
1170
+ if (!fileData.ok) {
1171
+ throw new Error(`Telegram API error: ${fileData.description || 'Unknown error'}`);
1172
+ }
1173
+
1174
+ const filePath = fileData.result.file_path;
1175
+ const downloadUrl = `https://api.telegram.org/file/bot${TELEGRAM_BOT_TOKEN}/${filePath}`;
1176
+
1177
+ logInfo(`📥 URL для скачивания файла: ${downloadUrl}`);
1178
+
1179
+ // Скачиваем файл
1180
+ const downloadResponse = await fetchWithProxy(downloadUrl, undefined, true);
1181
+
1182
+ if (!downloadResponse.ok) {
1183
+ throw new Error(`Не удалось скачать файл: HTTP ${downloadResponse.status}`);
1184
+ }
1185
+
1186
+ // Сохраняем во временный файл
1187
+ const tempDir = path.join(process.cwd(), 'temp');
1188
+ if (!fs.existsSync(tempDir)) {
1189
+ fs.mkdirSync(tempDir, { recursive: true });
1190
+ }
1191
+
1192
+ const tempFileName = `telegram_${Date.now()}_${fileId}.jpg`;
1193
+ const tempFilePath = path.join(tempDir, tempFileName);
1194
+
1195
+ const buffer = Buffer.from(await downloadResponse.arrayBuffer());
1196
+ fs.writeFileSync(tempFilePath, buffer);
1197
+
1198
+ logInfo(`✅ Файл сохранен: ${tempFilePath} (${buffer.length} bytes)`);
1199
+
1200
+ return tempFilePath;
1201
+
1202
+ } catch (error) {
1203
+ logError('❌ Ошибка скачивания файла из Telegram', error);
1204
+ throw error;
1205
+ }
1206
+ }
1207
+
1208
+ /**
1209
+ * Обрабатывает полученные документы (архивы)
1210
+ */
1211
+ async function handleDocument(chatId, document) {
1212
+ const fileName = document.file_name || 'unknown';
1213
+ const fileSize = document.file_size;
1214
+ const fileId = document.file_id;
1215
+
1216
+ logInfo(`📦 Получен файл: ${fileName} (${fileSize} bytes)`);
1217
+
1218
+ // Проверяем расширение файла
1219
+ const allowedExtensions = ['.zip', '.7z'];
1220
+ const ext = path.extname(fileName).toLowerCase();
1221
+
1222
+ if (!allowedExtensions.includes(ext)) {
1223
+ await sendMessage(chatId,
1224
+ `❌ Неподдерживаемый формат файла: ${ext}\n` +
1225
+ `📎 Поддерживаются только: .zip и .7z`
1226
+ );
1227
+ return;
1228
+ }
1229
+
1230
+ // Проверяем размер файла (максимум 50MB)
1231
+ const maxSize = 50 * 1024 * 1024; // 50MB
1232
+ if (fileSize > maxSize) {
1233
+ await sendMessage(chatId,
1234
+ `❌ Файл слишком большой: ${(fileSize / 1024 / 1024).toFixed(2)}MB\n` +
1235
+ `📏 Максимальный размер: 50MB`
1236
+ );
1237
+ return;
1238
+ }
1239
+
1240
+ // Проверяем есть ли уже ожидающий архив с таким же именем
1241
+ const archiveInfoPath = path.join(process.cwd(), '.pending_archive');
1242
+ if (fs.existsSync(archiveInfoPath)) {
1243
+ try {
1244
+ const existingArchive = JSON.parse(fs.readFileSync(archiveInfoPath, 'utf8'));
1245
+ if (existingArchive.fileName === fileName) {
1246
+ logInfo(`⚠️ Архив ${fileName} уже ожидает распаковки, перезапускаем`);
1247
+ await sendMessage(chatId,
1248
+ `✅ Архив ${fileName} уже загружен\n` +
1249
+ `🔄 Перезапуск для распаковки...`
1250
+ );
1251
+ // Запускаем перезапуск
1252
+ await gracefulRestart(chatId);
1253
+ return;
1254
+ }
1255
+ } catch (e) {
1256
+ // Игнорируем ошибку чтения
1257
+ }
1258
+ }
1259
+
1260
+ // Проверяем существует ли уже файл в temp (по оригинальному имени)
1261
+ const tempDir = path.join(process.cwd(), 'temp');
1262
+ const existingFiles = fs.existsSync(tempDir) ? fs.readdirSync(tempDir) : [];
1263
+ const existingFile = existingFiles.find(f => f.endsWith(`_${fileName}`) || f === fileName);
1264
+
1265
+ if (existingFile) {
1266
+ const tempFilePath = path.join(tempDir, existingFile);
1267
+ logInfo(`⚠️ Файл ${tempFilePath} уже существует, используем его`);
1268
+
1269
+ // Создаем флаг для существующего архива
1270
+ if (!fs.existsSync(archiveInfoPath)) {
1271
+ fs.writeFileSync(archiveInfoPath, JSON.stringify({
1272
+ archivePath: tempFilePath,
1273
+ fileName: fileName,
1274
+ ext: ext,
1275
+ uploadedAt: new Date().toISOString()
1276
+ }));
1277
+ logInfo(`📝 Создан флаг для существующего архива`);
1278
+
1279
+ // Запускаем перезапуск
1280
+ await gracefulRestart(chatId);
1281
+ } else {
1282
+ await sendMessage(chatId,
1283
+ `✅ Архив уже загружен и ожидает распаковки\n` +
1284
+ `🔄 Сервис будет перезапущен...`
1285
+ );
1286
+ // Запускаем перезапуск
1287
+ await gracefulRestart(chatId);
1288
+ }
1289
+ return;
1290
+ }
1291
+
1292
+ await sendMessage(chatId, `⏳ Загрузка файла ${fileName}...`);
1293
+
1294
+ try {
1295
+ // Получаем информацию о файле из Telegram
1296
+ const fileUrl = `https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/getFile?file_id=${fileId}`;
1297
+ logInfo(`📡 Запрос информации о файле: ${fileId}`);
1298
+
1299
+ const fileResponse = await fetchWithProxy(fileUrl);
1300
+ logInfo(`📥 Статус ответа: ${fileResponse.status}`);
1301
+
1302
+ const fileData = await fileResponse.json();
1303
+ logInfo(`📄 Получены данные: ${JSON.stringify(fileData).substring(0, 200)}...`);
1304
+
1305
+ if (!fileData.ok) {
1306
+ throw new Error(`Не удалось получить файл из Telegram: ${fileData.description || 'Unknown error'}`);
1307
+ }
1308
+
1309
+ const filePath = fileData.result.file_path;
1310
+ const downloadUrl = `https://api.telegram.org/file/bot${TELEGRAM_BOT_TOKEN}/${filePath}`;
1311
+
1312
+ logInfo(`📥 URL для скачивания: ${downloadUrl}`);
1313
+ logInfo(`📊 Ожидаемый размер (из Telegram): ${fileData.result.file_size || 'unknown'} bytes`);
1314
+
1315
+ // Скачиваем файл
1316
+ logInfo(`📥 Начинаем загрузку файла...`);
1317
+ const downloadResponse = await fetchWithProxy(downloadUrl);
1318
+ logInfo(`📥 Статус загрузки: ${downloadResponse.status}`);
1319
+ logInfo(`📥 Content-Type: ${downloadResponse.headers.get('content-type')}`);
1320
+ logInfo(`📥 Content-Length: ${downloadResponse.headers.get('content-length')}`);
1321
+
1322
+ const fileBuffer = await downloadResponse.arrayBuffer();
1323
+
1324
+ // Проверяем размер файла
1325
+ const fileSize = Buffer.from(fileBuffer).length;
1326
+ logInfo(`📥 Загружено ${fileSize} bytes из Telegram`);
1327
+
1328
+ if (fileSize === 0) {
1329
+ logError(`❌ Загружен пустой файл!`);
1330
+ logError(`📡 Статус ответа: ${downloadResponse.status}`);
1331
+ logError(`📊 Content-Length header: ${downloadResponse.headers.get('content-length')}`);
1332
+ logError(`🔗 URL: ${downloadUrl}`);
1333
+ throw new Error('Файл пустой или не удалось загрузить из Telegram. Проверьте что файл существует в Telegram');
1334
+ }
1335
+
1336
+ if (fileSize < 100) {
1337
+ logWarn(`⚠️ Файл очень маленький (${fileSize} bytes). Возможно это не настоящий архив`);
1338
+ }
1339
+
1340
+ // Создаем директорию temp
1341
+ if (!fs.existsSync(tempDir)) {
1342
+ fs.mkdirSync(tempDir, { recursive: true });
1343
+ }
1344
+
1345
+ // Генерируем уникальное имя файла с префиксом
1346
+ const uniquePrefix = `${Date.now()}_${Math.random().toString(36).substring(2, 8)}`;
1347
+ const uniqueFileName = `${uniquePrefix}_${fileName}`;
1348
+ const tempFilePath = path.join(tempDir, uniqueFileName);
1349
+
1350
+ // Удаляем старые файлы с таким же именем (если есть)
1351
+ const oldFiles = existingFiles.filter(f => f.endsWith(`_${fileName}`) || f === fileName);
1352
+ if (oldFiles.length > 0) {
1353
+ logInfo(`🗑️ Найдено ${oldFiles.length} старых файлов с именем ${fileName}, удаляем`);
1354
+ oldFiles.forEach(oldFile => {
1355
+ try {
1356
+ fs.unlinkSync(path.join(tempDir, oldFile));
1357
+ logInfo(`🗑️ Удален старый файл: ${oldFile}`);
1358
+ } catch (e) {
1359
+ logWarn(`⚠️ Не удалось удалить ${oldFile}:`, e.message);
1360
+ }
1361
+ });
1362
+ }
1363
+
1364
+ // Сохраняем временный файл
1365
+ fs.writeFileSync(tempFilePath, Buffer.from(fileBuffer));
1366
+
1367
+ // Проверяем что файл записался
1368
+ const writtenSize = fs.statSync(tempFilePath).size;
1369
+ logInfo(`💾 Файл сохранен: ${writtenSize} bytes`);
1370
+ logInfo(`📝 Уникальное имя: ${uniqueFileName}`);
1371
+
1372
+ if (writtenSize !== fileSize) {
1373
+ throw new Error(`Ошибка записи файла: ожидалось ${fileSize} bytes, записано ${writtenSize} bytes`);
1374
+ }
1375
+
1376
+ logInfo(`✅ Файл сохранен: ${tempFilePath}`);
1377
+ await sendMessage(chatId, `✅ Файл загружен. Распаковка...`);
1378
+
1379
+ // Сохраняем информацию об архиве для обработки при перезапуске
1380
+ fs.writeFileSync(archiveInfoPath, JSON.stringify({
1381
+ archivePath: tempFilePath,
1382
+ fileName: fileName,
1383
+ ext: ext,
1384
+ uploadedAt: new Date().toISOString()
1385
+ }));
1386
+
1387
+ logInfo(`📝 Записан флаг ожидающего архива: ${archiveInfoPath}`);
1388
+ await sendMessage(chatId,
1389
+ `✅ Архив сохранен\n` +
1390
+ `🔄 При перезапуске будет автоматически распакован\n` +
1391
+ `📂 Файл: ${tempFilePath}`
1392
+ );
1393
+
1394
+ // Запускаем перезапуск
1395
+ await gracefulRestart(chatId);
1396
+
1397
+ } catch (error) {
1398
+ logError('Ошибка при обработке файла', error);
1399
+ await sendMessage(chatId, `❌ Ошибка: ${error.message}`);
1400
+ }
1401
+ }
1402
+
1403
+ /**
1404
+ * Создает session_backup текущей папки session
1405
+ * @returns {boolean} true если backup успешен или не нужен, false если ошибка
1406
+ */
1407
+ async function createSessionBackup(sessionPath, chatId) {
1408
+ try {
1409
+ if (!fs.existsSync(sessionPath)) {
1410
+ logInfo('📦 Session папка не существует, session_backup не нужен');
1411
+ return true; // Backup не нужен, продолжаем
1412
+ }
1413
+
1414
+ // Создаем имя папки session_backup с датой и временем
1415
+ const now = new Date();
1416
+ const timestamp = now.toISOString().replace(/[:.]/g, '-').slice(0, 19); // YYYY-MM-DDTHH-mm-ss
1417
+ const backupDir = path.join(process.cwd(), 'session_backup', timestamp);
1418
+
1419
+ logInfo(`📦 Создание session_backup session в: ${backupDir}`);
1420
+
1421
+ // Создаем директорию session_backup
1422
+ try {
1423
+ if (!fs.existsSync(backupDir)) {
1424
+ fs.mkdirSync(backupDir, { recursive: true });
1425
+ }
1426
+ } catch (mkdirError) {
1427
+ logWarn(`⚠️ Не удалось создать папку session_backup: ${mkdirError.message}`);
1428
+ logWarn('⛔ session_backup отменен - СЕРВЕР НЕ БУДЕТ ПЕРЕЗАПУЩЕН');
1429
+ try {
1430
+ await sendMessage(chatId,
1431
+ `❌ Ошибка session_backup: нет прав для создания папки\n` +
1432
+ `⛔ Распаковка продолжена, но сервер НЕ будет перезапущен\n` +
1433
+ `💡 Перезапустите сервер вручную после проверки файлов`
1434
+ );
1435
+ } catch (sendError) {
1436
+ // Игнорируем
1437
+ }
1438
+ return false; // Backup не удался, НЕ перезапускаем сервер
1439
+ }
1440
+
1441
+ // Копируем все файлы и папки из session в session_backup
1442
+ const items = fs.readdirSync(sessionPath);
1443
+
1444
+ if (items.length === 0) {
1445
+ logInfo('📦 Session папка пуста, session_backup не нужен');
1446
+ return;
1447
+ }
1448
+
1449
+ try {
1450
+ await sendMessage(chatId,
1451
+ `💾 Создание session_backup текущей session...\n` +
1452
+ `📁 Найдено элементов: ${items.length}`
1453
+ );
1454
+ } catch (sendError) {
1455
+ logWarn(`⚠️ Не удалось отправить сообщение о backup: ${sendError.message}`);
1456
+ }
1457
+
1458
+ for (const item of items) {
1459
+ try {
1460
+ const sourcePath = path.join(sessionPath, item);
1461
+ const targetPath = path.join(backupDir, item);
1462
+
1463
+ if (fs.statSync(sourcePath).isDirectory()) {
1464
+ // Копируем директорию рекурсивно
1465
+ await execAsync(`cp -rf "${sourcePath}" "${targetPath}" 2>&1 || true`);
1466
+ } else {
1467
+ // Копируем файл
1468
+ fs.copyFileSync(sourcePath, targetPath);
1469
+ }
1470
+ } catch (copyError) {
1471
+ logWarn(`⚠️ Ошибка копирования ${item}: ${copyError.message}`);
1472
+ // Продолжаем копировать остальные файлы
1473
+ }
1474
+ }
1475
+
1476
+ logInfo(`✅ session_backup успешно создан: ${backupDir}`);
1477
+ try {
1478
+ await sendMessage(chatId, `✅ session_backup создан: <code>session_backup/${timestamp}</code>`);
1479
+ } catch (sendError) {
1480
+ // Игнорируем ошибки отправки сообщений
1481
+ }
1482
+
1483
+ return true; // Backup успешен
1484
+
1485
+ } catch (error) {
1486
+ // Полностью подавляем все ошибки - backup не должен ломать процесс
1487
+ logWarn(`⚠️ session_backup пропущен: ${error.message}`);
1488
+ logWarn('⛔ session_backup отменен - СЕРВЕР НЕ БУДЕТ ПЕРЕЗАПУЩЕН');
1489
+ try {
1490
+ await sendMessage(chatId,
1491
+ `❌ Ошибка session_backup: ${error.message}\n` +
1492
+ `⛔ Распаковка продолжена, но сервер НЕ будет перезапущен\n` +
1493
+ `💡 Перезапустите сервер вручную после проверки файлов`
1494
+ );
1495
+ } catch (sendError) {
1496
+ // Игнорируем
1497
+ }
1498
+ return false; // Ошибка, НЕ перезапускаем сервер
1499
+ }
1500
+ }
1501
+
1502
+ /**
1503
+ * Распаковывает архив в папку session
1504
+ */
1505
+ async function extractArchive(filePath, chatId, ext) {
1506
+ const sessionPath = path.join(process.cwd(), SESSION_DIR);
1507
+
1508
+ try {
1509
+ // Создаем session_backup текущей session папки
1510
+ const backupSuccess = await createSessionBackup(sessionPath, chatId);
1511
+
1512
+ // Если backup не удался, не продолжаем распаковку и НЕ перезапускаем сервер
1513
+ if (!backupSuccess) {
1514
+ logWarn('⛔ Распаковка отменена из-за ошибки backup');
1515
+ await sendMessage(chatId,
1516
+ `⚠️ <b>Распаковка отменена</b>\n\n` +
1517
+ `❌ Не удалось создать backup текущей session\n` +
1518
+ `🔒 Файлы не были изменены для безопасности\n` +
1519
+ `💡 Проверьте права доступа и попробуйте снова`
1520
+ );
1521
+ return; // Выходим без перезапуска
1522
+ }
1523
+
1524
+ let result;
1525
+ if (ext === '.zip') {
1526
+ result = await extractZip(filePath, sessionPath, chatId);
1527
+ } else if (ext === '.7z') {
1528
+ await extract7z(filePath, sessionPath, chatId);
1529
+ result = { successCount: 'все', errorCount: 0 };
1530
+ }
1531
+
1532
+ let statusMessage =
1533
+ `✅ <b>Архив успешно распакован!</b>\n\n` +
1534
+ `📂 Папка session обновлена\n` +
1535
+ `💾 Старая версия сохранена в session_backup\n`;
1536
+
1537
+ if (result && result.successCount !== 'все') {
1538
+ statusMessage += `📊 Распаковано: ${result.successCount} файлов\n`;
1539
+ if (result.errorCount > 0) {
1540
+ statusMessage += `⚠️ Ошибок: ${result.errorCount} (пропущены)\n`;
1541
+ }
1542
+ }
1543
+
1544
+ statusMessage += `🔄 Сервис будет перезапущен...`;
1545
+
1546
+ await sendMessage(chatId, statusMessage);
1547
+
1548
+ // Ждем 2 секунды чтобы сообщение дошло
1549
+ await new Promise(resolve => setTimeout(resolve, 2000));
1550
+
1551
+ // Запускаем перезапуск только если backup был успешен
1552
+ logInfo('✅ Backup успешен, запускаем перезапуск сервера');
1553
+ await gracefulRestart(chatId);
1554
+
1555
+ } catch (error) {
1556
+ logError('Ошибка при распаковке архива', error);
1557
+ await sendMessage(chatId, `❌ Ошибка распаковки: ${error.message}`);
1558
+ }
1559
+ }
1560
+
1561
+ /**
1562
+ * Распаковывает ZIP архив
1563
+ */
1564
+ async function extractZip(zipPath, sessionPath, chatId) {
1565
+ return new Promise((resolve, reject) => {
1566
+ try {
1567
+ const zip = new AdmZip(zipPath);
1568
+ const zipEntries = zip.getEntries();
1569
+
1570
+ // Для отладки: показываем первые несколько записей в архиве
1571
+ const firstEntries = zipEntries.slice(0, 20).map(e => e.entryName);
1572
+ logInfo(`📂 Первые 20 записей в ZIP архиве:`);
1573
+ logInfo(firstEntries.join('\n'));
1574
+
1575
+ // Проверяем что есть папка session
1576
+ const hasSessionFolder = zipEntries.some(entry =>
1577
+ entry.entryName.startsWith('session/') || entry.entryName === 'session'
1578
+ );
1579
+
1580
+ if (!hasSessionFolder) {
1581
+ // Пытаемся найти session в любом месте архива
1582
+ const sessionEntries = zipEntries.filter(e =>
1583
+ e.entryName.includes('session') || e.entryName.includes('Session')
1584
+ );
1585
+
1586
+ if (sessionEntries.length > 0) {
1587
+ logInfo(`🔍 Найдены записи содержащие 'session':`);
1588
+ logInfo(sessionEntries.slice(0, 10).map(e => e.entryName).join('\n'));
1589
+ reject(new Error(
1590
+ `Архив содержит '${sessionEntries[0].entryName}', но не содержит 'session/' в корне. ` +
1591
+ `Переместите папку session/ в корень архива`
1592
+ ));
1593
+ } else {
1594
+ reject(new Error('Архив не содержит папку "session". Проверите что архив содержит папку session/ в корне'));
1595
+ }
1596
+ return;
1597
+ }
1598
+
1599
+ logInfo('✅ Найдена папка session в корне архива');
1600
+
1601
+ // Создаем папку session если не существует
1602
+ if (!fs.existsSync(sessionPath)) {
1603
+ fs.mkdirSync(sessionPath, { recursive: true });
1604
+ }
1605
+
1606
+ // Распаковываем только содержимое папки session, игнорируя ошибки
1607
+ let successCount = 0;
1608
+ let errorCount = 0;
1609
+
1610
+ zipEntries.forEach(entry => {
1611
+ if (entry.entryName.startsWith('session/')) {
1612
+ try {
1613
+ const relativePath = entry.entryName.substring('session/'.length);
1614
+ if (relativePath) {
1615
+ const targetPath = path.join(sessionPath, relativePath);
1616
+
1617
+ if (entry.isDirectory) {
1618
+ if (!fs.existsSync(targetPath)) {
1619
+ fs.mkdirSync(targetPath, { recursive: true });
1620
+ }
1621
+ } else {
1622
+ const dir = path.dirname(targetPath);
1623
+ if (!fs.existsSync(dir)) {
1624
+ fs.mkdirSync(dir, { recursive: true });
1625
+ }
1626
+ fs.writeFileSync(targetPath, entry.getData());
1627
+ // Устанавливаем права доступа для записанных файлов
1628
+ fs.chmodSync(targetPath, 0o755);
1629
+ }
1630
+ successCount++;
1631
+ }
1632
+ } catch (err) {
1633
+ errorCount++;
1634
+ logWarn(`⚠️ Ошибка распаковки ${entry.entryName}: ${err.message}`);
1635
+ }
1636
+ }
1637
+ });
1638
+
1639
+ logInfo(`✅ ZIP архив распакован: ${successCount} файлов успешно, ${errorCount} ошибок`);
1640
+
1641
+ if (successCount === 0) {
1642
+ reject(new Error('Не удалось распаковать ни одного файла'));
1643
+ } else {
1644
+ resolve({ successCount, errorCount });
1645
+ }
1646
+ } catch (error) {
1647
+ reject(error);
1648
+ }
1649
+ });
1650
+ }
1651
+
1652
+ /**
1653
+ * Распаковывает 7z архив (требует p7zip)
1654
+ */
1655
+ async function extract7z(sevenZPath, sessionPath, chatId) {
1656
+ try {
1657
+ // Проверяем что p7zip установлен
1658
+ await execAsync('which 7z');
1659
+
1660
+ // Создаем временную папку для распаковки
1661
+ const tempExtractDir = path.join(process.cwd(), 'temp', 'extract_7z');
1662
+ if (fs.existsSync(tempExtractDir)) {
1663
+ logInfo('🗑️ Очищаем старую временную директорию');
1664
+ fs.rmSync(tempExtractDir, { recursive: true, force: true });
1665
+ }
1666
+ fs.mkdirSync(tempExtractDir, { recursive: true });
1667
+ logInfo(`📁 Создана временная директория: ${tempExtractDir}`);
1668
+
1669
+ // Распаковываем, игнорируя ошибки
1670
+ const { stdout, stderr } = await execAsync(
1671
+ `7z x "${sevenZPath}" -o"${tempExtractDir}" -y 2>&1 || true`
1672
+ );
1673
+
1674
+ logInfo('7z распаковка stdout:', stdout);
1675
+
1676
+ // Проверяем что что-то распаковалось
1677
+ try {
1678
+ const checkDir = await execAsync(`ls -A "${tempExtractDir}" 2>&1 || true`);
1679
+ if (!checkDir.stdout.trim()) {
1680
+ throw new Error('7z распаковка не создала ни одного файла. Архив поврежден или пустой');
1681
+ }
1682
+ } catch (checkError) {
1683
+ if (checkError.message.includes('ни одного файла')) {
1684
+ throw checkError;
1685
+ }
1686
+ }
1687
+
1688
+ // Показываем содержимое распакованной директории для отладки
1689
+ try {
1690
+ const listResult = await execAsync(`ls -la "${tempExtractDir}"`);
1691
+ logInfo('📂 Содержимое распакованной директории:');
1692
+ logInfo(listResult.stdout);
1693
+
1694
+ // Также показываем рекурсивный список
1695
+ const recursiveList = await execAsync(`find "${tempExtractDir}" -type f -o -type d | head -50`);
1696
+ logInfo('📂 Полный список файлов (первые 50):');
1697
+ logInfo(recursiveList.stdout);
1698
+ } catch (listError) {
1699
+ logWarn('Не удалось получить список файлов:', listError.message);
1700
+ }
1701
+
1702
+ // Проверяем что есть папка session
1703
+ let extractedSessionPath = path.join(tempExtractDir, 'session');
1704
+
1705
+ if (!fs.existsSync(extractedSessionPath)) {
1706
+ // Пытаемся найти папку session в поддиректориях
1707
+ let foundSessionPath = null;
1708
+ try {
1709
+ const findResult = await execAsync(
1710
+ `find "${tempExtractDir}" -type d -name "session" 2>&1 || true`
1711
+ );
1712
+
1713
+ if (findResult.stdout.trim()) {
1714
+ const foundPaths = findResult.stdout.trim().split('\n');
1715
+ logInfo(`🔍 Найдены папки session: ${foundPaths.join(', ')}`);
1716
+ foundSessionPath = foundPaths[0]; // Берем первую найденную
1717
+ }
1718
+ } catch (findError) {
1719
+ // Игнорируем
1720
+ }
1721
+
1722
+ if (foundSessionPath) {
1723
+ logInfo(`✅ Найдена папка session в: ${foundSessionPath}`);
1724
+ extractedSessionPath = foundSessionPath; // Используем найденную
1725
+ } else {
1726
+ throw new Error('Архив не содержит папку "session". Проверите что архив содержит папку session/ в корне');
1727
+ }
1728
+ } else {
1729
+ logInfo('✅ Найдена папка session в корне архива');
1730
+ }
1731
+
1732
+ // Создаем папку session если не существует
1733
+ const sessionPathFinal = path.join(process.cwd(), SESSION_DIR);
1734
+ if (!fs.existsSync(sessionPathFinal)) {
1735
+ fs.mkdirSync(sessionPathFinal, { recursive: true });
1736
+ logInfo(`📁 Создана папка session: ${sessionPathFinal}`);
1737
+ }
1738
+
1739
+ // Копируем содержимое session с помощью JavaScript
1740
+ logInfo(`📋 Копирование файлов из ${extractedSessionPath} в ${sessionPathFinal}`);
1741
+
1742
+ let successCount = 0;
1743
+ let errorCount = 0;
1744
+ let totalSize = 0;
1745
+
1746
+ function copyDirRecursive(sourceDir, targetDir) {
1747
+ // Создаем целевую директорию
1748
+ if (!fs.existsSync(targetDir)) {
1749
+ fs.mkdirSync(targetDir, { recursive: true });
1750
+ }
1751
+
1752
+ // Читаем все файлы и папки
1753
+ const items = fs.readdirSync(sourceDir);
1754
+ logInfo(`📂 Найдено элементов для копирования: ${items.length}`);
1755
+
1756
+ items.forEach(item => {
1757
+ const sourcePath = path.join(sourceDir, item);
1758
+ const targetPath = path.join(targetDir, item);
1759
+
1760
+ try {
1761
+ const stat = fs.statSync(sourcePath);
1762
+
1763
+ if (stat.isDirectory()) {
1764
+ // Рекурсивно копируем директорию
1765
+ copyDirRecursive(sourcePath, targetPath);
1766
+ } else {
1767
+ // Копируем файл
1768
+ fs.copyFileSync(sourcePath, targetPath);
1769
+
1770
+ // Устанавливаем права доступа
1771
+ fs.chmodSync(targetPath, 0o755);
1772
+
1773
+ const fileSize = stat.size;
1774
+ totalSize += fileSize;
1775
+ successCount++;
1776
+
1777
+ logInfo(`✅ Скопирован: ${item} (${fileSize} bytes)`);
1778
+ }
1779
+ } catch (err) {
1780
+ errorCount++;
1781
+ logError(`❌ Ошибка копирования ${item}:`, err.message);
1782
+ // Продолжаем копировать остальные файлы
1783
+ }
1784
+ });
1785
+ }
1786
+
1787
+ // Запускаем копирование
1788
+ copyDirRecursive(extractedSessionPath, sessionPathFinal);
1789
+
1790
+ logInfo(`📊 Итого скопировано: ${successCount} файлов, ${errorCount} ошибок`);
1791
+ logInfo(`📊 Общий размер: ${(totalSize / 1024).toFixed(2)} KB`);
1792
+
1793
+ if (successCount === 0) {
1794
+ throw new Error('Не удалось скопировать ни одного файла из архива');
1795
+ }
1796
+
1797
+ // Очищаем временную папку
1798
+ try {
1799
+ fs.rmSync(tempExtractDir, { recursive: true, force: true });
1800
+ logInfo('🗑️ Очищена временная директория');
1801
+ } catch (e) {
1802
+ logWarn('⚠️ Не удалось очистить временную директорию:', e.message);
1803
+ }
1804
+
1805
+ logInfo('✅ 7z архив успешно распакован');
1806
+ } catch (error) {
1807
+ if (error.message.includes('which 7z')) {
1808
+ throw new Error('p7zip не установлен. Установите: apt-get install p7zip-full');
1809
+ }
1810
+ throw error;
1811
+ }
1812
+ }
1813
+
1814
+ /**
1815
+ * Корректный перезапуск сервиса
1816
+ */
1817
+ async function gracefulRestart(chatId) {
1818
+ try {
1819
+ logInfo('🔄 Запуск корректного перезапуска сервиса...');
1820
+
1821
+ await sendMessage(chatId,
1822
+ `🔄 <b>Перезапуск сервиса...</b>\n\n` +
1823
+ `⏱️ Сервис будет перезапущен в течение 5 секунд`
1824
+ );
1825
+
1826
+ // Даем Docker Compose время на перезапуск
1827
+ // Выходим с кодом 42 - сигнал для docker-compose restart
1828
+ logInfo('🛑 Завершение работы для перезапуска Docker Compose...');
1829
+
1830
+ // Записываем файл-флаг для docker-compose
1831
+ const restartFlagPath = path.join(process.cwd(), '.restart_flag');
1832
+ const hasPendingArchive = fs.existsSync(path.join(process.cwd(), '.pending_archive'));
1833
+
1834
+ fs.writeFileSync(restartFlagPath, JSON.stringify({
1835
+ reason: hasPendingArchive ? 'telegram_session_upload_with_archive' : 'telegram_session_update',
1836
+ timestamp: new Date().toISOString(),
1837
+ chatId: chatId,
1838
+ hasPendingArchive: hasPendingArchive
1839
+ }));
1840
+
1841
+ // Завершаем процесс
1842
+ process.exit(42);
1843
+
1844
+ } catch (error) {
1845
+ logError('Ошибка при перезапуске', error);
1846
+ await sendMessage(chatId, `❌ Ошибка перезапуска: ${error.message}`);
1847
+ }
1848
+ }
1849
+
1850
+ /**
1851
+ * Отправляет сообщение помощи
1852
+ */
1853
+ async function sendHelpMessage(chatId) {
1854
+ const accountsExist = hasAccounts();
1855
+
1856
+ 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`;
1863
+
1864
+ // Команды генерации изображений
1865
+ 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`;
1870
+
1871
+ // Показываем LLM команды только если есть аккаунты
1872
+ if (accountsExist) {
1873
+ 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`;
1881
+ } else {
1882
+ helpText +=
1883
+ `⚠️ <b>LLM Чат недоступен</b>\n\n` +
1884
+ `🔒 Функции AI ассистента временно недоступны\n` +
1885
+ `📦 Отправьте архив с сессиями для активации\n\n`;
1886
+ }
1887
+
1888
+ 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`;
1902
+
1903
+ await sendMessage(chatId, helpText);
1904
+ }
1905
+
1906
+ /**
1907
+ * Отправляет сообщение со статусом
1908
+ */
1909
+ async function sendSetupMessage(chatId) {
1910
+ 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
+
1961
+ await sendMessage(chatId, setupText);
1962
+ }
1963
+
1964
+ async function sendConnectMessage(chatId) {
1965
+ 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
+
2044
+ await sendMessage(chatId, connectText);
2045
+ }
2046
+
2047
+ async function sendAboutMessage(chatId) {
2048
+ 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
+
2084
+ await sendMessage(chatId, aboutText);
2085
+ }
2086
+
2087
+ /**
2088
+ * Отправляет инструкции по созданию архива сессии
2089
+ */
2090
+ async function sendArchiveInstructions(chatId) {
2091
+ 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
+
2167
+ await sendMessage(chatId, archiveText);
2168
+ }
2169
+
2170
+ async function sendStatusMessage(chatId, isScheduled = false) {
2171
+ try {
2172
+ // Получаем статус бота
2173
+ const telegramToken = process.env.TELEGRAM_BOT_TOKEN;
2174
+ const botStarted = !!telegramToken;
2175
+
2176
+ // Проверяем все подсистемы (не отправляем автоматически, так как sendStatusMessage отправит сам)
2177
+ const checks = await checkAllSubsystems(botStarted, false);
2178
+
2179
+ // Формируем сообщение с统一的格式
2180
+ const reportLines = [];
2181
+
2182
+ // Заголовок
2183
+ if (isScheduled) {
2184
+ const now = new Date();
2185
+ const timeStr = now.toLocaleString('ru-RU', {
2186
+ day: '2-digit',
2187
+ month: '2-digit',
2188
+ year: 'numeric',
2189
+ hour: '2-digit',
2190
+ minute: '2-digit'
2191
+ });
2192
+ reportLines.push(`⏰ <b>Плановая проверка</b> (${timeStr})\n`);
2193
+ } else {
2194
+ reportLines.push(`🚀 <b>Сервис запущен!</b>\n`);
2195
+ }
2196
+
2197
+ // Группа 1: Основные компоненты
2198
+ reportLines.push(`<b>🔑 Основные компоненты:</b>`);
2199
+ const mainComponents = checks.filter(c =>
2200
+ c.name.includes('Session') || c.name.includes('Токены') || c.name.includes('Telegram') ||
2201
+ c.name.includes('Токен ') || c.name.includes('AI') || c.name.includes('Ответ')
2202
+ );
2203
+ mainComponents.forEach(check => {
2204
+ reportLines.push(`${check.name}: ${check.details}`);
2205
+ });
2206
+
2207
+ reportLines.push('');
2208
+
2209
+ // Группа 2: Инфраструктура
2210
+ reportLines.push(`<b>🏗️ Инфраструктура:</b>`);
2211
+ const infrastructure = checks.filter(c =>
2212
+ c.name.includes('Прокси') || c.name.includes('Uploads') || c.name.includes('Логирование')
2213
+ );
2214
+ infrastructure.forEach(check => {
2215
+ reportLines.push(`${check.name}: ${check.details}`);
2216
+ });
2217
+
2218
+ reportLines.push('');
2219
+
2220
+ // Группа 3: Инструменты
2221
+ reportLines.push(`<b>🔧 Инструменты:</b>`);
2222
+ const tools = checks.filter(c =>
2223
+ c.name.includes('p7zip')
2224
+ );
2225
+ tools.forEach(check => {
2226
+ reportLines.push(`${check.name}: ${check.details}`);
2227
+ });
2228
+
2229
+ reportLines.push('');
2230
+ reportLines.push(`━━━━━━━━━━━━━━━━━━━━━━━━━`);
2231
+
2232
+ // Итоговый статус
2233
+ const tokens = loadTokens();
2234
+ const hasTokens = tokens.length > 0;
2235
+ const allOk = checks.every(c => c.status);
2236
+
2237
+ if (hasTokens && allOk) {
2238
+ reportLines.push(`✅ <b>Все системы работают</b>`);
2239
+ } else if (!hasTokens) {
2240
+ reportLines.push(`⚠️ <b>Режим ожидания архива</b>`);
2241
+ reportLines.push(`📦 Отправьте архив с сессиями`);
2242
+ } else {
2243
+ reportLines.push(`⚠️ <b>Есть проблемы</b>`);
2244
+ }
2245
+
2246
+ // Ссылки
2247
+ reportLines.push(`\n🌐 API: http://localhost:${process.env.PORT || 3264}`);
2248
+ reportLines.push(`📖 Docs: http://localhost:${process.env.PORT || 3264}/api`);
2249
+
2250
+ // Репозиторий
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
+
2256
+ // Справка
2257
+ reportLines.push(`\n💡 <b>Справка:</b>`);
2258
+ reportLines.push(`📝 Используйте /help для списка команд`);
2259
+ reportLines.push(`🔍 Используйте /status для проверки состояния`);
2260
+ reportLines.push(`🤖 Используйте /chat для включения LLM режима`);
2261
+
2262
+ const report = reportLines.join('\n');
2263
+ await sendMessage(chatId, report);
2264
+ } catch (error) {
2265
+ logError('Ошибка получения статуса', error);
2266
+ await sendMessage(chatId, '❌ Не удалось получить статус');
2267
+ }
2268
+ }
2269
+
2270
+ /**
2271
+ * Обработчик команды /restart
2272
+ */
2273
+ async function handleRestart(chatId) {
2274
+ await sendMessage(chatId, '🔄 Перезапуск сервиса...');
2275
+ await new Promise(resolve => setTimeout(resolve, 2000));
2276
+ await gracefulRestart(chatId);
2277
+ }
2278
+
2279
+ /**
2280
+ * Обрабатывает команду продления сессии
2281
+ */
2282
+ async function handleExtendSession(chatId) {
2283
+ try {
2284
+ // Импортируем функции для продления сессии
2285
+ const { initBrowser, shutdownBrowser, getBrowserContext } = await import('../browser/browser.js');
2286
+ const { extractAuthToken } = await import('../api/chat.js');
2287
+ const { loadSession, saveAuthToken } = await import('../browser/session.js');
2288
+ const { loadTokens, saveTokens } = await import('../api/tokenManager.js');
2289
+ const { CHAT_PAGE_URL } = await import('../config.js');
2290
+
2291
+ const tokens = loadTokens();
2292
+
2293
+ if (tokens.length === 0) {
2294
+ await sendMessage(chatId,
2295
+ '⚠️ <b>Нет аккаунтов</b>\n\n' +
2296
+ 'Сначала создайте сессию:\n' +
2297
+ '1. Запустите <code>npm run create-session-archive</code>\n' +
2298
+ '2. Или отправьте архив через бота'
2299
+ );
2300
+ return;
2301
+ }
2302
+
2303
+ // Фильтруем только действительные токены для продления
2304
+ 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;
2309
+ // Проверяем наличие cookies.json
2310
+ const cookiesPath = path.join(process.cwd(), SESSION_DIR, 'accounts', t.id, 'cookies.json');
2311
+ if (!fs.existsSync(cookiesPath)) return false;
2312
+ return true;
2313
+ });
2314
+
2315
+ const expiredCount = tokens.length - validTokens.length;
2316
+
2317
+ if (validTokens.length === 0) {
2318
+ await sendMessage(chatId,
2319
+ `⚠️ <b>Нет действительных токенов</b>\n\n` +
2320
+ `Все ${tokens.length} токенов истекли.\n\n` +
2321
+ 'Создайте новые сессии:\n' +
2322
+ '1. Запустите <code>npm run create-session-archive</code>\n' +
2323
+ '2. Или отправьте архив через бота'
2324
+ );
2325
+ return;
2326
+ }
2327
+
2328
+ let startMessage = `🔄 <b>Продление сессий...</b>\n\n` +
2329
+ `📊 Найдено аккаунтов: ${validTokens.length}`;
2330
+
2331
+ if (expiredCount > 0) {
2332
+ startMessage += ` (пропущено ${expiredCount} истекших)`;
2333
+ }
2334
+
2335
+ startMessage += `\n⏳ Это может занять несколько минут...\n` +
2336
+ `🕐 Примерное время: ~2-4 минуты на аккаунт`;
2337
+
2338
+ await sendMessage(chatId, startMessage);
2339
+
2340
+ let successCount = 0;
2341
+ let failCount = 0;
2342
+ const results = [];
2343
+
2344
+ for (const token of validTokens) {
2345
+ try {
2346
+ // Показываем прогресс
2347
+ const currentNum = results.length + 1;
2348
+ await sendMessage(chatId,
2349
+ `🔄 Обрабатываю аккаунт ${currentNum}/${tokens.length}...\n` +
2350
+ `👤 ${token.id}`
2351
+ );
2352
+
2353
+ // Пропускаем недействительные токены
2354
+ if (token.invalid) {
2355
+ results.push(`⏭️ ${token.id} - пропущен (недействителен)`);
2356
+ failCount++;
2357
+ continue;
2358
+ }
2359
+
2360
+ // Загружаем cookies для аккаунта
2361
+ const cookiesPath = path.join(process.cwd(), SESSION_DIR, 'accounts', token.id, 'cookies.json');
2362
+
2363
+ if (!fs.existsSync(cookiesPath)) {
2364
+ results.push(`❌ ${token.id} - нет cookies`);
2365
+ failCount++;
2366
+ logWarn(`Session extension failed for ${token.id}: cookies.json not found`);
2367
+ continue;
2368
+ }
2369
+
2370
+ const cookiesData = fs.readFileSync(cookiesPath, 'utf8');
2371
+ const cookies = JSON.parse(cookiesData);
2372
+
2373
+ // Открываем браузер в headless режиме
2374
+ const browserOk = await initBrowser(false, true);
2375
+
2376
+ if (!browserOk) {
2377
+ throw new Error('Не удалось открыть браузер');
2378
+ }
2379
+
2380
+ const ctx = getBrowserContext();
2381
+
2382
+ // Загружаем cookies
2383
+ if (ctx && typeof ctx.setCookie === 'function') {
2384
+ await ctx.setCookie(...cookies);
2385
+ }
2386
+
2387
+ // Переходим на Qwen для обновления сессии (3 минуты таймаут)
2388
+ await ctx.goto(CHAT_PAGE_URL, {
2389
+ waitUntil: 'domcontentloaded',
2390
+ timeout: 180000 // 3 минуты
2391
+ });
2392
+
2393
+ // Ждем загрузки страницы (1 минута для полной загрузки)
2394
+ await new Promise(resolve => setTimeout(resolve, 60000));
2395
+
2396
+ // Извлекаем новый токен
2397
+ const newToken = await extractAuthToken(ctx, true);
2398
+
2399
+ if (!newToken) {
2400
+ results.push(`❌ ${token.id} - не удалось получить токен`);
2401
+ failCount++;
2402
+ await shutdownBrowser();
2403
+ continue;
2404
+ }
2405
+
2406
+ // Сохраняем новый токен
2407
+ const tokenFile = path.join(process.cwd(), SESSION_DIR, 'accounts', token.id, 'token.txt');
2408
+ fs.writeFileSync(tokenFile, newToken, 'utf8');
2409
+ saveAuthToken(newToken);
2410
+
2411
+ // Обновляем tokens.json
2412
+ const tokenIndex = tokens.findIndex(t => t.id === token.id);
2413
+ if (tokenIndex !== -1) {
2414
+ tokens[tokenIndex].token = newToken;
2415
+ tokens[tokenIndex].resetAt = null;
2416
+ tokens[tokenIndex].invalid = false;
2417
+ tokens[tokenIndex].lastExtended = new Date().toISOString();
2418
+ saveTokens(tokens);
2419
+ }
2420
+
2421
+ // Сохраняем обновленные cookies
2422
+ const newCookies = await ctx.cookies();
2423
+ fs.writeFileSync(cookiesPath, JSON.stringify(newCookies, null, 2));
2424
+
2425
+ // Закрываем браузер
2426
+ await shutdownBrowser();
2427
+
2428
+ results.push(`✅ ${token.id} - продлен`);
2429
+ successCount++;
2430
+
2431
+ // Небольшая задержка между аккаунтами
2432
+ if (successCount < tokens.length) {
2433
+ await new Promise(resolve => setTimeout(resolve, 2000));
2434
+ }
2435
+
2436
+ } catch (error) {
2437
+ logError(`Ошибка продления ${token.id}`, error);
2438
+ results.push(`❌ ${token.id} - ${error.message}`);
2439
+ failCount++;
2440
+
2441
+ // Убеждаемся что браузер закрыт
2442
+ try {
2443
+ await shutdownBrowser();
2444
+ } catch (e) {
2445
+ // ignore
2446
+ }
2447
+ }
2448
+ }
2449
+
2450
+ // Формируем отчет
2451
+ let report = `📋 <b>Результат продления сессий</b>\n\n`;
2452
+ report += `✅ Успешно: ${successCount}\n`;
2453
+ report += `❌ Ошибки: ${failCount}\n\n`;
2454
+ report += `<b>Детали:</b>\n`;
2455
+ report += results.join('\n');
2456
+
2457
+ if (successCount > 0) {
2458
+ report += `\n\n🎉 Сессии продлены!`;
2459
+ }
2460
+
2461
+ if (failCount > 0 && successCount === 0) {
2462
+ report += `\n\n⚠️ Все сессии не удалось продлить.\n`;
2463
+
2464
+ // Check if the issue is missing cookies
2465
+ const missingCookiesCount = results.filter(r => r.includes('нет cookies')).length;
2466
+
2467
+ 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💡 Или отправьте архив с сессиями через бота`;
2474
+ } else {
2475
+ report += `\nВыполните: <code>npm run create-session-archive</code>`;
2476
+ }
2477
+ }
2478
+
2479
+ await sendMessage(chatId, report);
2480
+
2481
+ } catch (error) {
2482
+ logError('Ошибка при продлении сессий', error);
2483
+ await sendMessage(chatId,
2484
+ `❌ <b>Ошибка продления сессий</b>\n\n` +
2485
+ `Ошибка: ${error.message}\n\n` +
2486
+ `Попробуйте:\n` +
2487
+ `1. <code>npm run create-session-archive</code>\n` +
2488
+ `2. Или отправьте архив с сессиями`
2489
+ );
2490
+ }
2491
+ }
2492
+
2493
+ /**
2494
+ * Отправка плановой проверки всем админам
2495
+ */
2496
+ async function sendScheduledStatusToAdmins() {
2497
+ try {
2498
+ const telegramToken = process.env.TELEGRAM_BOT_TOKEN;
2499
+ if (!telegramToken) {
2500
+ return;
2501
+ }
2502
+
2503
+ const adminUserIds = TELEGRAM_USER_IDS;
2504
+
2505
+ if (adminUserIds.length === 0) {
2506
+ logInfo('Нет админов для отправки плановой проверки');
2507
+ return;
2508
+ }
2509
+
2510
+ logInfo(`Отправка плановой проверки ${adminUserIds.length} админам`);
2511
+
2512
+ for (const adminId of adminUserIds) {
2513
+ try {
2514
+ await sendStatusMessage(adminId, true);
2515
+ logInfo(`Плановая проверка отправлена админу ${adminId}`);
2516
+ } catch (error) {
2517
+ logError(`Ошибка отправки плановой проверки админу ${adminId}`, error);
2518
+ }
2519
+ }
2520
+ } catch (error) {
2521
+ logError('Ошибка отправки плановой проверки', error);
2522
+ }
2523
+ }
2524
+
2525
+ /**
2526
+ * Запуск периодической проверки каждые 4 часа
2527
+ */
2528
+ export function startPeriodicHealthCheck() {
2529
+ const FOUR_HOURS = 4 * 60 * 60 * 1000; // 4 часа в миллисекундах
2530
+
2531
+ logInfo(`Запуск периодической проверки здоровья каждые 4 часа`);
2532
+
2533
+ setInterval(async () => {
2534
+ logInfo('Выполняется плановая проверка здоровья...');
2535
+ await sendScheduledStatusToAdmins();
2536
+ }, FOUR_HOURS);
2537
+ }
2538
+
2539
+ /**
2540
+ * Отправляет сообщение в Telegram
2541
+ */
2542
+ async function sendMessage(chatId, text) {
2543
+ try {
2544
+ const url = `https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/sendMessage`;
2545
+ const response = await fetchWithProxy(url, {
2546
+ method: 'POST',
2547
+ headers: { 'Content-Type': 'application/json' },
2548
+ body: JSON.stringify({
2549
+ chat_id: chatId,
2550
+ text: text,
2551
+ parse_mode: 'HTML'
2552
+ })
2553
+ });
2554
+
2555
+ if (!response.ok) {
2556
+ const errorBody = await response.text();
2557
+ logError(`Ошибка отправки сообщения Telegram: ${errorBody}`);
2558
+ }
2559
+ } catch (error) {
2560
+ logError('Ошибка отправки сообщения в Telegram', error);
2561
+ }
2562
+ }
2563
+
2564
+ /**
2565
+ * Отправляет уведомление всем пользователям
2566
+ */
2567
+ export async function notifyAllUsers(message) {
2568
+ if (!TELEGRAM_BOT_TOKEN || TELEGRAM_USER_IDS.length === 0) {
2569
+ return false;
2570
+ }
2571
+
2572
+ for (const userId of TELEGRAM_USER_IDS) {
2573
+ await sendMessage(userId, message);
2574
+ }
2575
+
2576
+ return true;
2577
+ }
2578
+
2579
+ /**
2580
+ * Обработчик LLM чата - отправляет сообщение в Qwen API
2581
+ */
2582
+ async function handleLLMChat(chatId, userMessage) {
2583
+ try {
2584
+ // Показываем индикатор набора текста
2585
+ await sendChatAction(chatId, 'typing');
2586
+
2587
+ // Получаем или создаем контекст чата
2588
+ if (!chatContexts.has(chatId)) {
2589
+ chatContexts.set(chatId, []);
2590
+ }
2591
+ const context = chatContexts.get(chatId);
2592
+
2593
+ // Добавляем сообщение пользователя в контекст
2594
+ context.push({ role: 'user', content: userMessage });
2595
+
2596
+ // Ограничиваем контекст последними 20 сообщениями
2597
+ if (context.length > 20) {
2598
+ context.splice(0, context.length - 20);
2599
+ }
2600
+
2601
+ logInfo(`🤖 LLM Chat: Запрос от пользователя ${chatId}: ${userMessage.substring(0, 50)}...`);
2602
+
2603
+ // Отправляем запрос к Qwen API
2604
+ const apiUrl = `http://localhost:${process.env.PORT || 3264}/api/chat/completions`;
2605
+
2606
+ // Получаем модель для этого чата
2607
+ const model = getModelForChat(chatId);
2608
+
2609
+ const requestBody = {
2610
+ messages: [
2611
+ {
2612
+ role: 'system',
2613
+ content: 'You are a helpful assistant in a Telegram bot. Keep responses concise and clear. Respond in the same language as the user.'
2614
+ },
2615
+ ...context
2616
+ ],
2617
+ model: model,
2618
+ stream: false
2619
+ };
2620
+
2621
+ // Используем chatId Telegram как x-chat-id header для изоляции диалогов
2622
+ // Это позволяет API управлять контекстом вместо локального chatContexts
2623
+ const response = await fetchWithQwenProxy(apiUrl, {
2624
+ method: 'POST',
2625
+ headers: {
2626
+ 'Content-Type': 'application/json',
2627
+ 'x-chat-id': `telegram-${chatId}` // Уникальный ID для каждого Telegram чата
2628
+ },
2629
+ body: JSON.stringify(requestBody)
2630
+ });
2631
+
2632
+ if (!response.ok) {
2633
+ const errorText = await response.text();
2634
+
2635
+ // Логируем полную ошибку
2636
+ logError(`❌ LLM Chat: Qwen API error ${response.status}`, errorText);
2637
+
2638
+ // Пытаемся распарсить JSON для лучшего форматирования
2639
+ let errorJson;
2640
+ try {
2641
+ errorJson = JSON.parse(errorText);
2642
+ } catch {
2643
+ errorJson = { error: errorText };
2644
+ }
2645
+
2646
+ // Формируем сообщение об ошибке с полным JSON
2647
+ const escapedJson = escapeHtmlForCode(JSON.stringify(errorJson, null, 2));
2648
+ const errorMessage =
2649
+ `❌ <b>Ошибка Qwen API</b>\n\n` +
2650
+ `Статус: <code>${response.status}</code>\n\n` +
2651
+ `<b>Полный ответ:</b>\n` +
2652
+ `<pre>${escapedJson}</pre>\n\n` +
2653
+ `💡 Попробуйте еще раз или используйте /clear`;
2654
+
2655
+ // Отправляем ошибку (разбиваем если длинная)
2656
+ if (errorMessage.length > 4000) {
2657
+ const chunks = splitMessage(errorMessage, 4000);
2658
+ for (const chunk of chunks) {
2659
+ await sendMessage(chatId, chunk);
2660
+ }
2661
+ } else {
2662
+ await sendMessage(chatId, errorMessage);
2663
+ }
2664
+
2665
+ // Удаляем последнее сообщение пользователя из контекста (оно не было обработано)
2666
+ if (context.length > 0) {
2667
+ context.pop();
2668
+ }
2669
+
2670
+ return; // Выходим, не выбрасывая ошибку
2671
+ }
2672
+
2673
+ const data = await response.json();
2674
+
2675
+ // Извлекаем ответ
2676
+ const assistantMessage = data.choices?.[0]?.message?.content || 'Извините, я не смог обработать ваш запрос.';
2677
+
2678
+ // Добавляем ответ в контекст
2679
+ context.push({ role: 'assistant', content: assistantMessage });
2680
+
2681
+ // Отправляем ответ пользователю
2682
+ // Если сообщение длинное, разбиваем на части (Telegram limit: 4096 chars)
2683
+ if (assistantMessage.length > 4000) {
2684
+ const chunks = splitMessage(assistantMessage, 4000);
2685
+ for (const chunk of chunks) {
2686
+ await sendMessage(chatId, chunk);
2687
+ }
2688
+ } else {
2689
+ await sendMessage(chatId, assistantMessage);
2690
+ }
2691
+
2692
+ logInfo(`✅ LLM Chat: Ответ отправлен (${assistantMessage.length} символов)`);
2693
+
2694
+ } catch (error) {
2695
+ logError('❌ LLM Chat: Ошибка', error);
2696
+
2697
+ // Формируем сообщение об ошибке с деталями
2698
+ const escapedStack = escapeHtmlForCode(error.stack || 'Stack trace unavailable');
2699
+ const errorMessage =
2700
+ `❌ <b>Ошибка при обработке запроса</b>\n\n` +
2701
+ `<b>Тип:</b> ${error.name || 'Unknown'}\n` +
2702
+ `<b>Сообщение:</b> ${escapeHtml(error.message)}\n\n` +
2703
+ `<b>Стек:</b>\n` +
2704
+ `<pre>${escapedStack}</pre>\n\n` +
2705
+ `💡 Попробуйте еще раз или используйте /clear для очистки контекста.`;
2706
+
2707
+ // Отправляем ошибку (разбиваем если длинная)
2708
+ if (errorMessage.length > 4000) {
2709
+ const chunks = splitMessage(errorMessage, 4000);
2710
+ for (const chunk of chunks) {
2711
+ await sendMessage(chatId, chunk);
2712
+ }
2713
+ } else {
2714
+ await sendMessage(chatId, errorMessage);
2715
+ }
2716
+
2717
+ // Удаляем последнее сообщение пользователя из контекста
2718
+ if (context && context.length > 0) {
2719
+ context.pop();
2720
+ }
2721
+ }
2722
+ }
2723
+
2724
+ /**
2725
+ * Переключает режим LLM чата
2726
+ */
2727
+ async function toggleLLMChat(chatId) {
2728
+ // Проверяем есть ли аккаунты
2729
+ if (!hasAccounts()) {
2730
+ await sendMessage(chatId,
2731
+ `❌ <b>LLM чат недоступен</b>\n\n` +
2732
+ `🔒 Для работы AI ассистента нужны аккаунты\n` +
2733
+ `📦 Отправьте архив с сессиями через бота\n` +
2734
+ `💡 После загрузки аккаунтов функции будут доступны`
2735
+ );
2736
+ logInfo(`❌ LLM Chat запрошен, но аккаунты отсутствуют`);
2737
+ return;
2738
+ }
2739
+
2740
+ // Если LLM уже включен - выключаем, и наоборот
2741
+ llmChatEnabled = !llmChatEnabled;
2742
+
2743
+ // Сохраняем состояние LLM чата
2744
+ saveBotSettings({
2745
+ activeModel: activeModel,
2746
+ llmChatEnabled: llmChatEnabled
2747
+ });
2748
+
2749
+ if (llmChatEnabled) {
2750
+ // Инициализируем контекст чата
2751
+ if (!chatContexts.has(chatId)) {
2752
+ chatContexts.set(chatId, []);
2753
+ }
2754
+
2755
+ await sendMessage(chatId,
2756
+ `✅ <b>LLM чат включен!</b>\n\n` +
2757
+ `🤖 Теперь я отвечаю как AI ассистент.\n` +
2758
+ `📝 Модель: ${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 - Все команды бота`
2767
+ );
2768
+
2769
+ logInfo(`✅ LLM Chat включен для пользователя ${chatId} (сохранено)`);
2770
+ } else {
2771
+ await sendMessage(chatId,
2772
+ `❌ <b>LLM чат выключен</b>\n\n` +
2773
+ `🔧 Возвращен в режим управления ботом.\n` +
2774
+ `💾 Настройка сохранена\n` +
2775
+ `Используйте /togglechat чтобы включить снова.`
2776
+ );
2777
+
2778
+ logInfo(`❌ LLM Chat выключен для пользователя ${chatId} (сохранено)`);
2779
+ }
2780
+ }
2781
+
2782
+ /**
2783
+ * Показывает текущее состояние LLM чата
2784
+ */
2785
+ async function showLLMChatStatus(chatId) {
2786
+ const status = llmChatEnabled ? '✅ Включен' : '❌ Выключен';
2787
+ const model = getModelForChat(chatId);
2788
+ const context = chatContexts.get(chatId) || [];
2789
+
2790
+ await sendMessage(chatId,
2791
+ `📊 <b>Состояние LLM чата</b>\n\n` +
2792
+ `🔧 Статус: ${status}\n` +
2793
+ `🤖 Модель: <code>${model}</code>\n` +
2794
+ `💬 Сообщений в контексте: ${context.length}\n\n` +
2795
+ `💡 Используйте /togglechat чтобы ${llmChatEnabled ? 'выключить' : 'включить'} LLM чат`
2796
+ );
2797
+
2798
+ logInfo(`📊 Проверка статуса LLM чата: ${llmChatEnabled ? 'включен' : 'выключен'}`);
2799
+ }
2800
+
2801
+ /**
2802
+ * Обрабатывает команду /setmodel
2803
+ */
2804
+ async function handleSetModel(chatId, text) {
2805
+ const parts = text.trim().split(/\s+/);
2806
+
2807
+ // Если нет аргумента - показываем текущую модель
2808
+ if (parts.length < 2) {
2809
+ await showModelInfo(chatId);
2810
+ return;
2811
+ }
2812
+
2813
+ const requestedModel = parts[1].trim();
2814
+ const { getAvailableModelsFromFile } = await import('../api/chat.js');
2815
+ const availableModels = getAvailableModelsFromFile();
2816
+
2817
+ // Проверяем что модель существует
2818
+ if (!availableModels.includes(requestedModel)) {
2819
+ await sendMessage(chatId,
2820
+ `❌ <b>Модель не найдена</b>\n\n` +
2821
+ `Модель <code>${requestedModel}</code> не найдена в списке доступных.\n\n` +
2822
+ `<b>Используйте /model для списка доступных моделей</b>`
2823
+ );
2824
+ logWarn(`❌ Пользователь ${chatId} попытался установить несуществующую модель: ${requestedModel}`);
2825
+ return;
2826
+ }
2827
+
2828
+ // Устанавливаем глобальную активную модель
2829
+ activeModel = requestedModel;
2830
+
2831
+ // Сохраняем в файл
2832
+ const settings = loadBotSettings();
2833
+ settings.activeModel = requestedModel;
2834
+ saveBotSettings(settings);
2835
+
2836
+ await sendMessage(chatId,
2837
+ `✅ <b>Модель изменена!</b>\n\n` +
2838
+ `🤖 Новая модель: <code>${requestedModel}</code>\n` +
2839
+ `💬 Будет использоваться во всех чатах\n` +
2840
+ `💾 Настройка сохранена\n\n` +
2841
+ `💡 Для сброса используйте /clear`
2842
+ );
2843
+
2844
+ logInfo(`✅ Установлена глобальная модель: ${requestedModel} (сохранено)`);
2845
+ }
2846
+
2847
+ /**
2848
+ * Получает активную модель
2849
+ * Приоритет: activeModel из настроек > default из config
2850
+ */
2851
+ function getModelForChat(chatId) {
2852
+ // Если установлена активная модель, используем её
2853
+ if (activeModel) {
2854
+ return activeModel;
2855
+ }
2856
+
2857
+ // Возвращаем модель из настроек бота
2858
+ return getBotSettingsModel();
2859
+ }
2860
+
2861
+ /**
2862
+ * Получает глобальную активную модель (для использования в API)
2863
+ * Экспортируется для использования в routes.js
2864
+ * @returns {string|null} activeModel или null
2865
+ */
2866
+ export function getActiveModel() {
2867
+ return activeModel;
2868
+ }
2869
+
2870
+ /**
2871
+ * Показывает информацию о модели
2872
+ */
2873
+ async function showModelInfo(chatId) {
2874
+ const currentModel = getModelForChat(chatId);
2875
+ const context = chatContexts.get(chatId) || [];
2876
+ const { getAvailableModelsFromFile } = await import('../api/chat.js');
2877
+ const availableModels = getAvailableModelsFromFile();
2878
+
2879
+ const message =
2880
+ `📊 <b>Информация о модели</b>\n\n` +
2881
+ `🤖 Активная модель: <code>${currentModel}</code>\n` +
2882
+ `💬 Сообщений в контексте: ${context.length}\n` +
2883
+ `🔧 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`;
2889
+
2890
+ await sendMessage(chatId, message);
2891
+ }
2892
+
2893
+ /**
2894
+ * Очищает контекст чата
2895
+ */
2896
+ async function clearChatContext(chatId) {
2897
+ chatContexts.set(chatId, []);
2898
+
2899
+ await sendMessage(chatId,
2900
+ `🗑️ <b>Контекст чата очищен</b>\n\n` +
2901
+ `💬 Новая история чата начата.\n` +
2902
+ `🤖 LLM чат: ${llmChatEnabled ? '✅ Включен' : '❌ Выключен'}`
2903
+ );
2904
+
2905
+ logInfo(`🗑️ Контекст чата очищен для пользователя ${chatId}`);
2906
+ }
2907
+
2908
+ /**
2909
+ * Отправляет индикатор действия в Telegram
2910
+ */
2911
+ async function sendChatAction(chatId, action) {
2912
+ try {
2913
+ const url = `https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/sendChatAction`;
2914
+ await fetchWithProxy(url, {
2915
+ method: 'POST',
2916
+ headers: { 'Content-Type': 'application/json' },
2917
+ body: JSON.stringify({
2918
+ chat_id: chatId,
2919
+ action: action // 'typing', 'upload_photo', etc.
2920
+ })
2921
+ });
2922
+ } catch (error) {
2923
+ logWarn('Не удалось отправить chat action', error);
2924
+ }
2925
+ }
2926
+
2927
+ /**
2928
+ * Разбивает длинное сообщение на части
2929
+ */
2930
+ function splitMessage(text, maxLength) {
2931
+ const chunks = [];
2932
+ let remaining = text;
2933
+
2934
+ while (remaining.length > maxLength) {
2935
+ // Ищем подходящее место для разреза (конец строки или пробел)
2936
+ let splitIndex = remaining.lastIndexOf('\n', maxLength);
2937
+
2938
+ if (splitIndex === -1 || splitIndex < maxLength * 0.5) {
2939
+ splitIndex = remaining.lastIndexOf(' ', maxLength);
2940
+ }
2941
+
2942
+ if (splitIndex === -1) {
2943
+ splitIndex = maxLength;
2944
+ }
2945
+
2946
+ chunks.push(remaining.substring(0, splitIndex).trim());
2947
+ remaining = remaining.substring(splitIndex).trim();
2948
+ }
2949
+
2950
+ if (remaining) {
2951
+ chunks.push(remaining);
2952
+ }
2953
+
2954
+ return chunks;
2955
+ }
2956
+
2957
+ /**
2958
+ * Экранирует HTML специальные символы
2959
+ */
2960
+ function escapeHtml(text) {
2961
+ if (!text) return '';
2962
+ return String(text)
2963
+ .replace(/&/g, '&amp;')
2964
+ .replace(/</g, '&lt;')
2965
+ .replace(/>/g, '&gt;');
2966
+ }
2967
+
2968
+ /**
2969
+ * Экранирует текст для вставки в <pre> тег (только < и &)
2970
+ */
2971
+ function escapeHtmlForCode(text) {
2972
+ if (!text) return '';
2973
+ return String(text)
2974
+ .replace(/&/g, '&amp;')
2975
+ .replace(/</g, '&lt;')
2976
+ .replace(/>/g, '&gt;');
2977
+ }