User: привет
Assistant: Привет! Как я могу помочь тебе сегодня?
User: напиши историю сша подробно
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,
			delay: 1000,                // Задержка по умолчанию, например, 1000 мс
			responseTimes: []			// массив для хранения временных меток (начало и конец 3-х ответов)
        };
	}
	else
		clientActions[userId].connected = true;
	
	if (!limiters[userId]) { 
	  // Создаем экземпляр Bottleneck для каждого клиента (ограничение на количество запросов по Id)
		limiters[userId] = new Bottleneck({
            maxConcurrent: 1, // Максимальное количество одновременных задач
            minTime: 1000, // Минимальное время между запросами в миллисекундах
		    highWater: 0 // Не разрешать в очереди больше 0 запросов (игнорировать)
        });
    }
	
    ws.on('message', (message) => {
		// Обновляем время последнего запроса
		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++;
			
			// Задержка перед отправкой запроса
			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(() => {
				// Отправка запроса на сервер чата с задержкой
				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);
		}
    });
});

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

// Обновляем временные метки в 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 {
        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);
		}, 1 * 1500); // 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 {
                        // Изменение здесь: используем метод 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--;
    }
}

Если на обработку последних 3 сообщений у чата ушло более чем 15 секунд, то устанавливается задержка 20000 мс. Проблема в том, что пользователь может обойти это просто перезагрузив страницу. вебсокет соединение перезапустится и соответственно данные о клиенте будут удалены (ws.on('close', ()...), задержка будет снова равна 1. Как это решить, чтобы задержка сохранялась между переподключениями?
Assistant:  сохранить задержку между переподключениями WebSocket для конкретного пользователя, можно хранить значение задержки (delay) в базе данных или в Redis, а не только в памяти сервера. Это позволит вам восстанавливать состояние пользователя даже после разрыва соединения.

Вот как это можно реализовать:

1. **Сохранение задержки в Redis**: Вместо того чтобы хранить задержку только в `clientActions`, вы можете сохранить её в Redis с использованием уникального идентификатора пользователя. 

2. **Восстановление задержки при новом подключении**: При каждом новом подключении вы можете проверять Redis, чтобы получить значение задержки для этого пользователя.

Вот пример изменений, которые необходимо внести в ваш код:

### 1. Сохранение задержки в Redis

Как только вы обновляете задержку в `updateClientDelay`:

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

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

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

### 2. Восстановление задержки при новом подключении

Добавьте при подключении пользователю:

```javascript
wss.on('connection', async (ws, req) => {
    // ... ваш существующий код ...

    // Восстановление задержки из Redis при новом подключении
    const storedDelay = await getAsync(`delay:${userId}`);
    if (storedDelay) {
        clientActions[userId].delay = parseInt(storedDelay); // Устанавливаем задержку
    }

    // ... ваш существующий код ...
});
```

### Обработка отключений

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

```javascript
ws.on('close', () => {
    // ... ваш существующий код ...

    // Удаление delay из памяти, но мы оставляем его в Redis
    if (clientActions[userId]) {
        delete clientActions[userId]; // Удаление состояния клиента
    }

    // Уменьшаем счетчик подключений для этого IP
    // ... ваш существующий код ...
});
```

С этими изменениями ваша система будет сохранять задержку между переподключениями пользователя, что сделает ее более устойчивой к действиям пользователей и возможным разрывам соединения.
User: задача в том, чтобы задержка продолжала действовать, пока клиент отключен.
Assistant: