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) => {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(); 
            }
			// Удаляем записи о клиенте, если нет активности более 30 минут
            delete clientActions[userId]; 
            delete limiters[userId];
        }
    }
	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('Отправка ответа из кэша для Qwen');
        // Имитируем задержку перед отправкой ответа из кэша (например, 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 Обработка сообщений обратной связи --------

Этот файл лежит в папке sitename.ru\chat\API\ как должен быть настроен контекст server на nginx
Assistant:  настроить контекст сервера для вашего Node.js приложения в Nginx, вам нужно будет создать конфигурацию Nginx, которая будет проксировать запросы к вашему приложению. Предположим, что ваше приложение запущено на локальном сервере (localhost) на порту 3000. Убедитесь, что у вас установлен Nginx и он доступен.

Вот пример конфигурации Nginx для вашего Node.js приложения, расположенного в папке `sitename.ru/chat/API/`:

1. **Создайте новый файл конфигурации Nginx** для вашего сайта:

   Обычно файлы конфигурации находятся в директории `/etc/nginx/sites-available/`. Вы можете создать новый файл, например, `sitename.ru.conf`:

   ```bash
   sudo nano /etc/nginx/sites-available/sitename.ru.conf
   ```

2. **Добавьте следующую конфигурацию** в файл:

   ```nginx
   server {
       listen 80;
       server_name sitename.ru www.sitename.ru;

       location / {
           proxy_pass http://localhost:3000;  # Ваш Node.js сервер
           proxy_http_version 1.1;
           proxy_set_header Upgrade $http_upgrade;
           proxy_set_header Connection 'upgrade';
           proxy_set_header Host $host;
           proxy_cache_bypass $http_upgrade;
       }

       # Настройки для HTTPS
       listen 443 ssl; 
       ssl_certificate /path/to/your/certificate.crt; # Путь к вашему SSL сертификату
       ssl_certificate_key /path/to/your/private.key; # Путь к вашему приватному ключу

       location / {
           proxy_pass http://localhost:3000;  # Ваш Node.js сервер
           proxy_http_version 1.1;
           proxy_set_header Upgrade $http_upgrade;
           proxy_set_header Connection 'upgrade';
           proxy_set_header Host $host;
           proxy_cache_bypass $http_upgrade;
       }
   }
   ```

   Замените `/path/to/your/certificate.crt` и `/path/to/your/private.key` на фактические пути к вашему SSL сертификату и ключу.

3. **Создайте символическую ссылку** файла конфигурации в `sites-enabled` для активации:

   ```bash
   sudo ln -s /etc/nginx/sites-available/sitename.ru.conf /etc/nginx/sites-enabled/
   ```

4. **Проверьте конфигурацию на ошибки**:

   ```bash
   sudo nginx -t
   ```

5. **Перезапустите Nginx**, чтобы применить изменения:

   ```bash
   sudo systemctl restart nginx
   ```

Теперь Nginx будет проксировать запросы с `http://sitename.ru` и `https://sitename.ru` на ваш Node.js сервер, работающий на порту 3000.

### Для HTTPS:
Если вы еще не настроили сертификаты, вы можете использовать Let's Encrypt для создания бесплатного SSL-сертификата. Убедитесь, что Nginx открыт на порту 443 для HTTPS. 

### Примечания:
- Убедитесь, что у вашего Node.js приложения правильные настройки CORS, если ваш frontend и backend находятся на разных доменах или если вы используете разные порты.
- Если вы хотите работать с поддоменами или различными маршрутами, вам нужно будет соответствующим образом настроить дополнительные `location` блоки в конфигурации Nginx.
- Запускайте Node.js приложение через процесс-менеджер, например, `pm2`, чтобы оно автоматически перезапускалось в случае сбоя.
User: Зачем два раза блок
location / {
        proxy_pass http://localhost:3000;  # Ваш Node.js сервер
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection 'upgrade';
        proxy_set_header Host $host;
        proxy_cache_bypass $http_upgrade;
    }
Assistant:  вашем примере конфигурации Nginx блок `location /` повторяется дважды — один для порта 80 и другой для порта 443. Это делается для разделения настроек, так как два порта обслуживают разные протоколы (HTTP и HTTPS). 

Тем не менее, повторение одного и того же блока (с идентичными настройками) может быть избыточным. Вместо этого, вы можете просто определить его один раз, убрав различные `server` блоки, однако вам придется дублировать директивы `proxy_pass` в каждом из этих блоков. Вот как это можно сделать более аккуратно, объединив `location` блоки:

```nginx
server {
    listen 80;
    server_name sitename.ru www.sitename.ru;

    # Перенаправление HTTP на HTTPS
    return 301 https://$host$request_uri;
}

server {
    listen 443 ssl; 
    server_name sitename.ru www.sitename.ru;

    ssl_certificate /path/to/your/certificate.crt; # Путь к вашему SSL сертификату
    ssl_certificate_key /path/to/your/private.key; # Путь к вашему приватному ключу

    location / {
        proxy_pass http://localhost:3000;  # Ваш Node.js сервер
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection 'upgrade';
        proxy_set_header Host $host;
        proxy_cache_bypass $http_upgrade;
    }
}
```

Таким образом, при обращении к вашему сайту по HTTP (порт 80) автоматически перенаправляется на HTTPS (порт 443). Теперь блок `location /` определён только для блока HTTPS, но также нужно изменить конфигурацию так, чтобы ненужные дублирования не возникали. 

### Объяснение:
1. **HTTP блок (порт 80)**: Этот блок обрабатывает запросы на HTTP и перенаправляет их на HTTPS. Это важно для безопасности, так как HTTP соединения могут быть перехвачены.
  
2. **HTTPS блок (порт 443)**: Этот блок обрабатывает маршруты к вашему Node.js приложению и использует SSL/TLS для защищённой передачи данных.

Эта конфигурация является стандартной практикой в Nginx для обеспечения безопасного веб-сервиса.
User: теперь все запросы будут перенаправляться на http://localhost:3000, т.е. на node.js, даже index.html, который по сути является статичной html страницей?
Assistant: , если вы используете один и тот же блок `location /` для обработки всех запросов, включая статические файлы (например, `index.html`), все запросы будут перенаправляться на ваш Node.js сервер, запущенный на `http://localhost:3000`. Это может быть неэффективно, если вы хотите обслуживать статические файлы непосредственно через Nginx, так как обычно Nginx настроен для обработки статического контента более эффективно и быстро.

Чтобы обеспечить корректное обслуживание статических файлов, вы можете изменить конфигурацию Nginx так, чтобы запросы к статическим файлам (например, HTML, CSS, JS и изображениям) обрабатывались локально, а все остальные запросы (например, API) проксировались на Node.js. Вот пример, как это можно сделать:

```nginx
server {
    listen 80;
    server_name sitename.ru www.sitename.ru;

    # Перенаправление HTTP на HTTPS
    return 301 https://$host$request_uri;
}

server {
    listen 443 ssl; 
    server_name sitename.ru www.sitename.ru;

    ssl_certificate /path/to/your/certificate.crt; # Путь к вашему SSL сертификату
    ssl_certificate_key /path/to/your/private.key; # Путь к вашему приватному ключу

    # Определите каталог с вашими статическими файлами
    root /path/to/your/static/files; # Замените на реальный путь к диретории с вашими статическими файлами

    location / {
        try_files $uri $uri/ @nodejs; # Пробуем найти файл, если не найден, передаём на прокси
    }

    location @nodejs {
        proxy_pass http://localhost:3000;  # Ваш Node.js сервер
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection 'upgrade';
        proxy_set_header Host $host;
        proxy_cache_bypass $http_upgrade;
    }
}
```

### Объяснение изменений:
1. **`root /path/to/your/static/files;`**: Укажите путь к папке, где находятся ваши статические файлы (например, `index.html`, CSS и JS файлы).

2. **`try_files $uri $uri/ @nodejs;`**: Nginx сначала проверяет, существует ли запрашиваемый файл. Если запрашиваемый файл найден, Nginx отправляет его клиенту. Если файл не найден, запрос передаётся в блок `@nodejs`, который проксирует его на ваш Node.js сервер.

3. **Обработка запросов**: Теперь Nginx будет сначала пытаться обслуживать статические файлы, что повышает производительность и снижает нагрузку на Node.js, который будет обрабатывать только динамические запросы.

Эта конфигурация позволяет плавно сочетать статический и динамический контент, улучшая производительность вашего веб-приложения.
User: try_files $uri $uri/ @nodejs;: Nginx сначала проверяет, существует ли запрашиваемый файл. Если запрашиваемый файл найден, Nginx отправляет его клиенту. Если файл не найден, запрос передаётся в блок @nodejs, который проксирует его на ваш Node.js сервер.

Зачем? Если пользователь запрашивает несуществующую страницу надо перенаправлять его на кастомную страницу 404
Assistant:  абсолютно правы! Если пользователь запрашивает несуществующую страницу, гораздо лучше перенаправить его на кастомную страницу 404, чем просто проксировать запрос на сервер Node.js. Это обеспечивает лучшую пользовательскую опытность и позволяет вам показать удобное сообщение об ошибке.

Чтобы реализовать обработку ошибки 404 в Nginx, вы можете скорректировать конфигурацию следующим образом:

```nginx
server {
    listen 80;
    server_name sitename.ru www.sitename.ru;

    # Перенаправление HTTP на HTTPS
    return 301 https://$host$request_uri;
}

server {
    listen 443 ssl; 
    server_name sitename.ru www.sitename.ru;

    ssl_certificate /path/to/your/certificate.crt; # Путь к вашему SSL сертификату
    ssl_certificate_key /path/to/your/private.key; # Путь к вашему приватному ключу

    # Определите каталог с вашими статическими файлами
    root /path/to/your/static/files; # Замените на реальный путь к диретории с вашими статическими файлами

    location / {
        try_files $uri $uri/ =404; # Если файл не найден, возвращаем 404
    }

    location @nodejs {
        proxy_pass http://localhost:3000;  # Ваш Node.js сервер
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection 'upgrade';
        proxy_set_header Host $host;
        proxy_cache_bypass $http_upgrade;
    }

    error_page 404 /404.html;  # Указываем кастомную страницу 404
    location = /404.html {
        internal;  # Указываем, что эта страница доступна только из Nginx, а не непосредственно через клиент
    }
}
```

### Объяснение изменений:

1. **`try_files $uri $uri/ =404;`**: Если запрашиваемый файл существует, он будет возвращен клиенту. Если файл не найден, Nginx вернет код ошибки 404 вместо перенаправления на Node.js сервер.

2. **`error_page 404 /404.html;`**: Указывает Nginx, что использовать страницу `/404.html` для отображения ошибки 404. Вам нужно создать этот файл и поместить его в корневую директорию, указанную в `root`.

3. **`location = /404.html { internal; }`**: Эта настройка указывает, что страница 404 доступна только из внутренних перенаправлений Nginx, и не может быть запрошена непосредственно пользователем.

Теперь, если пользователь запрашивает несуществующую страницу, он увидит кастомизированную страницу 404, а не просто перенаправления на ваш Node.js сервер. Это позволит вам предоставить более удобный и целенаправленный интерфейс в случае ошибок.
User: location @nodejs
что это означает? Код в моем первом сообщении это файл call.js в папке sitename.ru\chat\API\, который обрабатывает вебсокет подключения и запросы от страницы index.html из папки sitename.ru\chat. Значит ли это что location @nodejs будет срабатывать, когда пользователь открывает этот index.html или отправляет сообщения в чат через него, т.е. когда в итоге выполняется call.js?
Assistant: