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:  функция `sendMessage(ws, response)` также вызывается много раз, это может быть связано с тем, что код внутри обработчика колбэка вызывается несколько раз и/или неправильно обрабатывает результаты в определенных условиях, что приводит к отправке сообщений при каждом вызове.

Для решения этой проблемы, помимо добавления флага для отслеживания ошибок, можно рассмотреть следующие шаги:

1. Использование флага для предотвращения повторной отправки сообщений.
2. Убедитесь, что сообщение отправляется только один раз, когда результат обработки завершен.

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

Вот пример, как можно изменить вашу функцию `callBlackbox`, чтобы избежать многократной отправки сообщений:

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

    try {
        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 || "";
                } else {
                    // Если завершено, выводим полный ответ
                    let response = { code: 1, message: fullResponse };
                    console.log('Ответ чата: ', response);
                    // отправляем сообщение только если еще не отправили его
                    if (!messageSent) {
                        sendMessage(ws, response);
                        messageSent = true; // Устанавливаем флаг, что сообщение отправлено
                    }
                }
            } 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) {
        console.error('Error:', error);
        if (!messageSent) { // Отправляем сообщение об ошибке только если еще не отправили ответ
            stats.totalBlackboxMessErrors++;
            let response = { code: 2, message: "Произошла ошибка. Попробуйте еще раз." };
            sendMessage(ws, response);
            messageSent = true; // Устанавливаем флаг, что сообщение отправлено
        }
    }
}
```

### Пояснение:
- **Флаг `messageSent`:** используется для отслеживания того, было ли сообщение уже отправлено. Это позволяет избежать повторной отправки одного и того же сообщения.
- **Проверка флага перед отправкой:** каждое сообщение отправляется только в том случае, если флаг `messageSent` равен `false`. После успешной отправки сообщения флаг устанавливается в `true`.

Таким образом, даже если функция колбэка будет вызвана несколько раз, вы сможете гарантировать, что каждое сообщение будет отправлено только один раз, что также должно помочь уменьшить нагрузку на интерфейс пользователя.
User: что если написать так
catch (callbackError) {
				console.error('Error processing callback result:', callbackError);
				if (!messageSent) { // Отправляем сообщение об ошибке только если еще не отправили ответ
                    stats.totalBlackboxCallbackErrors++;
                    let errorResponse = { code: 2, message: "Произошла ошибка. Попробуйте еще раз." };
                    sendMessage(ws, errorResponse);
                    messageSent = true; // Обновляем флаг, чтобы сообщение больше не отправлялось
return;
                }
            }
Assistant: