User: const { gpt, gptweb } = require("./API/gpt");
const { dalle } = require("./API/dalle");
const { gpt4o } = require("./API/gpt4o");
const { gpt3 } = require("./API/gpt3.5");
const { prompts } = require("./API/gpt-prompts");
const { blackbox } = require("./API/blackbox");
const { qwen } = require("./API/qwen");
const { gemini } = require("./API/gemini");
const WebSocket = require('ws');
const Bottleneck = require('bottleneck');
const { RateLimiterMemory } = require('rate-limiter-flexible');
const { stats, scheduleDailyReset, resetStatistics } = require('./stats'); // Импортируем модуль статистики
const fs = require('fs');
const path = require('path');
const Ajv = require('ajv');

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

// Настройки для ограничений по IP
const rateLimiter = new RateLimiterMemory({
    points: 20, // разрешается 10 запросов
    duration: 1, // за 1 секунду
	blockDuration: 60 // время блокировки в секундах
});

// Создайте WebSocket-сервер
const wss = new WebSocket.Server({ port: 3000 });

wss.on('connection', (ws, req) => {
	
	const urlParams = new URLSearchParams(req.url.split('?')[1]);
    const userId = urlParams.get('token'); // Получаем UUID из URL
    const userIP = req.socket.remoteAddress;
	const userHeaders = req.headers;

	// Проверяем Origin
    if (!checkOrigin(userHeaders.origin, ws)) {
        return; // Если отклонено, выходим из обработчика
    }
	
	// Увеличиваем общий счетчик подключений
    stats.totalConnections++;
	
	console.log('Client connected with user ID:', userId, 'Домен - ', userHeaders.origin);
	console.log('Общее количество подключений:', stats.totalConnections);
	
	// Инициализируем состояние клиента, если его еще нет
    if (!clientActions[userId]) {
      clientActions[userId] = {
          actions: [], // Массив для хранения действий
          connected: true, // Статус подключения
      };
	  
	  // Создаем экземпляр Bottleneck для каждого клиента (ограничение на количество запросов по Id)
      limiters[userId] = new Bottleneck({
          maxConcurrent: 1, // Максимальное количество одновременных задач
          minTime: 1000, // Минимальное время между запросами в миллисекундах
		  highWater: 0 // Не разрешать в очереди больше 0 запросов (игнорировать)
      });
    }

    ws.on('message', (message) => {
		// Проверяем, является ли сообщение экземпляром Buffer
		if (Buffer.isBuffer(message))
			message = message.toString(); 
		try {
			// Преобразуем строку JSON обратно в объект
			message = JSON.parse(message); 
			//Получаем историю чата из запроса
			let messages = message.messages;
			console.log('До обработки:', messages); 
			// Выполняем валидацию сообщений
			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);
			}
			console.log('После обработки:', messages); 
			// Увеличиваем общий счетчик запросов
            stats.totalRequests++;
			
			rateLimiter.consume(userIP)
                .then(() => {
					// Используем Bottleneck для управления количеством запросов
					limiters[userId].schedule(() => {
						// Отправка запроса на сервер чата
						return call(messages, ws);
					}).catch(error => {
						//console.error('Rate limit exceeded, message ignored:', error);
						const response = { code: 2, message: "Превышен лимит запросов. Пожалуйста, попробуйте позже." };
						// Отправка ответа клиенту
						sendMessage(ws, response);
					});
				})
                .catch(() => {
					// Увеличиваем счетчик блокировок по IP
                    stats.totalIPBlocks++;
                    const response = { code: 2, message: "Вы временно заблокированы за превышение лимита запросов." };
                    // Отправка ответа клиенту
					sendMessage(ws, response);
                });
			
		} catch (error) {
				console.error('Error parsing JSON:', error);
				const response = { code: 2, message: "Произошла ошибка. Попробуйте еще раз."};
				console.log(response);
				// Отправка ответа клиенту
				sendMessage(ws, response);
		}
		
    });

    ws.on('close', () => {
		clientActions[userId].connected = false; // Обновление статуса подключения
		delete clientActions[userId]; // Удаление состояния клиента
		delete limiters[userId]; // Удаление ограничителя для данного клиента
        console.log('Client ',userId, ' disconnected');
    });
});


async function call(messages, ws) {
    try {
        let fullResponse = ""; // Переменная для результата
		
		console.log('Запрос клиента:', messages); 
		//throw new Error("Намеренная ошибка для тестирования catch");
        gpt4o({
            messages: messages,
            markdown: false
        }, (result) => {
			try {
				if (!result.finish) {
					// Запоминаем только последний ответ чата (он будет полный, промежуточные ответы - это накопления)
					fullResponse = result.message || "";
					//console.log(result);
				} else {
					// Если завершено, выводим полный ответ
					let response = { code: 1, message: fullResponse};
					console.log('Ответ чата: ', response);
					// Отправка ответа клиенту
					sendMessage(ws, response);
				}
			}
			catch (callbackError) {
                console.error('Error processing callback result:', callbackError);
                let response = { code: 2, message: "Произогла ошибка. Попробуйте еще раз." };
                // Отправка ответа клиенту
				sendMessage(ws, response);
            }
        });
    } catch (error) {
        console.error('Error:', error);
		let response = { code: 2, message: "Произогла ошибка. Попробуйте еще раз."};
		// Отправка ответа клиенту
		sendMessage(ws, response);
    }
}

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

// Очистка старых соединений раз в минуту (по желанию)
setInterval(() => {
  Object.keys(clientActions).forEach(userId => {
    if (!clientActions[userId].connected) {
      delete clientActions[userId]; // Удаляем пользователя из памяти, если он отключен
    }
  });
}, 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(`Trying to send message to a closed WebSocket connection.`);
    }
}

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

    // Фильтруем массив, оставляя только корректные сообщения
    return messages.filter(message => ajv.validate(schema, message));
}

console.log('WebSocket сервер запущен через порт 3000');

Необходимо дополнительно для каждого клиента хранить время последнего сообщения от него в clientActions
Assistant:  того чтобы хранить время последнего сообщения от каждого клиента в объекте `clientActions`, вам нужно будет внести изменения в код обработчика сообщения. Мы можем добавить новое поле, например, `lastMessageTime`, в объект `clientActions` для каждого клиента и обновлять его каждый раз, когда клиент отправляет сообщение.

Внесем необходимые изменения в этом коде:

1. Добавляем новое поле `lastMessageTime` при инициализации клиента.
2. Обновляем это поле каждый раз, когда клиент отправляет сообщение.

Вот необходимые изменения в вашем коде:

```javascript
// Инициализируем состояние клиента, если его еще нет
if (!clientActions[userId]) {
    clientActions[userId] = {
        actions: [], // Массив для хранения действий
        connected: true, // Статус подключения
        lastMessageTime: new Date() // Время последнего сообщения
    };

    // Создаем экземпляр Bottleneck для каждого клиента (ограничение на количество запросов по Id)
    limiters[userId] = new Bottleneck({
        maxConcurrent: 1, // Максимальное количество одновременных задач
        minTime: 1000, // Минимальное время между запросами в миллисекундах
        highWater: 0 // Не разрешать в очереди больше 0 запросов (игнорировать)
    });
}

ws.on('message', (message) => {
    // Проверяем, является ли сообщение экземпляром Buffer
    if (Buffer.isBuffer(message))
        message = message.toString(); 
    try {
        // Преобразуем строку JSON обратно в объект
        message = JSON.parse(message); 
        // Получаем историю чата из запроса
        let messages = message.messages;
        console.log('До обработки:', messages); 
        // Выполняем валидацию сообщений
        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);
        }
        
        // Обновляем время последнего сообщения от клиента
        clientActions[userId].lastMessageTime = new Date();

        console.log('После обработки:', messages); 
        // Увеличиваем общий счетчик запросов
        stats.totalRequests++;
        
        rateLimiter.consume(userIP)
            .then(() => {
                // Используем Bottleneck для управления количеством запросов
                limiters[userId].schedule(() => {
                    // Отправка запроса на сервер чата
                    return call(messages, ws);
                }).catch(error => {
                    const response = { code: 2, message: "Превышен лимит запросов. Пожалуйста, попробуйте позже." };
                    // Отправка ответа клиенту
                    sendMessage(ws, response);
                });
            })
            .catch(() => {
                // Увеличиваем счетчик блокировок по IP
                stats.totalIPBlocks++;
                const response = { code: 2, message: "Вы временно заблокированы за превышение лимита запросов." };
                // Отправка ответа клиенту
                sendMessage(ws, response);
            });

    } catch (error) {
        console.error('Error parsing JSON:', error);
        const response = { code: 2, message: "Произошла ошибка. Попробуйте еще раз."};
        console.log(response);
        // Отправка ответа клиенту
        sendMessage(ws, response);
    }
});
```

Теперь у вас будет поле `lastMessageTime` для каждого клиента, которое обновляется каждый раз, когда клиент отправляет сообщение. Это позволит вам отслеживать время последнего сообщения от каждого пользователя.
User: Теперь перепиши эту часть

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

удаляй пользователя если соединение закрыто и последнее сообщение было больше часа назад
Assistant:  изменить логику очистки пользователей в том коде, который вы предоставили, вам нужно будет проверять не только состояние соединения, но и время последнего сообщения. Если соединение закрыто и последнее сообщение было отправлено более часа назад, пользователь будет удалён из `clientActions`.

Вот как можно переписать эту часть кода:

```javascript
// Очистка старых соединений раз в минуту
setInterval(() => {
    const currentTime = new Date();
    Object.keys(clientActions).forEach(userId => {
        const clientAction = clientActions[userId];
        if (!clientAction.connected && (currentTime - clientAction.lastMessageTime) > 60 * 60 * 1000) {
            // Если соединение закрыто и время последнего сообщения больше часа, удаляем пользователя из памяти
            delete clientActions[userId];
            console.log(`Пользователь ${userId} удален из памяти.`);
        }
    });
}, 60 * 1000); // Очистка каждые 60 секунд
```

### Объяснение изменений:
1. **Сохраняем текущее время:** Мы создаем переменную `currentTime` для хранения текущего времени.
2. **Проверяем условия:** Внутри `forEach` проверяем, соответствует ли условие: `!clientAction.connected` (соединение закрыто) и `(currentTime - clientAction.lastMessageTime) > 60 * 60 * 1000` (последнее сообщение было более часа назад).
3. **Удаляем пользователя:** Если оба условия соблюдены, мы удаляем пользователя из `clientActions` и выводим в консоль сообщение об удалении.

С этими изменениями ваш сервер теперь будет правильно очищать старые соединения, которые были отключены более часа назад.
User: const { gpt, gptweb } = require("./API/gpt");
const { dalle } = require("./API/dalle");
const { gpt4o } = require("./API/gpt4o");
const { gpt3 } = require("./API/gpt3.5");
const { prompts } = require("./API/gpt-prompts");
const { blackbox } = require("./API/blackbox");
const { qwen } = require("./API/qwen");
const { gemini } = require("./API/gemini");
const WebSocket = require('ws');
const Bottleneck = require('bottleneck');
const { RateLimiterMemory } = require('rate-limiter-flexible');
const { stats, scheduleDailyReset, resetStatistics } = require('./stats'); // Импортируем модуль статистики
const fs = require('fs');
const path = require('path');
const Ajv = require('ajv');

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

// Настройки для ограничений по IP
const rateLimiter = new RateLimiterMemory({
    points: 20, // разрешается 10 запросов
    duration: 1, // за 1 секунду
	blockDuration: 60 // время блокировки в секундах
});

// Создайте WebSocket-сервер
const wss = new WebSocket.Server({ port: 3000 });

wss.on('connection', (ws, req) => {
	
	const urlParams = new URLSearchParams(req.url.split('?')[1]);
    const userId = urlParams.get('token'); // Получаем UUID из URL
    const userIP = req.socket.remoteAddress;
	const userHeaders = req.headers;

	// Проверяем Origin
    if (!checkOrigin(userHeaders.origin, ws)) {
        return; // Если отклонено, выходим из обработчика
    }
	
	// Увеличиваем общий счетчик подключений
    stats.totalConnections++;
	
	console.log('Client connected with user ID:', userId, 'Домен - ', userHeaders.origin);
	console.log('Общее количество подключений:', stats.totalConnections);
	
	// Инициализируем состояние клиента, если его еще нет
    if (!clientActions[userId]) {
      clientActions[userId] = {
          actions: [], // Массив для хранения действий
          connected: true, // Статус подключения
		  lastMessageTime: new Date() // Время последнего сообщения
      };
	  
	  // Создаем экземпляр Bottleneck для каждого клиента (ограничение на количество запросов по Id)
      limiters[userId] = new Bottleneck({
          maxConcurrent: 1, // Максимальное количество одновременных задач
          minTime: 1000, // Минимальное время между запросами в миллисекундах
		  highWater: 0 // Не разрешать в очереди больше 0 запросов (игнорировать)
      });
    }

    ws.on('message', (message) => {
		// Проверяем, является ли сообщение экземпляром Buffer
		if (Buffer.isBuffer(message))
			message = message.toString(); 
		try {
			// Преобразуем строку JSON обратно в объект
			message = JSON.parse(message); 
			//Получаем историю чата из запроса
			let messages = message.messages;
			// Выполняем валидацию сообщений
			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);
			}
			
			// Обновляем время последнего сообщения от клиента
			clientActions[userId].lastMessageTime = new Date();
			
			// Увеличиваем общий счетчик запросов
            stats.totalRequests++;
			
			rateLimiter.consume(userIP)
                .then(() => {
					// Используем Bottleneck для управления количеством запросов
					limiters[userId].schedule(() => {
						// Отправка запроса на сервер чата
						return call(messages, ws);
					}).catch(error => {
						//console.error('Rate limit exceeded, message ignored:', error);
						const response = { code: 2, message: "Превышен лимит запросов. Пожалуйста, попробуйте позже." };
						// Отправка ответа клиенту
						sendMessage(ws, response);
					});
				})
                .catch(() => {
					// Увеличиваем счетчик блокировок по IP
                    stats.totalIPBlocks++;
                    const response = { code: 2, message: "Вы временно заблокированы за превышение лимита запросов." };
                    // Отправка ответа клиенту
					sendMessage(ws, response);
                });
			
		} catch (error) {
				console.error('Error parsing JSON:', error);
				const response = { code: 2, message: "Произошла ошибка. Попробуйте еще раз."};
				console.log(response);
				// Отправка ответа клиенту
				sendMessage(ws, response);
		}
		
    });

    ws.on('close', () => {
		clientActions[userId].connected = false; // Обновление статуса подключения
		//delete clientActions[userId]; // Удаление состояния клиента
		//delete limiters[userId]; // Удаление ограничителя для данного клиента
        console.log('Client ',userId, ' disconnected');
    });
});


async function call(messages, ws) {
    try {
        let fullResponse = ""; // Переменная для результата
		
		console.log('Запрос клиента:', messages); 
		//throw new Error("Намеренная ошибка для тестирования catch");
        gpt4o({
            messages: messages,
            markdown: false
        }, (result) => {
			try {
				if (!result.finish) {
					// Запоминаем только последний ответ чата (он будет полный, промежуточные ответы - это накопления)
					fullResponse = result.message || "";
					//console.log(result);
				} else {
					// Если завершено, выводим полный ответ
					let response = { code: 1, message: fullResponse};
					console.log('Ответ чата: ', response);
					// Отправка ответа клиенту
					sendMessage(ws, response);
				}
			}
			catch (callbackError) {
                console.error('Error processing callback result:', callbackError);
                let response = { code: 2, message: "Произогла ошибка. Попробуйте еще раз." };
                // Отправка ответа клиенту
				sendMessage(ws, response);
            }
        });
    } catch (error) {
        console.error('Error:', error);
		let response = { code: 2, message: "Произогла ошибка. Попробуйте еще раз."};
		// Отправка ответа клиенту
		sendMessage(ws, response);
    }
}

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

// Очистка старых соединений раз в минуту
setInterval(() => {
    const currentTime = new Date();
    Object.keys(clientActions).forEach(userId => {
        const clientAction = clientActions[userId];
        if (!clientAction.connected && (currentTime - clientAction.lastMessageTime) > 60 * 60 * 1000) {
            // Если соединение закрыто и время последнего сообщения больше часа, удаляем пользователя из памяти
            delete clientActions[userId];
			delete limiters[userId]; 
            console.log(`Пользователь ${userId} удален из памяти.`);
        }
    });
}, 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(`Trying to send message to a closed WebSocket connection.`);
    }
}

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

    // Фильтруем массив, оставляя только корректные сообщения
    return messages.filter(message => ajv.validate(schema, message));
}

console.log('WebSocket сервер запущен через порт 3000');

Проверь, особенно на возможность утечки памяти
Assistant: