User: привнт
Assistant: Привет! Как я могу помочь?
User: 0|server  | TypeError: Cannot read properties of undefined (reading 'processing')
0|server  |     at WebSocket.<anonymous> (C:\OSPanel\domains\gpt.loc\chat\API\call.js:163:70)
0|server  |     at WebSocket.emit (node:events:518:28)
0|server  |     at Receiver.receiverOnMessage (C:\OSPanel\domains\gpt.loc\chat\API\node_modules\ws\lib\websocket.js:1220:20)
0|server  |     at Receiver.emit (node:events:518:28)
0|server  |     at Receiver.dataMessage (C:\OSPanel\domains\gpt.loc\chat\API\node_modules\ws\lib\receiver.js:596:14) 0|server  |     at Receiver.getData (C:\OSPanel\domains\gpt.loc\chat\API\node_modules\ws\lib\receiver.js:496:10)
0|server  |     at Receiver.startLoop (C:\OSPanel\domains\gpt.loc\chat\API\node_modules\ws\lib\receiver.js:167:16)
0|server  |     at Receiver._write (C:\OSPanel\domains\gpt.loc\chat\API\node_modules\ws\lib\receiver.js:94:10)
0|server  |     at writeOrBuffer (node:internal/streams/writable:564:12)
0|server  |     at _write (node:internal/streams/writable:493:10)

что это значит?
Assistant: Ошибка, которую вы видите, означает, что код пытается получить доступ к свойству `processing` объекта, который в данный момент не определён. Это может произойти по нескольким причинам:

1. **Проблема с инициализацией:** Объект, откуда вы пытаетесь получить свойство, не был инициализирован или был объявлен, но не присвоен.

2. **Ошибка в логике:** Возможно, логика вашего кода неверна, и в определённый момент времени объект не содержит ожидаемого значения.

Чтобы устранить эту ошибку, вы можете:

- Проверить, существует ли объект, перед тем как обращаться к его свойству. Например, можете использовать условие:
  ```javascript
  if (object && object.processing) {
      // Логика, когда object.processing существует
  }
  ```

- Просмотреть часть кода, связанную с `WebSocket`, чтобы убедиться, что все необходимые данные инициализируются корректно перед тем, как их использовать.

- Добавить отладочную информацию (например, `console.log`), чтобы отследить значения, которые принимают переменные до момента возникновения ошибки.

Если у вас есть конкретные вопросы по коду, я могу помочь с их решением!
User: Вот сам код

//const { gpt, gptweb } = require("./API/gpt");
//const { dalle } = require("./API/dalle");
const { gpt4o } = require("c:/PrivateAPI/API/gpt4o");
//const { gpt3 } = require("./API/gpt3.5");
//const { prompts } = require("./API/gpt-prompts");
const { blackbox } = require("c:/PrivateAPI/API/blackbox");
const { qwen } = require("c:/PrivateAPI/API/qwen");
const { gemini } = require("c:/PrivateAPI/API/gemini");
const WebSocket = require('ws');
const Bottleneck = require('bottleneck');
const { stats, scheduleDailyReset, resetStatistics } = require('./stats'); // Импортируем модуль статистики
const fs = require('fs');
const path = require('path');
const Ajv = require('ajv');
const axios = require('axios');
const https = require('https');
const redis = require('redis');
const { promisify } = require('util');
const express = require('express');
const cors = require('cors'); // Импортируем cors 


// Обработка необработанных исключений (помогут выявить обшибки, которые не обрабатываются блоками try/catch) 
process.on('uncaughtException', (err) => {
    console.error('Необработанное исключение:', err);
});

process.on('unhandledRejection', (reason, promise) => {
    console.error('Необработанное отклонение промиса:', reason);
});

// Текущее количество подключенных клиентов
let currentConnectedClients = 0;

// Текущее количество обрабатываемых запросов
let currentGPT4oProcessingRequests = 0;
let currentGeminiProcessingRequests = 0; 
let currentQwenProcessingRequests = 0;  
let currentBlackboxProcessingRequests = 0;   

// Объект для хранения количества подключений по IP
const ipConnectionCount = {};

// Максимальное количество подключений с одного IP
const MAX_CONNECTIONS_PER_IP = 100;

// Создание и настройка клиента Redis
const redisClient = redis.createClient();
// Подключаем клиента Redis
redisClient.connect().catch(err => {
    console.error('Ошибка подключения к Redis:', err);
});
redisClient.on('error', (err) => {
    console.error('Ошибка Redis:', err);
});

// Промис-обертка для команды get
const getAsync = async (key) => {
	if (!redisClient.isOpen) { // Проверяем, открыт ли клиент
        console.error('Клиент Redis не подключён, невозможно выполнить get:', key);
        return null; // Возвращаем null, если клиент не подключен
    }
    try {
        return await redisClient.get(key);
    } catch (error) {
        console.error('Ошибка при получении из Redis:', error);
        throw error; //throw error чтобы выше поймали
    }
};

// SSL сертификаты
const serverOptions = {
	key: fs.readFileSync('C:\\OSPanel\\userdata\\config\\cert_files\\gpt.loc-key.pem'), // Приватный ключ
    cert: fs.readFileSync('C:\\OSPanel\\userdata\\config\\cert_files\\gpt.loc.pem'), // Сертификат
};

// HTTPS сервер
const httpsServer = https.createServer(serverOptions);

// Создайте WebSocket-сервер, используя существующий HTTPS сервер
const wss = new WebSocket.Server({ server: httpsServer });

// Запустите сервер
httpsServer.listen(3000, () => {
    console.log('HTTPS сервер и WebSocket сервер запущены на порту 3000');
});

// Создаем экземпляр Ajv (проверка входящей переписки на соответствие схеме)
const ajv = new Ajv(); 
// Объект для хранения действий и состояния каждого клиента
const clientActions = {};
// Объект для хранения ограничителей по каждому клиенту
const limiters = {}; 
// Домен
const allowedOrigin = 'https://gpt.loc'; 

wss.on('connection', (ws, req) => {
	//console.log('Новое соединение:', req.socket.remoteAddress);
	const urlParams = new URLSearchParams(req.url.split('?')[1]);
    const userId = urlParams.get('token'); // Получаем UUID из URL
    const userIP = req.socket.remoteAddress;
	//console.log("IP-адрес - ", userIP, "Время подключения - ", new Date().toISOString());
	const userHeaders = req.headers;
	
	// Проверяем Origin
    if (!checkOrigin(userHeaders.origin, ws)) {
        return; // Если отклонено, выходим из обработчика
    }

	// Если количество подключений с этого IP превышает лимит, закрываем соединение
    if (ipConnectionCount[userIP] >= MAX_CONNECTIONS_PER_IP) {
        console.log(`Превышен лимит подключений для IP: ${userIP}, отказано в соединении.`);
		stats.totalIPBlocks++;
        ws.close(); // Закрываем соединение
        return; // Выходим из обработчика 
    }
	
	// Увеличиваем счетчик подключений
    currentConnectedClients++;
	
	// Обновляем максимальное количество клиентов, если текущее количество больше 
	if (currentConnectedClients > stats.maxConnectedClients) {
        stats.maxConnectedClients = currentConnectedClients;
        console.log(`Максимальное количество подключенных клиентов: ${stats.maxConnectedClients}`);
    }
	
    // Увеличиваем счетчик подключений для этого IP
    ipConnectionCount[userIP] = (ipConnectionCount[userIP] || 0) + 1;
	
	// Увеличиваем общий счетчик подключений
    stats.totalConnections++;
	
	console.log('Client connected with user ID:', userId, ' and IP:', userIP);
	
	// Инициализируем состояние клиента, если его еще нет
    if (!clientActions[userId]) {
		clientActions[userId] = {
			actions: [], // Массив для хранения действий 
			connected: true, // Статус подключения
			lastRequestTime: new Date(), // Добавляем время последнего запроса 
			ws: ws,
			processing: false,
			delay: 1000,                // Задержка по умолчанию, например, 1000 мс
			responseTimes: []			// массив для хранения временных меток (начало и конец 3-х ответов)
        };
	}
	else {
        // обновляем ws при переподключении
        clientActions[userId].connected = true; 
        clientActions[userId].ws = ws;
        // вы можете обновить другие параметры здесь
    }
	
	if (!limiters[userId]) { 
	  // Создаем экземпляр Bottleneck для каждого клиента (ограничение на количество запросов по Id)
		limiters[userId] = new Bottleneck({
            maxConcurrent: 1, // Максимальное количество одновременных задач
            minTime: 1000, // Минимальное время между запросами в миллисекундах
		    highWater: 0 // Не разрешать в очереди больше 0 запросов (игнорировать)
        });
    }
	
    ws.on('message', (message) => {
		if (clientActions[userId])
			console.log("Клиент существует");
		else
			console.log("Клиент не существует");
		console.log(clientActions[userId].processing);
		// Обновляем время последнего запроса
		clientActions[userId].lastRequestTime = new Date(); 
		
		// Проверяем, является ли сообщение экземпляром Buffer
		if (Buffer.isBuffer(message))
			message = message.toString(); 
		try {
			// Преобразуем строку JSON обратно в объект
			message = JSON.parse(message); 
			//Получаем историю чата из запроса
			let messages = message.messages;
			let model = message.model;
			let rewrite = message.rewrite;
			
			// Выполняем валидацию сообщений
			messages = validateMessages(messages);
			
			// Проверяем, остались ли сообщения после валидации
			if (messages.length === 0) {
				const response = { code: 2, message: "Нет валидных сообщений для обработки." };
				sendMessage(ws, response);
				return;
			}
			
			// Урезаем последнее сообщение до 2000 символов, если оно от клиента
			if (messages[messages.length - 1].role === 'user') {
				messages[messages.length - 1].content = messages[messages.length - 1].content.substring(0, 2000000);
			}
			
			// Увеличиваем общий счетчик запросов  
            stats.totalRequests++;
			
			// Проверяем не обрабатывается ли в данный момент запрос клиента 
			if(clientActions[userId].processing){
					const responseTimes = clientActions[userId].responseTimes;
					let lastEndTime;

					// Проверяем, существует ли time end последнего ответа
					if (responseTimes.length > 0 && responseTimes[responseTimes.length - 1].end) {
						lastEndTime = responseTimes[responseTimes.length - 1].end;
					} else {
						lastEndTime = new Date(); // Присваиваем текущее время, если нет предыдущего завершения
					}
					const currentTime = new Date();
					const timeSinceLastEnd = currentTime - lastEndTime;
					// Если с момента последнего ответа прошло больше 40 секунд, сбрасываем флаг
					if (timeSinceLastEnd > 40000) {
						clientActions[userId].processing = false;
				}
			else{
					// Увеличиваем счетчик ошибок 
					stats.totalProcessingErrors++;
					console.log('Обрабатывается предыдущий запрос.');
					const response = { code: 2, message: "Сервер занят. Пожалуйста, попробуйте позже." };
					// Отправка ответа клиенту
					setTimeout(() => {
						sendMessage(ws, response);
					}, 5 * 1000);
					return;
				}
			}
			
			
			
			// Задержка перед отправкой запроса
			const responseTimes = clientActions[userId].responseTimes; // Получаем массив responseTimes

			// Проверяем, прошло ли больше 15 секунд с момента окончания последнего ответа
			if (responseTimes.length === 3) {
				const lastEndTime = responseTimes[responseTimes.length - 1].end;
				const currentTime = new Date();

				// Вычисляем разницу во времени
				const timeSinceLastEnd = currentTime - lastEndTime; // Время в миллисекундах

				if (timeSinceLastEnd > 20000) { // Если прошло больше 15 секунд
					clientActions[userId].delay = 1000; // Устанавливаем delay на 1 мс
				}
			}
			console.log('Задержка - ', clientActions[userId].delay)
			// Используем Bottleneck для управления количеством запросов
			limiters[userId].schedule(() => {
				clientActions[userId].processing = true;
				// Отправка запроса на сервер чата с задержкой
				setTimeout(() => {
					call(messages, model, ws, userId, rewrite);
				}, clientActions[userId].delay);
			}).catch(error => {
				stats.totalIPBlocks++;
				//console.error('Rate limit exceeded, message ignored:', error);
				//const response = { code: 2, message: "Превышен лимит запросов. Пожалуйста, попробуйте позже." };
				// Отправка ответа клиенту
				//sendMessage(ws, response);
			});
			
		} catch (error) {
				stats.totalWsMessErrors++;
				console.error('Error parsing JSON:', error);
				const response = { code: 2, message: "Произошла ошибка. Попробуйте еще раз."};
				console.log(response);
				// Отправка ответа клиенту
				sendMessage(ws, response);
		}
		
    });

    ws.on('close', () => {
		try {
			// Проверка, существует ли userId в clientActions
			if (clientActions[userId]) {
				clientActions[userId].connected = false; // Обновление статуса подключения
				//delete clientActions[userId]; // Удаление состояния клиента
			}
			// Проверка, существует ли userId в limiters
			if (limiters[userId]) {
				//delete limiters[userId]; // Удаление ограничителя для данного клиента
			}
			console.log('Client ',userId, ' disconnected'); 
			// Уменьшаем счетчик подключений для этого IP		
			if (ipConnectionCount[userIP] !== undefined) {
				ipConnectionCount[userIP]--;
				
				// Если счетчик равен 0, удаляем запись из объекта
				if (ipConnectionCount[userIP] === 0) {
					delete ipConnectionCount[userIP];
				}
			}
			// Уменьшаем счетчик при отключении клиента
			currentConnectedClients = Math.max(currentConnectedClients - 1, 0); 
		} catch (error) {
			console.error('Error handling client disconnect:', error);
		}
    });
});

// Запускаем планировщик для статистики
scheduleDailyReset();

// Таймер для проверки клиентов каждые 3 минут 
setInterval(() => {
    const currentTime = new Date();
    for (const userId in clientActions) {
        const client = clientActions[userId];
        const lastRequestTime = client.lastRequestTime;
		
		// Проверяем, прошло ли более 30 минут с последнего сообщения
        const longTimeInactive = lastRequestTime && (currentTime - lastRequestTime) > 30 * 60 * 1000;
        
        // Проверка на отключение и прошло ли более минуты с последнего сообщения
        const userDisconnectedAndInactive = !client.connected && (currentTime - lastRequestTime) > 3 * 60 * 1000;
		
        // Проверяем, прошло ли более 30 минут с последнего сообщения 
        if (longTimeInactive || userDisconnectedAndInactive) {
            const response = { code: 3, message: "" };
            // Отправляем сообщение клиенту 
            if (client.ws && client.ws.readyState === WebSocket.OPEN) {
                sendMessage(client.ws, response);
                // Закрываем соединение
                client.ws.close(); 
				console.log("Соединение закрыто по таймеру");
            }
			// Удаляем записи о клиенте, если нет активности более 30 минут
            delete clientActions[userId]; 
            delete limiters[userId];
			console.log("Клиент удален по таймеру");
        }
    }
	console.log('Количество активных клиентов - ', Object.keys(clientActions).length);
}, 3 * 60 * 1000); // Проверяем каждые 10 минут

// Очистка старых соединений раз в минуту (по желанию)
/*setInterval(() => {
  Object.keys(clientActions).forEach(userId => {
    if (!clientActions[userId].connected) {
      delete clientActions[userId]; // Удаляем пользователя из памяти, если он отключен
	  delete limiters[userId];
    }
  });
  console.log('Количество активных клиентов - ', Object.keys(clientActions).length);
}, 10 * 60 * 1000);*/ // Очистка каждые 10 минут

// Код выводит текущие метрики использования памяти приложения
setInterval(() => {
  const memoryUsage = process.memoryUsage();
  console.log(`Текущие метрики использования памяти: 
    RSS: ${memoryUsage.rss / (1024 * 1024)} MB,
    Heap Total: ${memoryUsage.heapTotal / (1024 * 1024)} MB,
    Heap Used: ${memoryUsage.heapUsed / (1024 * 1024)} MB,
    External: ${memoryUsage.external / (1024 * 1024)} MB`);
}, 60 * 60 * 1000); // Вывод каждый 60 секунд


// Проверяет с какого домена пришел запрос
function checkOrigin(origin, socket) {
    if (origin !== allowedOrigin) {
        console.log('Недопустимый домен. Соединение разорвано: ', origin);
        socket.close(); // Закрываем соединение, если домен не разрешен
        return false; // Возвращаем false для индикатора отклонения
    }
    return true; // Возвращаем true, если домен разрешен
}

// Отправляет ответ клиенту
function sendMessage(ws, message) {
    if (ws.readyState === WebSocket.OPEN) {
        ws.send(JSON.stringify(message));
    } else {
        console.warn(`Соединение закрыто. Сообщение не отправлено.`);
    }
}

// Удаление из массива сообщений не соответствующих схеме
function validateMessages(messages) {
    // Определяем схему для сообщений
    const schema = {
        type: 'object',
        properties: {
            role: { type: 'string' },
            content: { type: 'string' }
        },
        required: ['role', 'content'],
        additionalProperties: false
    };

    // Фильтруем массив, оставляя только корректные сообщения
    return messages.filter(message => {
        // Проверяем, соответствует ли сообщение схеме
        if (ajv.validate(schema, message)) {
            // Обрезаем содержимое, если оно длиннее 2000 символов
            if (message.content.length > 2000000) {
                message.content = message.content.substring(0, 2000000);
            }
            return true; // Сообщение валидно, оставляем в массиве
        }
        return false; // Сообщение не валидно, исключаем из массива
    });
}

// Слуйчайное число
function getRandomInRange(min, max) {
    return Math.floor(Math.random() * (max - min + 1)) + min;
}

// Обновляем временные метки в responseTimes
function updateResponseTimes(userId, startTime, endTime) {
    try {
        const responseTime = { start: startTime, end: endTime };

        // Получаем массив responseTimes
        const responseTimes = clientActions[userId].responseTimes;

        // Добавляем новую временную метку
        responseTimes.push(responseTime);

        // Удаляем старые временные метки, если их больше трех
        if (responseTimes.length > 3) {
            responseTimes.shift(); // Удаляем старую временную метку
        }

        // Обновляем задержку на основе средней продолжительности ответов
        updateClientDelay(userId);

        console.log(clientActions[userId].responseTimes);
    } catch (error) {
        console.error('Ошибка при обновлении временных меток:', error);
        // Можно добавить дополнительные действия в случае ошибки, например отправить уведомление
    }
}

// Проверяем длительность последних трех ответов и устанавливаем новое значение delay
function updateClientDelay(userId) {
    try {
        const responseTimes = clientActions[userId].responseTimes;

        if (responseTimes.length < 3) { 
            clientActions[userId].delay = 1000; // Устанавливаем delay на 1000 мс
            return; // Если еще нет ответов, ничего не делаем
        }
        
        // Проверяем, все ли три ответа дольше 15 секунд
        const allResponsesLongerThan15s = responseTimes.every(time => {
            const duration = time.end - time.start; // Вычисляем продолжительность
            return duration > 15000; // Проверяем, больше ли 15 секунд
        });

        // Устанавливаем delay в зависимости от проверки 
        if (allResponsesLongerThan15s) {
            clientActions[userId].delay = 20000; // Устанавливаем delay на 20000 мс
        } else {
            clientActions[userId].delay = 1000; // Устанавливаем delay на 1000 мс
        }
    } catch (error) {
        console.error('Ошибка при обновлении задержки клиента:', error);
        // Можно добавить дополнительные действия в случае ошибки, например установить задержку по умолчанию
        clientActions[userId].delay = 1000; // Пример установки по умолчанию
    }
}



// Вызов модели

async function call(messages, model, ws, userId, rewrite){
	if (model == 'gpt-4o')
		return callGPT4o(messages, ws, userId, rewrite);
	else if (model == 'gemini-pro')
		return callGemini(messages, ws, userId, rewrite);
	else if (model == 'qwen')
		return callQwen(messages, ws, userId, rewrite);
	else if (model == 'blackbox')
		return callBlackbox(messages, ws, userId, rewrite);
	else
		return callGPT4o(messages, ws, userId, rewrite);
}







//============== ФУНКЦИИ API МОДЕЛЕЙ ===============================================
//==================================================================================
//==================================================================================
//==================================================================================

// GPT4o
async function callGPT4o(messages, ws, userId, rewrite) {
	const lastMessageContent = messages[messages.length - 1].content; // Используем только последнее сообщение
    const cacheKey = `gpt4o:${userId}:${lastMessageContent}`; // Создаем ключ кэша на основе сообщения, модели и userId
	
	// Записываем время начала ответа
    const startTime = new Date();

    // Пробуем получить ответ из кэша 
    let cachedResponse;
    try {//throw new Error("Намеренная ошибка для тестирования catch");
        cachedResponse = await getAsync(cacheKey);
    } catch (error) {
        console.error('Ошибка при получении из Redis:', error);
        // Переходим к запросу по API
    }
	
    if (cachedResponse && !rewrite) {
        console.log('Отправка ответа из кэша для GPT4o');
        // Имитируем задержку перед отправкой ответа из кэша (например, 1 с)
		setTimeout(() => {
			sendMessage(ws, JSON.parse(cachedResponse));
			// Записываем время окончания ответа
            const endTime = new Date();
            updateResponseTimes(userId, startTime, endTime);
			clientActions[userId].processing = false;
		}, 2 * 1000); // 2 секунды задержки
        return;
    }
	
	// Увеличиваем счетчик запросов находящихся в обработке
    currentGPT4oProcessingRequests++;

    // Обновляем максимальное количество обрабатываемых запросов, если текущее больше
    if (currentGPT4oProcessingRequests > stats.maxGPT4oProcessingRequests) {
        stats.maxGPT4oProcessingRequests = currentGPT4oProcessingRequests;
        console.log(`Новое максимальное количество одновременно обрабатываемых запросов к GPT4o: ${stats.maxGPT4oProcessingRequests}`);
    }
	
    try {//throw new Error("Намеренная ошибка для тестирования catch");
        console.log('Запрос клиента к GPT4o: ', messages[messages.length - 1]); 
        let fullResponse = "";

        await new Promise((resolve, reject) => {
            gpt4o({ messages, markdown: false }, (result) => {
                if (!result.finish) {
                    fullResponse = result.message || "";
                } else {
					if(fullResponse == '')
						stats.totalGPT4oLimitStops++;
                    let responseMessage = fullResponse || 'Попробуйте написать позже или выберите другую модель.';
                    console.log('Ответ GPT4o:', { code: 1, message: responseMessage });
					try {//throw new Error("Намеренная ошибка для тестирования catch");
                        // Изменение здесь: используем метод set для установки TTL (в секундах)
                        redisClient.set(cacheKey, JSON.stringify({ code: 1, message: responseMessage }), { EX: 10 * 60 });
                    } catch (error) {
                        console.error('Ошибка при сохранении в Redis:', error);
                    }
                    resolve({ code: 1, message: responseMessage });
					stats.totalGPT4oResponse++;
					sendMessage(ws, { code: 1, message: responseMessage });
					
					// Записываем время окончания ответа
                    const endTime = new Date();
                    updateResponseTimes(userId, startTime, endTime);
                }
            });
        });
    } catch (error) {
		stats.totalGPT4oMessErrors++;
        console.error('Error:', error);
		sendMessage(ws, { code: 2, message: "Произошла ошибка. Попробуйте еще раз." });
    }finally {
        // Уменьшаем счетчик независимо от результата
        currentGPT4oProcessingRequests--;
		clientActions[userId].processing = false; // Сбрасываем флаг обработки в false
    }
}

// Gemini
async function callGemini(messages, ws, userId, rewrite) {
	const lastMessageContent = messages[messages.length - 1].content; // Используем только последнее сообщение
    const cacheKey = `gemini:${userId}:${lastMessageContent}`; // Создаем ключ кэшана основе сообщения, модели и userId
	
	// Записываем время начала ответа
    const startTime = new Date();

    // Попробуйте получить ответ из кэша 
    let cachedResponse;
    try {
        cachedResponse = await getAsync(cacheKey);
    } catch (error) {
        console.error('Ошибка при получении из Redis:', error);
        // Переходим к запросу по API
    }

    if (cachedResponse && !rewrite) {
        console.log('Отправка ответа из кэша для Gemini');
        // Имитируем задержку перед отправкой ответа из кэша (например, 1 с)
		setTimeout(() => {
			sendMessage(ws, JSON.parse(cachedResponse));
			// Записываем время окончания ответа
            const endTime = new Date();
            updateResponseTimes(userId, startTime, endTime);
			clientActions[userId].processing = false;
		}, 1 * 1500); // 2 секунды задержки
        return;
    }
	
	// Увеличиваем счетчик запросов находящихся в обработке
    currentGeminiProcessingRequests++;

    // Обновляем максимальное количество обрабатываемых запросов, если текущее больше
    if (currentGeminiProcessingRequests > stats.maxGeminiProcessingRequests) {
        stats.maxGeminiProcessingRequests = currentGeminiProcessingRequests;
        console.log(`Новое максимальное количество одновременно обрабатываемых запросов к Gemini: ${stats.maxGeminiProcessingRequests}`);
    }
	
    try {//throw new Error("Намеренная ошибка для тестирования catch");
        console.log('Запрос клиента к Gemini: ', messages[messages.length - 1]); 
        let fullResponse = "";

        await new Promise((resolve, reject) => {
            gemini({ messages }, (result) => {
                if (!result.finish) {
                    fullResponse = result.message || "";
                } else {
					if(fullResponse == '')
						stats.totalGeminiLimitStops++;
                    let responseMessage = fullResponse || 'Попробуйте написать позже или выберите другую модель.';
                    console.log('Ответ Gemini:', { code: 1, message: responseMessage });
					try {
                        // Изменение здесь: используем метод set для установки TTL
                        redisClient.set(cacheKey, JSON.stringify({ code: 1, message: responseMessage }), { EX: 10 * 60 });
                    } catch (error) {
                        console.error('Ошибка при сохранении в Redis:', error);
                    }
                    resolve({ code: 1, message: responseMessage });
					stats.totalGeminiResponse++;
					sendMessage(ws, { code: 1, message: responseMessage });
					
					// Записываем время окончания ответа
                    const endTime = new Date();
                    updateResponseTimes(userId, startTime, endTime); 
                }
            });
        });
    } catch (error) {
		stats.totalGeminiMessErrors++;
        console.error('Error:', error);
		sendMessage(ws, { code: 2, message: "Произошла ошибка. Попробуйте еще раз." });
    }finally {
        // Уменьшаем счетчик независимо от результата
        currentGeminiProcessingRequests--;
		clientActions[userId].processing = false;
    }
}

// Qwen
async function callQwen(messages, ws, userId, rewrite) {
	const lastMessageContent = messages[messages.length - 1].content; // Используем только последнее сообщение
    const cacheKey = `qwen:${userId}:${lastMessageContent}`; // Создаем ключ кэшана основе сообщения, модели и userId
	
	// Записываем время начала ответа
    const startTime = new Date();

    // Попробуйте получить ответ из кэша 
    let cachedResponse;
    try {
        cachedResponse = await getAsync(cacheKey);
    } catch (error) {
        console.error('Ошибка при получении из Redis:', error);
        // Переходим к запросу по API
    }

    if (cachedResponse && !rewrite) {
        console.log('Отправка ответа из кэша для Qwen');
        // Имитируем задержку перед отправкой ответа из кэша (например, 1 с)
		setTimeout(() => {
			sendMessage(ws, JSON.parse(cachedResponse));
			// Записываем время окончания ответа
            const endTime = new Date();
            updateResponseTimes(userId, startTime, endTime);
			clientActions[userId].processing = false;
		}, 1 * 1500); // 2 секунды задержки
        return;
    }
	
	// Увеличиваем счетчик запросов находящихся в обработке
    currentQwenProcessingRequests++;

    // Обновляем максимальное количество обрабатываемых запросов, если текущее больше
    if (currentQwenProcessingRequests > stats.maxQwenProcessingRequests) {
        stats.maxQwenProcessingRequests = currentQwenProcessingRequests;
        console.log(`Новое максимальное количество одновременно обрабатываемых запросов к Qwen: ${stats.maxQwenProcessingRequests}`);
    }
	
    try {//throw new Error("Намеренная ошибка для тестирования catch");
        console.log('Запрос клиента к Qwen: ', messages[messages.length - 1]); 
        let fullResponse = "";

        await new Promise((resolve, reject) => {
            qwen({ messages, markdown: false }, (result) => {
                if (!result.finish) {
                    fullResponse = result.message || "";
                } else {
					if(fullResponse == '')
						stats.totalQwenLimitStops++;
                    let responseMessage = fullResponse || 'Попробуйте написать позже или выберите другую модель.';
                    console.log('Ответ Qwen:', { code: 1, message: responseMessage });
					try {
                        // Изменение здесь: используем метод set для установки TTL
                        redisClient.set(cacheKey, JSON.stringify({ code: 1, message: responseMessage }), { EX: 10 * 60 });
                    } catch (error) {
                        console.error('Ошибка при сохранении в Redis:', error);
                    }
                    resolve({ code: 1, message: responseMessage });
					stats.totalQwenResponse++;
					sendMessage(ws, { code: 1, message: responseMessage });
					
					// Записываем время окончания ответа
                    const endTime = new Date();
                    updateResponseTimes(userId, startTime, endTime); 
                }
            });
        });
    } catch (error) {
		stats.totalQwenMessErrors++;
        console.error('Error:', error);
		sendMessage(ws, { code: 2, message: "Произошла ошибка. Попробуйте еще раз." });
    }finally {
        // Уменьшаем счетчик независимо от результата
        currentQwenProcessingRequests--;
		clientActions[userId].processing = false;
    }
}

// Blackbox
async function callBlackbox(messages, ws, userId, rewrite) {
	const lastMessageContent = messages[messages.length - 1].content; // Используем только последнее сообщение
    const cacheKey = `qwen:${userId}:${lastMessageContent}`; // Создаем ключ кэшана основе сообщения, модели и userId
	
	// Записываем время начала ответа
    const startTime = new Date();

    // Попробуйте получить ответ из кэша 
    let cachedResponse;
    try {
        cachedResponse = await getAsync(cacheKey);
    } catch (error) {
        console.error('Ошибка при получении из Redis:', error);
        // Переходим к запросу по API
    }

    if (cachedResponse) {
        console.log('Отправка ответа из кэша для Blackbox');
        // Имитируем задержку перед отправкой ответа из кэша (например, 1 с)
		setTimeout(() => {
			sendMessage(ws, JSON.parse(cachedResponse));
			// Записываем время окончания ответа
            const endTime = new Date();
            updateResponseTimes(userId, startTime, endTime);
			clientActions[userId].processing = false;
		}, 1 * 1500); // 2 секунды задержки
        return;
    }
	
	// Увеличиваем счетчик запросов находящихся в обработке
    currentBlackboxProcessingRequests++;

    // Обновляем максимальное количество обрабатываемых запросов, если текущее больше
    if (currentBlackboxProcessingRequests > stats.maxBlackboxProcessingRequests) {
        stats.maxBlackboxProcessingRequests = currentBlackboxProcessingRequests;
        console.log(`Новое максимальное количество одновременно обрабатываемых запросов к Blackbox: ${stats.maxBlackboxProcessingRequests}`);
    }
	
    try {//throw new Error("Намеренная ошибка для тестирования catch");
        console.log('Запрос клиента к Blackbox: ', messages[messages.length - 1]); 
        let fullResponse = "";

        await new Promise((resolve, reject) => {
            blackbox({ messages, markdown: false, websearch: false, llama: false }, (result) => {
                if (!result.finish) {
                    fullResponse = result.message || "";
                } else {
					if(fullResponse == '')
						stats.totalBlackboxLimitStops++;
                    let responseMessage = fullResponse || 'Попробуйте написать позже или выберите другую модель.';
                    console.log('Ответ Blackbox:', { code: 1, message: responseMessage });
					try {
                        // Изменение здесь: используем метод set для установки TTL
                        redisClient.set(cacheKey, JSON.stringify({ code: 1, message: responseMessage }), { EX: 10 * 60 });
                    } catch (error) {
                        console.error('Ошибка при сохранении в Redis:', error);
                    }
                    resolve({ code: 1, message: responseMessage });
					stats.totalBlackboxResponse++;
					sendMessage(ws, { code: 1, message: responseMessage });
					
					// Записываем время окончания ответа
                    const endTime = new Date();
                    updateResponseTimes(userId, startTime, endTime); 
                }
            });
        });
    } catch (error) {
		stats.totalBlackboxMessErrors++;
        console.error('Error:', error);
		sendMessage(ws, { code: 2, message: "Произошла ошибка. Попробуйте еще раз." });
    }finally {
        // Уменьшаем счетчик независимо от результата
        currentBlackboxProcessingRequests--;
		clientActions[userId].processing = false;
    }
}

//============== ВЫЗОВ API МОДЕЛЕЙ (КОНЕЦ) ======================









// ======== КОД ДЛЯ ПОЛУЧЕНИЯ ДОПОЛНИТЕЛЬНЫХ ЗАПРОСОВ ОТ ДРУГИХ СЕРВЕРОВ ПУТЕМ БАЛЛАНСИРОВКИ =======
// ...

//const app = express();
//const PORT = 3001;

// Увеличиваем лимит на 200kb (лимит на размер данных, которые можно получать через express (по умолчанию 100Кб))
//app.use(bodyParser.json({ limit: '200kb' })); //(200Кб ~ 200000 символов)
//app.use(bodyParser.urlencoded({ limit: '200kb', extended: true }));





// -------- Обработка сообщений обратной связи --------

const app = express();
const PORT = 3002;

// Настройки CORS
const corsOptions = {
    origin: 'https://gpt.loc', // Разрешаем только этот домен
    methods: ['GET', 'POST'], // Разрешаем только GET и POST запросы
    credentials: true, // Разрешаем использование куки (если нужно)
};

// Использование cors с опциями
app.use(cors(corsOptions));

// Middleware для парсинга JSON
app.use(express.json());

const TELEGRAM_BOT_TOKEN = '<TRUFFLEHOG_REDACTED_TELEGRAMBOTTOKEN>'; // Ваш токен бота
const TELEGRAM_CHAT_ID = '-1002303094611'; // ID вашей группы или чата

// Роут для обработки POST-запроса от клиента
app.post('/feedback', async (req, res) => {
	
	// Получаем IP-адрес отправителя
    const ipAddress = req.ip; // Это IP-адрес клиента
	
    let { feedback, email } = req.body; // Извлечение feedback и email из тела запроса
	
	// Очищаем входящие данные
    feedback = sanitizeInput(feedback);
    email = sanitizeInput(email);

    // Проверяем, что оба поля присутствуют
    if (!feedback || !email) {
        return res.status(400).send({ message: 'Feedback и Email обязательны.' });
    }
	
	// Проверяем корректность email
    if (!isValidEmail(email)) {
        return res.status(400).send({ message: 'Некорректный Email.' });
    }

    // Здесь можно добавить логическую обработку, например, сохранение в базе данных

    //console.log("Полученные данные:");
    //console.log("Отзыв:", feedback);
    //console.log("Email:", email);
	//console.log("IP:", ipAddress); 
	
	// Формируем сообщение для отправки в Telegram 
    const message = `Получено сообщение от ${email}:\n${feedback}`;
	
	// Отправка сообщения в Telegram
    axios.post(`https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/sendMessage`, {
        chat_id: TELEGRAM_CHAT_ID,
        text: message,
    })
    .then(response => {//throw new Error("Намеренная ошибка для тестирования catch");
        console.log('Сообщение от пользователя отправлено в Telegram.');
        res.status(200).json({ message: 'Ok', feedback: feedback, email: email });
    })
    .catch(error => {
        console.error('Ошибка при отправке сообщения в Telegram:', error);
        res.status(500).send({ message: 'Ошибка при отправке сообщения в Telegram.' });
    });

});

// Запуск сервера с HTTPS
https.createServer(serverOptions, app).listen(PORT, () => {
    console.log(`Сервер запущен на https://localhost:${PORT}`);
});

// Функция для очистки входящих данных
function sanitizeInput(input) {
    return input
        .replace(/<\/?[^>]+(>|$)/g, "") // Убираем HTML-теги
        .replace(/&lt;/g, "<")         // Заменяем HTML-коды
        .replace(/&gt;/g, ">")
        .trim();                        // Убираем лишние пробелы
}

// Функция для проверки email
function isValidEmail(email) {
    const emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; // Регулярное выражение для проверки email
    return emailPattern.test(email);
}

// -------- END Обработка сообщений обратной связи --------

ошибка возникает на строке
console.log(clientActions[userId].processing);

Assistant: , которую вы сталкиваетесь, происходит на строке `console.log(clientActions[userId].processing);`, и судя по всему, проблема заключается в том, что `clientActions[userId]` не определен (т.е. `undefined`). Это происходит потому, что переменной `userId` может не соответствовать ни одному ключу в объекте `clientActions`.

В вашем коде, вы инициализируете `clientActions[userId]` только в том случае, если этот объект еще не создан:

```javascript
if (!clientActions[userId]) {
    clientActions[userId] = {
        actions: [],
        connected: true,
        lastRequestTime: new Date(),
        ws: ws,
        processing: false,
        delay: 1000,
        responseTimes: []
    };
}
```

Если по какой-то причине `userId` неверен или отсутствует в `clientActions` на момент вызова `console.log`, вы получите ошибку доступа к свойствам `undefined`.

### Решение:
1. **Проверьте, существует ли `clientActions[userId]`:** Перед тем как обратиться к его свойствам, добавьте проверку на его существование.
   
    ```javascript
    if (clientActions[userId]) {
        console.log(clientActions[userId].processing);
    } else {
        console.error(`clientActions[${userId}] не существует.`);
    }
    ```

2. **Убедитесь, что `userId` правильный:** Входящий `userId` может оказаться невалидным. Возможно, нужно добавить логи для отслеживания значения `userId` в точках, где происходит его получение.

3. **Проверьте логику обработчиков сообщений:** Если вы вне контекста `wss.on('connection', ...)`, он может не устанавливать `clientActions[userId]`, и когда WebSocket отправляет сообщение, код не находит нужный объект.

### Пример исправления:

Вот как вы можете изменить обработчик для сообщений WebSocket:

```javascript
ws.on('message', (message) => {
    console.log("Получено сообщение от клиента");
    
    // Проверяем пользователя
    if (!clientActions[userId]) {
        console.error(`clientActions[${userId}] не существует. Соединение может быть неправильно инициализировано!`);
        return; // Ранний выход
    }
  
    // Теперь безопасно обращаться к свойствам клиента
    console.log(clientActions[userId].processing);

    // Остальная часть вашего кода
});
```

С таким подходом, вы избегаете возможных ошибок и можете отследить, когда и почему `clientActions[userId]` может отсутствовать.
User: может проблема тут возникает?
setInterval(() => {
    const currentTime = new Date();
    for (const userId in clientActions) {
        const client = clientActions[userId];
        const lastRequestTime = client.lastRequestTime;
		
		// Проверяем, прошло ли более 30 минут с последнего сообщения
        const longTimeInactive = lastRequestTime && (currentTime - lastRequestTime) > 30 * 60 * 1000;
        
        // Проверка на отключение и прошло ли более минуты с последнего сообщения
        const userDisconnectedAndInactive = !client.connected && (currentTime - lastRequestTime) > 3 * 60 * 1000;
		
        // Проверяем, прошло ли более 30 минут с последнего сообщения 
        if (longTimeInactive || userDisconnectedAndInactive) {
            const response = { code: 3, message: "" };
            // Отправляем сообщение клиенту 
            if (client.ws && client.ws.readyState === WebSocket.OPEN) {
                sendMessage(client.ws, response);
                // Закрываем соединение
                client.ws.close(); 
				console.log("Соединение закрыто по таймеру");
            }
			// Удаляем записи о клиенте, если нет активности более 30 минут
            delete clientActions[userId]; 
            delete limiters[userId];
			console.log("Клиент удален по таймеру");
        }
    }
	console.log('Количество активных клиентов - ', Object.keys(clientActions).length);
}, 3 * 60 * 1000); // Проверяем каждые 10 минут
Assistant: , описанный вами блок кода может быть источником проблемы. Давайте разберем его!

В коде, который вы привели, используются `setInterval` и цикл `for ... in`, чтобы проверить каждый объект в `clientActions`. Если клиент неактивен более 30 минут или отключен более минуты, вы удаляете запись о клиенте из `clientActions` и `limiters`:

```javascript
if (longTimeInactive || userDisconnectedAndInactive) {
    const response = { code: 3, message: "" };
    // Отправляем сообщение клиенту 
    if (client.ws && client.ws.readyState === WebSocket.OPEN) {
        sendMessage(client.ws, response);
        // Закрываем соединение
        client.ws.close(); 
        console.log("Соединение закрыто по таймеру");
    }
    // Удаляем записи о клиенте, если нет активности более 30 минут
    delete clientActions[userId]; 
    delete limiters[userId];
    console.log("Клиент удален по таймеру");
}
```

Когда вы удаляете запись о клиенте из `clientActions`, эта запись больше не доступна, и если в дальнейшем код попытается обратиться к `clientActions[userId]`, возникнет ошибка.

### Возможные способы решения проблемы:

1. **Проверяйте существование клиента перед доступом к его свойствам**:
    Везде, где вы обращаетесь к свойствам `clientActions[userId]`, добавьте проверку существования:

    ```javascript
    if (clientActions[userId]) {
        console.log(clientActions[userId].processing);
    } else {
        console.error(`Клиент с userId ${userId} больше не существует.`);
    }
    ```

2. **Измените механизм удаления клиента**:
   Вместо того, чтобы сразу удалять клиента из `clientActions`, вы можете пометить его как "удалённый" (например, добавив свойство `removed: true`) и проверять это свойство в других местах вашего кода.

3. **Проверяйте и удаляйте записи корректно**:
    Когда вы удаляете клиента из `clientActions`, убедитесь, что логика обработки сообщений правильно обрабатывает случаи, когда клиента уже нет. Например, возможно стоит использовать дополнительную проверку на наличие клиента внутри обработчика сообщений.

### Пример кода с проверками:

```javascript
ws.on('message', (message) => {
    if (!clientActions[userId]) {
        console.warn(`Сообщение от отключенного клиента: ${userId}`);
        return; // Ранний выход, если клиента нет
    }

    // Дальше обрабатываем сообщение
    console.log(clientActions[userId].processing);
    // Остальная обработка...
});
```

Этот подход позволит избежать ошибок обращения к свойствам несуществующих объектов и улучшит стабильность вашего приложения.
User: Т.е. может возникнуть ситуация когда исполняется код
// Удаляем записи о клиенте, если нет активности более 30 минут
            delete clientActions[userId]; 
            delete limiters[userId];

но не исполняется код (в результате которого клиент видит, что соединение разорвано)
// Отправляем сообщение клиенту 
            if (client.ws && client.ws.readyState === WebSocket.OPEN) {
                sendMessage(client.ws, response);
                // Закрываем соединение
                client.ws.close(); 
				console.log("Соединение закрыто по таймеру");
            }
Так? И в этом проблема?
Assistant: