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

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

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

wss.on('connection', (ws, req) => {
	
	const urlParams = new URLSearchParams(req.url.split('?')[1]);
    const userId = urlParams.get('token'); // Получаем UUID из URL
    const userIP = req.socket.remoteAddress;
	//console.log("IP-адрес - ", userIP, "Время подключения - ", new Date().toISOString());
	const userHeaders = req.headers;

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

    ws.on('message', (message) => {
		// Проверяем, является ли сообщение экземпляром Buffer
		if (Buffer.isBuffer(message))
			message = message.toString(); 
		try {
			// Преобразуем строку JSON обратно в объект
			message = JSON.parse(message); 
			//Получаем историю чата из запроса
			let messages = message.messages;
			let model = message.model;
			// Выполняем валидацию сообщений
			messages = validateMessages(messages);
			
			// Проверяем, остались ли сообщения после валидации
			if (messages.length === 0) {
				const response = { code: 2, message: "Нет валидных сообщений для обработки." };
				sendMessage(ws, response);
				return;
			}
			
			// Урезаем последнее сообщение до 2000 символов, если оно от клиента
			if (messages[messages.length - 1].role === 'user') {
				messages[messages.length - 1].content = messages[messages.length - 1].content.substring(0, 2000000);
			}
			
			// Увеличиваем общий счетчик запросов
            stats.totalRequests++;
			
			// Используем Bottleneck для управления количеством запросов
			limiters[userId].schedule(() => {
				// Отправка запроса на сервер чата
				if (model == 'gpt-4o')
					return callGPT4o(messages, ws);
				else if (model == 'gemini-pro')
					return callGemini(messages, ws);
				else if (model == 'qwen')
					return callQwen(messages, ws);
				else if (model == 'blackbox')
					return callBlackbox(messages, ws);
				else if (model == 'gpt-3.5-turbo')
					return callGPT35Turbo(messages, ws);
				else
					return callGPT4o(messages, ws);
			}).catch(error => {
				stats.totalIPBlocks++;
				//console.error('Rate limit exceeded, message ignored:', error);
				const response = { code: 2, message: "Превышен лимит запросов. Пожалуйста, попробуйте позже." };
				// Отправка ответа клиенту
				//sendMessage(ws, response);
			});
			
		} catch (error) {
				stats.totalWsMessErrors++;
				console.error('Error parsing JSON:', error);
				const response = { code: 2, message: "Произошла ошибка. Попробуйте еще раз."};
				console.log(response);
				// Отправка ответа клиенту
				sendMessage(ws, response);
		}
		
    });

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

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

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

// Код выводит текущие метрики использования памяти приложения
setInterval(() => {
  const memoryUsage = process.memoryUsage();
  console.log(`Текущие метрики использования памяти: 
    RSS: ${memoryUsage.rss / (1024 * 1024)} MB,
    Heap Total: ${memoryUsage.heapTotal / (1024 * 1024)} MB,
    Heap Used: ${memoryUsage.heapUsed / (1024 * 1024)} MB,
    External: ${memoryUsage.external / (1024 * 1024)} MB`);
}, 60000); // Вывод каждый 60 секунд


function checkOrigin(origin, socket) {
    if (origin !== allowedOrigin) {
        console.log('Недопустимый домен. Соединение разорвано: ', origin);
        socket.close(); // Закрываем соединение, если домен не разрешен
        return false; // Возвращаем false для индикатора отклонения
    }
    return true; // Возвращаем true, если домен разрешен
}

// Отправляет ответ клиенту
function sendMessage(ws, message) {
    if (ws.readyState === WebSocket.OPEN) {
        ws.send(JSON.stringify(message));
    } else {
        console.warn(`Trying to send message to a closed WebSocket connection.`);
    }
}

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

    // Фильтруем массив, оставляя только корректные сообщения
    return messages.filter(message => {
        // Проверяем, соответствует ли сообщение схеме
        if (ajv.validate(schema, message)) {
            // Обрезаем содержимое, если оно длиннее 2000 символов
            if (message.content.length > 2000000) {
                message.content = message.content.substring(0, 2000000);
            }
            return true; // Сообщение валидно, оставляем в массиве
        }
        return false; // Сообщение не валидно, исключаем из массива
    });
}

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

















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

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

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


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

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






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

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

как провести нагрузочное тестирование на этот код на сервере
Assistant:  провести нагрузочное тестирование на WebSocket сервере, описанном в вашем коде, вы можете использовать специальные инструменты и библиотеки. Я приведу несколько подходов и шагов, которые помогут вам организовать нагрузочное тестирование.

### 1. Использование специализированных инструментов

Существует несколько инструментов, которые позволяют проводить нагрузочное тестирование WebSocket серверов:

- **Artillery**: Это мощный инструмент для тестирования нагрузок, который поддерживает WebSockets.
- **K6**: Также поддерживает WebSocket, и может быть использован для сценарного тестирования с высоким уровнем настройки.
- **Gatling**: HTTP и WebSockets тестировщик с хорошей производительностью и возможностями визуализации.
- **Locust**: Позволяет писать сценарии тестирования на Python.

#### Пример с Artillery

Если вы выберете Artillery, вот как вы можете настроить его для тестирования вашего WebSocket сервера.

1. Установите Artillery:

```bash
npm install -g artillery
```

2. Создайте файл конфигурации Artillery, например `test.yml`:

```yaml
config:
  target: 'ws://localhost:3000'
  phases:
    - duration: 300
      arrivalRate: 10
scenarios:
  - flow:
      - send:
          text: '{"token": "user-1", "messages": [{"role":"user", "content":"Test message!"}], "model": "gpt-4o"}'
      - think: 1
```

3. Запустите тест:

```bash
artillery run test.yml
```

### 2. Написание собственного скрипта на Node.js

Вы также можете написать собственный скрипт на Node.js для проведения нагрузочного тестирования. Вот пример простого теста, который создает множество соединений и отправляет сообщения:

```javascript
const WebSocket = require('ws');

const totalConnections = 100; // Количество одновременных соединений
const targetUrl = 'ws://localhost:3000';
const message = '{"token": "user-1", "messages": [{"role":"user", "content":"Test message!"}], "model": "gpt-4o"}';

for (let i = 0; i < totalConnections; i++) {
    const ws = new WebSocket(targetUrl);

    ws.on('open', () => {
        console.log(`Connection ${i + 1} opened.`);
        ws.send(message); // Отправить сообщение
    });

    ws.on('message', (data) => {
        console.log(`Message from server: ${data}`);
    });

    ws.on('close', () => {
        console.log(`Connection ${i + 1} closed.`);
    });

    ws.on('error', (error) => {
        console.error(`Error in connection ${i + 1}:`, error);
    });
}
```

Запустите этот скрипт с помощью Node.js. Он создаст указанное количество соединений и отправит тестовые сообщения. Вы можете адаптировать его для отслеживания времени отклика, успешных и неуспешных запросов.

### 3. Мониторинг результатов

Во время нагрузки мониторьте следующие параметры:

- Время отклика сервера
- Количество успешных и неуспешных соединений
- Загруженность CPU и памяти сервера
- Показатели производительности базы данных (если имеются)

### 4. Анализ результатов

После завершения теста проанализируйте результаты:

- Определите максимальную нагрузку, при которой сервер работает стабильно.
- Изучите точки отказа и узкие места в производительности.
- Используйте полученные данные для оптимизации и улучшения вашего WebSocket сервера.

### Заключение

Нагрузочное тестирование — это важный этап для обеспечения стабильности и производительности вашего приложения. Выберите любой из описанных методов в зависимости от ваших потребностей и возможностей.
User: config:
  target: 'ws://localhost:3000'
  phases:
    - duration: 300
      arrivalRate: 10
scenarios:
  - flow:
      - send:
          text: '{"token": "user-1", "messages": [{"role":"user", "content":"Test message!"}], "model": "gpt-4o"}'
      - think: 1

куда сохранить файл
Assistant: айл конфигурации для `Artillery` можно сохранить в любом удобном для вас месте на вашем компьютере. Однако для удобства работы рекомендуется создать специальную папку для ваших тестов. Вот шаги, которые можно выполнить:

1. **Создайте папку для тестов** (по желанию, например, в вашем рабочем каталоге):
   ```bash
   mkdir artillery-tests
   cd artillery-tests
   ```
   
2. **Создайте файл конфигурации**. Вы можете использовать текстовый редактор (например, `nano`, `vim`, `notepad`, или редактор кода, как Visual Studio Code) для создания файла:
   ```bash
   nano test.yml
   ```

3. **Сохраните конфигурацию** в файле `test.yml`, вставив в него следующий текст:

   ```yaml
   config:
     target: 'ws://localhost:3000'
     phases:
       - duration: 300
         arrivalRate: 10
   scenarios:
     - flow:
         - send:
             text: '{"token": "user-1", "messages": [{"role":"user", "content":"Test message!"}], "model": "gpt-4o"}'
         - think: 1
   ```

4. **Сохраните файл** и выйдите из редактора. Например, в `nano` это делается с помощью комбинации клавиш `Ctrl + O` (для сохранения) и `Ctrl + X` (для выхода).

Теперь файл `test.yml` будет находиться в вашей папке `artillery-tests`. Чтобы запустить тест, вы можете выполнить следующую команду в терминале, находясь в этой папке:

```bash
artillery run test.yml
```

Таким образом, вы сможете управлять своими конфигурациями тестов и легко их находить.
User: config:
  target: 'ws://localhost:3000'
  phases:
    - duration: 300
      arrivalRate: 10
scenarios:
  - flow:
      - send:
          text: '{"token": "user-1", "messages": [{"role":"user", "content":"Test message!"}], "model": "gpt-4o"}'
      - think: 1

что писать тут target: 'ws://localhost:3000', если сайт на локальном сервере доступен по адресу http://gpt.loc
Assistant:  ваш сайт доступен по адресу `http://gpt.loc` и вы хотите протестировать WebSocket-сервер, который работает на этом домене, вам нужно изменить `target` в вашем файле конфигурации Artillery следующим образом:

```yaml
config:
  target: 'ws://gpt.loc:3000'  # Обратите внимание на протокол ws:// вместо http://
  phases:
    - duration: 300
      arrivalRate: 10
scenarios:
  - flow:
      - send:
          text: '{"token": "user-1", "messages": [{"role":"user", "content":"Test message!"}], "model": "gpt-4o"}'
      - think: 1
```

Вот что следует учитывать:

1. **Протокол**: Убедитесь, что вы используете `ws://` для WebSocket-соединений вместо `http://`.
2. **Порт**: Убедитесь, что ваш WebSocket-сервер действительно слушает на порту `3000`. Если он работает на другом порту, замените `3000` на соответствующий номер порта.
3. **Домен**: Если `gpt.loc` настроен правильно и указывает на ваш локальный сервер, тест будет выполняться корректно.

После внесения изменений сохраните файл и снова запускайте тест с помощью команды:

```bash
artillery run test.yml
```

Это позволит Artillery протестировать ваш WebSocket-сервер по адресу `ws://gpt.loc:3000`.
User: Это тест на количество соединений? А как проверить нагрузку при большом количестве сообщений?
Assistant: