User: stats.js

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

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

// Статистика
const stats = {
    totalRequests: 0,
    totalConnections: 0,
	totalIPBlocks: 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
    };

    // Добавляем текущую статистику
    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.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,
};

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;
			console.log('Модель - ', 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) {
				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('Запрос клиента:', messages[messages.length - 1]); 
		//throw new Error("Намеренная ошибка для тестирования catch");
        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);
                let response = { code: 2, message: "Произошла ошибка. Попробуйте еще раз." };
                // Отправка ответа клиенту
				sendMessage(ws, response);
            }
        });
    } catch (error) {
        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('Запрос клиента:', messages[messages.length - 1]); 
		//throw new Error("Намеренная ошибка для тестирования catch");
        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);
                let response = { code: 2, message: "Произошла ошибка. Попробуйте еще раз." };
                // Отправка ответа клиенту
				sendMessage(ws, response);
            }
        });
    } catch (error) {
        console.error('Error:', error);
		let response = { code: 2, message: "Произошла ошибка. Попробуйте еще раз."};
		// Отправка ответа клиенту
		sendMessage(ws, response);
    }
}


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


// Qwen
async function callQwen(messages, ws) {
    try {
        let fullResponse = ""; // Переменная для результата
		
		console.log('Запрос клиента:', messages[messages.length - 1]); 
		//throw new Error("Намеренная ошибка для тестирования catch");
        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);
                let response = { code: 2, message: "Произошла ошибка. Попробуйте еще раз." };
                // Отправка ответа клиенту
				sendMessage(ws, response);
            }
        });
    } catch (error) {
        console.error('Error:', error);
		let response = { code: 2, message: "Произошла ошибка. Попробуйте еще раз."};
		// Отправка ответа клиенту
		sendMessage(ws, response);
    }
}

// Blackbox
async function callBlackbox(messages, ws) {
    try {
        let fullResponse = ""; // Переменная для результата
		
		console.log('Запрос клиента:', messages[messages.length - 1]); 
		//throw new Error("Намеренная ошибка для тестирования catch");
        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);
                let response = { code: 2, message: "Произошла ошибка. Попробуйте еще раз." };
                // Отправка ответа клиенту
				sendMessage(ws, response);
            }
        });
    } catch (error) {
        console.error('Error:', error);
		let response = { code: 2, message: "Произошла ошибка. Попробуйте еще раз."};
		// Отправка ответа клиенту
		sendMessage(ws, response);
    }
}

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

нужно расширить статистику. Нужно добавить сколько всего раз сработал этот участок кода

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

Новое свойство stats назови totalWsMessErrors
Assistant:  добавить новое свойство для статистики, которое будет отслеживать количество срабатываний блока catch, вам нужно сделать несколько изменений в коде:

1. Добавить новое свойство в объект `stats`, называемое `totalWsMessErrors`.
2. Увеличивать это новое свойство каждый раз, когда срабатывает блок catch для обработки ошибок разбора JSON.

Вот конкретные изменения, которые нужно внести:

### 1. Изменение в `stats.js`

Добавьте новое свойство `totalWsMessErrors` в объект `stats`:

```javascript
// Статистика
const stats = {
    totalRequests: 0,
    totalConnections: 0,
    totalIPBlocks: 0,
    totalWsMessErrors: 0, // Новое свойство для отслеживания ошибок сообщений WebSocket
    date: new Date().toISOString().split('T')[0]
};
```

### 2. Изменение в `call.js`

Измените блок catch, который ловит ошибки разбора JSON, чтобы увеличить `totalWsMessErrors` каждый раз, когда возникает ошибка:

```javascript
ws.on('message', (message) => {
    // Проверяем, является ли сообщение экземпляром Buffer
    if (Buffer.isBuffer(message))
        message = message.toString(); 
    try {
        // Преобразуем строку JSON обратно в объект
        message = JSON.parse(message); 
        // ... остальной код ...
    } catch (error) {
        stats.totalWsMessErrors++; // Увеличиваем счетчик ошибок
        console.error('Error parsing JSON:', error);
        const response = { code: 2, message: "Произошла ошибка. Попробуйте еще раз." };
        console.log(response);
        // Отправка ответа клиенту
        sendMessage(ws, response);
    }
});
```

### Полный код изменения

Вот полный код с учетом всех изменений:

**`stats.js`**
```javascript
// Статистика
const stats = {
    totalRequests: 0,
    totalConnections: 0,
    totalIPBlocks: 0,
    totalWsMessErrors: 0, // Новое свойство
    date: new Date().toISOString().split('T')[0] // Начальная дата - текущая
};

// ... остальная часть кода
```

**`call.js`**
```javascript
ws.on('message', (message) => {
    // Проверяем, является ли сообщение экземпляром Buffer
    if (Buffer.isBuffer(message))
        message = message.toString(); 
    try {
        // Преобразуем строку JSON обратно в объект
        message = JSON.parse(message); 
        // ... остальной код ...
    } catch (error) {
        stats.totalWsMessErrors++; // Увеличиваем счетчик ошибок
        console.error('Error parsing JSON:', error);
        const response = { code: 2, message: "Произошла ошибка. Попробуйте еще раз." };
        console.log(response);
        // Отправка ответа клиенту
        sendMessage(ws, response);
    }
});
```

Теперь ваше приложение будет отслеживать количество ошибок, связанных с разбором сообщений WebSocket, и хранить эту информацию в `stats.totalWsMessErrors`.
User: приведи код stats.js полностью
Assistant:  полный код файла `stats.js` с добавленным свойством `totalWsMessErrors` в объекте статистики:

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

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

// Статистика
const stats = {
    totalRequests: 0,
    totalConnections: 0,
    totalIPBlocks: 0,
    totalWsMessErrors: 0, // Новое свойство для отслеживания ошибок сообщений WebSocket
    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 // Включаем новое свойство
    };

    // Добавляем текущую статистику
    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.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);
}

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

Этот код теперь включает в себя новое свойство `totalWsMessErrors`, которое будет отслеживать количество ошибок при разборе сообщений WebSocket. Все соответствующие изменения были внесены в функции для сохранения и обнуления статистики.
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;
			console.log('Модель - ', 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('Запрос клиента:', messages[messages.length - 1]); 
		//throw new Error("Намеренная ошибка для тестирования catch");
        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) {
				stats.totalGPT4oCallbackErrors++;
                console.error('Error processing callback result:', callbackError);
                let response = { code: 2, message: "Произошла ошибка. Попробуйте еще раз." };
                // Отправка ответа клиенту
				sendMessage(ws, response);
            }
        });
    } 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('Запрос клиента:', messages[messages.length - 1]); 
		//throw new Error("Намеренная ошибка для тестирования catch");
        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) {
				stats.totalGPT35CallbackErrors++;
                console.error('Error processing callback result:', callbackError);
                let response = { code: 2, message: "Произошла ошибка. Попробуйте еще раз." };
                // Отправка ответа клиенту
				sendMessage(ws, response);
            }
        });
    } 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('Запрос клиента:', messages[messages.length - 1]); 
		//throw new Error("Намеренная ошибка для тестирования catch");
        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) {
				stats.totalGeminiCallbackErrors++;
                console.error('Error processing callback result:', callbackError);
                let response = { code: 2, message: "Произошла ошибка. Попробуйте еще раз." };
                // Отправка ответа клиенту
				sendMessage(ws, response);
            }
        });
    } 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('Запрос клиента:', messages[messages.length - 1]); 
		//throw new Error("Намеренная ошибка для тестирования catch");
        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) {
				stats.totalQwenCallbackErrors++;
                console.error('Error processing callback result:', callbackError);
                let response = { code: 2, message: "Произошла ошибка. Попробуйте еще раз." };
                // Отправка ответа клиенту
				sendMessage(ws, response);
            }
        });
    } 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('Запрос клиента:', messages[messages.length - 1]); 
		//throw new Error("Намеренная ошибка для тестирования catch");
        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) {
				stats.totalBlackboxCallbackErrors++;
                console.error('Error processing callback result:', callbackError);
                let response = { code: 2, message: "Произошла ошибка. Попробуйте еще раз." };
                // Отправка ответа клиенту
				sendMessage(ws, response);
            }
        });
    } 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: Произошла ошибка. Попробуйте еще раз.
User: В этом месте

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

при возникновении ошибки многократно вызывается catch. В результате stats.totalBlackboxCallbackErrors++; срабатывает несколько раз пока не придут все колбеки. Кроме того, интерфейс пользователя подвисает
Assistant: роблема, с которой вы сталкиваетесь, вероятно, связана с тем, что ошибка в обработке результатов (callback) повторно вызывает блок `catch`, увеличивая счетчик `stats.totalBlackboxCallbackErrors` несколько раз, тем самым создавая ненадежную статистику. 

Чтобы предотвратить повторные вызовы `catch` и множественные инкременты счетчика, вы можете добавить флаг, который будет отслеживать, была ли ошибка уже зарегистрирована. 

Вот один из способов исправления вашего кода:

1. Добавьте переменную флага внутри функции `callBlackbox` для отслеживания произошедшей ошибки.
2. Используйте этот флаг, чтобы избежать повторного увеличения счетчика.

### Пример изменения в функции `callBlackbox`

```javascript
async function callBlackbox(messages, ws) {
    try {
        let fullResponse = ""; // Переменная для результата
        console.log('Запрос клиента:', messages[messages.length - 1]); 
        let errorOccurred = false; // Флаг для отслеживания ошибки

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

### Пояснение:
- **Флаг `errorOccurred`:** используется для отслеживания того, была ли уже зарегистрирована ошибка в обработчиках колбэков.
- **Проверка перед инкрементом:** добавлена проверка перед увеличением `stats.totalBlackboxCallbackErrors`. Если ошибка уже была зарегистрирована, инкремент не произойдет.

Этот подход поможет контролировать число регистраций ошибки и предотвратить зависание интерфейса пользователя из-за многократных вызовов ошибок.
User: sendMessage(ws, response); тоже срабатывает много раз
Assistant: