User: Проанализируй весь код на предмет правильности работы сбора статистики
call.js

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 { 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'; 

// Создайте 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;
	//console.log("IP-адрес - ", userIP, "Время подключения - ", new Date().toISOString());
	const userHeaders = req.headers;

	// Проверяем Origin
    if (!checkOrigin(userHeaders.origin, ws)) {
        return; // Если отклонено, выходим из обработчика
    }
	
	// Увеличиваем общий счетчик подключений
    stats.totalConnections++;
	
	console.log('Client connected with user ID:', userId, 'Домен - ', userHeaders.origin);
	
	// Инициализируем состояние клиента, если его еще нет
    if (!clientActions[userId]) {
		clientActions[userId] = {
			actions: [], // Массив для хранения действий
			connected: true, // Статус подключения
        };
	}
	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) => {
		// Проверяем, является ли сообщение экземпляром Buffer
		if (Buffer.isBuffer(message))
			message = message.toString(); 
		try {
			// Преобразуем строку JSON обратно в объект
			message = JSON.parse(message); 
			//Получаем историю чата из запроса
			let messages = message.messages;
			let model = message.model;
			// Выполняем валидацию сообщений
			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++;
			
			// Используем Bottleneck для управления количеством запросов
			limiters[userId].schedule(() => {
				// Отправка запроса на сервер чата
				if (model == 'gpt-4o')
					return callGPT4o(messages, ws);
				else if (model == 'gemini-pro')
					return callGemini(messages, ws);
				else if (model == 'qwen')
					return callQwen(messages, ws);
				else if (model == 'blackbox')
					return callBlackbox(messages, ws);
				else if (model == 'gpt-3.5-turbo')
					return callGPT35Turbo(messages, ws);
				else
					return callGPT4o(messages, ws);
			}).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', () => {
		clientActions[userId].connected = false; // Обновление статуса подключения
		delete clientActions[userId]; // Удаление состояния клиента
		delete limiters[userId]; // Удаление ограничителя для данного клиента
        console.log('Client ',userId, ' disconnected');
    });
});

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

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


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');

















//============== ВЫЗОВ API МОДЕЛЕЙ =================================================
//==================================================================================
//==================================================================================
//==================================================================================

// GPT4o
async function callGPT4o(messages, ws) {
    try {
        let fullResponse = ""; // Переменная для результата
		
		console.log('Запрос клиента к GPT4o:', messages[messages.length - 1]); 
		//throw new Error("Намеренная ошибка для тестирования catch");
		let messageSent = false;
        gpt4o({
            messages: messages,
            markdown: false
        }, (result) => {
			// Обработка результата внутри колбэка
            if (result.error) {
                throw new Error(result.error); // Обрабатываем ошибку, если она есть
            }
			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);
				if (!messageSent) { // Отправляем сообщение об ошибке только если еще не отправили ответ
                    stats.totalGPT4oCallbackErrors++;
                    let errorResponse = { code: 2, message: "Произошла ошибка. Попробуйте еще раз." };
                    sendMessage(ws, errorResponse);
                    messageSent = true; // Обновляем флаг, чтобы сообщение больше не отправлялось
                }
            }
        });
    } catch (error) {
		stats.totalGPT4oMessErrors++;
        console.error('Error:', error);
		let response = { code: 2, message: "Произошла ошибка. Попробуйте еще раз."};
		// Отправка ответа клиенту
		sendMessage(ws, response);
    }
}

// GPT3.5-Turbo
async function callGPT35Turbo(messages, ws) {
    try {
        let fullResponse = ""; // Переменная для результата
		
		console.log('Запрос клиента к GPT3.5-Turbo:', messages[messages.length - 1]); 
		//throw new Error("Намеренная ошибка для тестирования catch");
		let messageSent = false;
        gpt3({
            messages: messages,
            markdown: false
        }, (result) => {
			// Обработка результата внутри колбэка
            if (result.error) {
                //throw new Error(result.error); // Обрабатываем ошибку, если она есть
            }
			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);
				if (!messageSent) { // Отправляем сообщение об ошибке только если еще не отправили ответ
                    stats.totalGPT35CallbackErrors++;
                    let errorResponse = { code: 2, message: "Произошла ошибка. Попробуйте еще раз." };
                    sendMessage(ws, errorResponse);
                    messageSent = true; // Обновляем флаг, чтобы сообщение больше не отправлялось
                }
            }
        });
    } catch (error) {
		stats.totalGPT35MessErrors++;
        console.error('Error:', error);
		let response = { code: 2, message: "Произошла ошибка. Попробуйте еще раз."};
		// Отправка ответа клиенту
		sendMessage(ws, response);
    }
}


// Gemini
async function callGemini(messages, ws) {
    try {
        let fullResponse = ""; // Переменная для результата
		
		console.log('Запрос клиента к Gemini:', messages[messages.length - 1]); 
		//throw new Error("Намеренная ошибка для тестирования catch");
		let messageSent = false;
        gemini({
            messages: messages
        }, (result) => {
			// Обработка результата внутри колбэка
            if (result.error) {
                throw new Error(result.error); // Обрабатываем ошибку, если она есть
            }
			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);
				if (!messageSent) { // Отправляем сообщение об ошибке только если еще не отправили ответ
                    stats.totalGeminiCallbackErrors++;
                    let errorResponse = { code: 2, message: "Произошла ошибка. Попробуйте еще раз." };
                    sendMessage(ws, errorResponse);
                    messageSent = true; // Обновляем флаг, чтобы сообщение больше не отправлялось
                }
            }
        });
    } catch (error) {
		stats.totalGeminiMessErrors++;
        console.error('Error:', error);
		let response = { code: 2, message: "Произошла ошибка. Попробуйте еще раз."};
		// Отправка ответа клиенту
		sendMessage(ws, response);
    }
}


// Qwen
async function callQwen(messages, ws) {
    try {
        let fullResponse = ""; // Переменная для результата
		
		console.log('Запрос клиента к Qwen:', messages[messages.length - 1]); 
		//throw new Error("Намеренная ошибка для тестирования catch");
		let messageSent = false;
        qwen({
            messages: messages,
            markdown: false
        }, (result) => {
			// Обработка результата внутри колбэка
            if (result.error) {
                throw new Error(result.error); // Обрабатываем ошибку, если она есть
            }
			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);
				if (!messageSent) { // Отправляем сообщение об ошибке только если еще не отправили ответ
                    stats.totalQwenCallbackErrors++;
                    let errorResponse = { code: 2, message: "Произошла ошибка. Попробуйте еще раз." };
                    sendMessage(ws, errorResponse);
                    messageSent = true; // Обновляем флаг, чтобы сообщение больше не отправлялось
                }
            }
        });
    } catch (error) {
		stats.totalQwenMessErrors++;
        console.error('Error:', error);
		let response = { code: 2, message: "Произошла ошибка. Попробуйте еще раз."};
		// Отправка ответа клиенту
		sendMessage(ws, response);
    }
}

// Blackbox
async function callBlackbox(messages, ws) {
    try {
        let fullResponse = ""; // Переменная для результата
		
		console.log('Запрос клиента к Blackbox:', messages[messages.length - 1]); 
		//throw new Error("Намеренная ошибка для тестирования catch");
		let messageSent = false;
        blackbox({
            messages: messages,
            markdown: false,
            websearch: false,
            llama: false
        }, (result) => {
			// Обработка результата внутри колбэка
            if (result.error) {
                throw new Error(result.error); // Обрабатываем ошибку, если она есть
            }
			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);
				if (!messageSent) { // Отправляем сообщение об ошибке только если еще не отправили ответ
                    stats.totalBlackboxCallbackErrors++;
                    let errorResponse = { code: 2, message: "Произошла ошибка. Попробуйте еще раз." };
                    sendMessage(ws, errorResponse);
                    messageSent = true; // Обновляем флаг, чтобы сообщение больше не отправлялось
                }
            }
        });
    } catch (error) {
		stats.totalBlackboxMessErrors++;
        console.error('Error:', error);
		let response = { code: 2, message: "Произошла ошибка. Попробуйте еще раз."};
		// Отправка ответа клиенту
		sendMessage(ws, response);
    }
}

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

stats.js

const fs = require('fs');
const path = require('path');

// Количество дней для статистики
const statsDays = 365; // Можно сделать глобальной переменной, если нужно передавать со стороны

// Статистика
const stats = {
    totalRequests: 0,
    totalConnections: 0,
	totalIPBlocks: 0,
	totalWsMessErrors: 0,
	totalGPT4oCallbackErrors: 0,
	totalGPT4oMessErrors: 0,
	totalGPT35CallbackErrors: 0,
	totalGPT35MessErrors: 0,
	totalGeminiCallbackErrors: 0,
	totalGeminiMessErrors: 0,
	totalQwenCallbackErrors: 0,
	totalQwenMessErrors: 0,
	totalBlackboxCallbackErrors: 0,
	totalBlackboxMessErrors: 0,
    date: new Date().toISOString().split('T')[0] // Начальная дата - текущая
};

// Функция для сохранения статистики в файл
function saveStatistics() {
    const statsFilePath = path.join(__dirname, 'statistics.json');

    // Считываем существующие данные из файла
    let existingData = [];
    if (fs.existsSync(statsFilePath)) {
        const fileContent = fs.readFileSync(statsFilePath, 'utf8');
        existingData = JSON.parse(fileContent);
    }

    // Создаем объект с текущей статистикой
    const currentStat = {
        date: stats.date,
        totalRequests: stats.totalRequests,
        totalConnections: stats.totalConnections,
		totalIPBlocks: stats.totalIPBlocks,
		totalWsMessErrors: stats.totalWsMessErrors,
		totalGPT4oCallbackErrors: stats.totalGPT4oCallbackErrors,
		totalGPT4oMessErrors: stats.totalGPT4oMessErrors,
		totalGPT35CallbackErrors: stats.totalGPT35CallbackErrors,
		totalGPT35MessErrors: stats.totalGPT35MessErrors,
		totalGeminiCallbackErrors: stats.totalGeminiCallbackErrors,
		totalGeminiMessErrors: stats.totalGeminiMessErrors,
		totalQwenCallbackErrors: stats.totalQwenCallbackErrors,
		totalQwenMessErrors: stats.totalQwenMessErrors,
		totalBlackboxCallbackErrors: stats.totalBlackboxCallbackErrors,
		totalBlackboxMessErrors: stats.totalBlackboxMessErrors
    };

    // Добавляем текущую статистику
    existingData.push(currentStat);

    // Удаляем старые записи, если их больше statsDays
    if (existingData.length > statsDays) {
        existingData = existingData.slice(existingData.length - statsDays); // сохраняем только последние statsDays записей
    }

    // Сохраняем обновленные данные
    fs.writeFileSync(statsFilePath, JSON.stringify(existingData, null, 4)); // Форматируем JSON для удобочитаемости

    // Логируем только текущую статистику
    console.log('Статистика сохранена:', currentStat);
}

// Функция для обнуления статистики и сохранения её в файле
function resetStatistics() {
    saveStatistics();
    stats.totalRequests = 0;
    stats.totalConnections = 0;
	stats.totalIPBlocks = 0;
	stats.totalWsMessErrors = 0;
	stats.totalGPT4oCallbackErrors = 0;
	stats.totalGPT4oMessErrors = 0;
	stats.totalGPT35CallbackErrors = 0;
	stats.totalGPT35MessErrors = 0;
	stats.totalGeminiCallbackErrors = 0;
	stats.totalGeminiMessErrors = 0;
	stats.totalQwenCallbackErrors = 0;
	stats.totalQwenMessErrors = 0;
	stats.totalBlackboxCallbackErrors = 0;
	stats.totalBlackboxMessErrors = 0;
    stats.date = new Date().toISOString().split('T')[0]; // Обновляем дату
}

// Функция для вычисления времени до следующего обнуления (в полночь). Сохраняет раз в сутки
function scheduleDailyReset() {
    const now = new Date();
    const nextReset = new Date();
    nextReset.setHours(24, 0, 0, 0); // Устанавливаем на полночь следующего дня

    const delay = nextReset - now;
    setTimeout(() => {
        resetStatistics(); // Обнуляем статистику в полночь
        setInterval(resetStatistics, 24 * 60 * 60 * 1000); // Затем устанавливаем интервал
    }, delay);
}

// Сохраняет раз в 20 секунд
/*function scheduleDailyReset() {
    // Запланировать первую запись через 20 секунд
    setTimeout(() => {
        resetStatistics(); // первая запись статистики
        // Устанавливаем интервал на 2 минут для последующих записей
        setInterval(resetStatistics, 10 * 1000);
    }, 10 * 1000); // 20 секунд
}*/

// Экспортируем функции и переменные
module.exports = {
    stats,
    scheduleDailyReset,
    resetStatistics,
    saveStatistics,
};

Assistant:  вашем коде реализована система сбора статистики, связанная с количеством запросов и подключений WebSocket-сервера для разных моделей. Я проведу анализ основных частей кода и дам рекомендации.

### Основные аспекты работы кода

1. **Сбор статистики**:
   - Вы используете объект `stats`, который хранит различные статистические метрики, такие как `totalRequests`, `totalConnections` и ошибки в обработке сообщений (например, `totalWsMessErrors`, `totalGPT4oCallbackErrors` и т.д.).
   - Каждое успешное подключение и каждый запрос увеличивают соответствующий счетчик.

2. **Сохранение статистики**:
   - Функция `saveStatistics` отвечает за сохранение текущей статистики в файл `statistics.json`. При этом она считывает существующие данные и добавляет к ним новую запись текущей статистики.
   - В функция `resetStatistics` сбрасывает все счетчики, после чего также вызывает сохранение текущей статистики в файл.

3. **Запланированное обнуление статистики**:
   - Статистика обнуляется каждый день в полночь с помощью функции `scheduleDailyReset`. Сначала устанавливается таймер до следующего обнуления, после чего вызывается периодическая функция с интервалом в 24 часа.

4. **Валидация входящих сообщений**:
   - Вы используете `Ajv` для проверки структуры входящих сообщений перед их обработкой в функции, которые вызывают API различных моделей.

5. **Управление подключениями и ограничение по скорости**:
   - Каждое подключение WebSocket имеет уникальный идентификатор, который используется для отслеживания действий клиента и ограничения по скорости с помощью `Bottleneck` для предотвращения спама.

### Рекомендации по улучшению и выявленные проблемы

1. **Обработка ошибок и логирование**:
   - Убедитесь, что все участки кода, где могут возникнуть исключения, тщательно обрабатывают эти исключения. Это включает обработку исключений внутри ваших колбэков при работе с API.
   - Возможно, стоит добавить больше логирования о том, как обрабатываются запросы клиента, чтобы упростить отладку.

2. **Эффективность работы с файлами**:
   - При больших объемах данных о статистике, использование синхронного чтения и записи может привести к блокировке потока. Рассмотрите возможность использования асинхронных операций с `fs.promises` для работы с файлами.

3. **Лимиты по времени**:
   - Вы установили `minTime: 1000`, что означает, что клиент может отправлять один запрос в секунду. Это может быть недостаточно быстро в зависимости от ваших требований.

4. **Управление памятью**:
   - Удаление старых соединений из памяти можно сделать более динамичным, вместо проверки каждые 10 минут: например, использовать `setTimeout` на каждое новое соединение.

5. **Валидация сообщений**:
   - В функции `validateMessages` вы проверяете только `role` и `content`. Возможно, стоит добавить дополнительные проверки, например, на минимальную или максимальную длину содержимого для логики обработки.

6. **Структура статистики**:
   - Рассмотрите добавление больше метрик для детального анализа работы сервера, например, средней продолжительности ответа, времени выполнения запросов и т.д.

### Заключение
Ваш код выглядит хорошо организованным и с учетом многих важных аспектов работы с API и сохранения статистики. Однако с точки зрения производительности, обработки ошибок и управления памятью есть некоторые области для улучшения.
User: В функции validateMessages вы проверяете только role и content. Возможно, стоит добавить дополнительные проверки, например, на минимальную или максимальную длину содержимого для логики обработки.

Измени функцию validateMessages. Если сообщения от user длиннее 2000 символов иъ надо обрезать до 2000 символов.
Assistant: