Описание изменений

This commit is contained in:
2025-02-25 22:36:14 +03:00
parent e7a0aacb60
commit c68b866fa1
9 changed files with 2188 additions and 22 deletions

View File

@@ -7,7 +7,7 @@
"deploy": "hardhat run scripts/deploy.js --network sepolia",
"node": "hardhat node",
"test": "hardhat test",
"server": "nodemon server.js"
"server": "nodemon --signal SIGUSR2 server.js"
},
"dependencies": {
"@langchain/community": "^0.3.31",
@@ -18,7 +18,11 @@
"cors": "^2.8.5",
"express": "^4.18.3",
"express-session": "^1.18.0",
"imap": "^0.8.19",
"langchain": "^0.3.19",
"mailparser": "^3.7.2",
"node-telegram-bot-api": "^0.66.0",
"nodemailer": "^6.10.0",
"nodemon": "^3.1.0",
"openai": "^4.85.2",
"pg": "^8.13.3",
@@ -40,5 +44,11 @@
"elliptic": "^6.6.1",
"secp256k1": "^5.0.0",
"cookie": "^0.7.0"
},
"nodemonConfig": {
"delay": "2000",
"events": {
"restart": "kill -SIGUSR2 $PPID"
}
}
}

View File

@@ -10,6 +10,7 @@ const { Pool } = require('pg');
const { ethers } = require('ethers');
const contractABI = require('../artifacts/contracts/MyContract.sol/MyContract.json').abi;
const crypto = require('crypto');
const TelegramBotService = require('../services/telegramBot');
require('dotenv').config();
const pool = new Pool({
@@ -806,8 +807,45 @@ async function initializeTables() {
);
`);
console.log('Таблицы успешно инициализированы');
// Инициализируем vectorStore
vectorStore = await PGVectorStore.initialize(
embeddings,
{
postgresConnectionOptions: {
connectionString: process.env.DATABASE_URL
},
tableName: 'documents',
columns: {
idColumnName: 'id',
vectorColumnName: 'embedding',
contentColumnName: 'content',
metadataColumnName: 'metadata'
}
}
);
console.log('Векторное хранилище инициализировано:', {
tableName: 'documents',
columns: vectorStore.columns,
config: {
tableName: vectorStore.tableName,
columns: vectorStore.columns,
client: vectorStore.client ? 'Connected' : 'Not Connected',
embeddings: vectorStore.embeddings ? 'Initialized' : 'Not Initialized'
}
});
// Создаем экземпляр TelegramBotService только после инициализации vectorStore
if (vectorStore) {
const telegramBot = new TelegramBotService(
process.env.TELEGRAM_BOT_TOKEN,
vectorStore
);
}
} catch (error) {
console.error('Ошибка инициализации таблиц:', error);
console.error('Ошибка при инициализации:', error);
}
}

View File

@@ -5,9 +5,24 @@ const { SiweMessage, generateNonce } = require('siwe');
const path = require('path');
const apiRoutes = require('./routes/api.js');
const FileStore = require('session-file-store')(session);
const TelegramBotService = require('./services/telegramBot');
const EmailBotService = require('./services/emailBot');
const { PGVectorStore } = require('@langchain/community/vectorstores/pgvector');
const { OpenAIEmbeddings } = require('@langchain/openai');
const { exec } = require('child_process');
const util = require('util');
const execAsync = util.promisify(exec);
const app = express();
// Флаг для отслеживания состояния сервера
let isShuttingDown = false;
// Глобальные переменные для сервисов
let telegramBot;
let emailBot;
let vectorStore;
// 1. Парсинг JSON
app.use(express.json());
@@ -258,4 +273,127 @@ server.on('error', (error) => {
server.listen(PORT);
});
}
});
// Функция корректного завершения работы
async function gracefulShutdown(signal) {
if (isShuttingDown) return;
isShuttingDown = true;
console.log(`Получен сигнал ${signal}, начинаем корректное завершение...`);
try {
if (telegramBot) {
console.log('Останавливаем Telegram бота...');
await telegramBot.stop();
}
if (emailBot) {
console.log('Останавливаем Email бота...');
await emailBot.stop();
}
console.log('Все сервисы остановлены');
} catch (error) {
console.error('Ошибка при остановке сервисов:', error);
}
// Даем время на завершение всех процессов
setTimeout(() => {
process.exit(0);
}, 1000);
}
// Обработчики сигналов
process.on('SIGTERM', () => gracefulShutdown('SIGTERM'));
process.on('SIGINT', () => gracefulShutdown('SIGINT'));
process.on('SIGUSR2', () => gracefulShutdown('SIGUSR2')); // Для nodemon
// Обработка необработанных исключений
process.on('uncaughtException', (error) => {
console.error('Необработанное исключение:', error);
gracefulShutdown('uncaughtException');
});
process.on('unhandledRejection', (reason, promise) => {
console.error('Необработанное отклонение промиса:', reason);
gracefulShutdown('unhandledRejection');
});
// Функция для проверки и остановки существующих процессов бота
async function killExistingBotProcesses() {
try {
console.log('Проверяем существующие процессы бота...');
const { stdout } = await execAsync('ps aux | grep "[n]ode.*telegram"');
if (stdout) {
console.log('Найдены существующие процессы бота, останавливаем...');
await execAsync('pkill -SIGTERM -f "[n]ode.*telegram"');
// Ждем немного, чтобы процессы успели завершиться
await new Promise(resolve => setTimeout(resolve, 2000));
}
} catch (error) {
// Ошибка означает, что процессы не найдены - это нормально
console.log('Активные процессы бота не найдены');
}
}
// Инициализация векторного хранилища
async function initializeVectorStore() {
try {
// Сначала останавливаем существующие процессы
await killExistingBotProcesses();
// Даем время на освобождение ресурсов
await new Promise(resolve => setTimeout(resolve, 2000));
// Инициализируем embeddings
const embeddings = new OpenAIEmbeddings({
openAIApiKey: process.env.OPENAI_API_KEY,
configuration: {
baseURL: process.env.OPENAI_API_BASE || 'https://api.openai.com/v1'
}
});
// Инициализируем векторное хранилище
vectorStore = await PGVectorStore.initialize(embeddings, {
postgresConnectionOptions: {
connectionString: process.env.DATABASE_URL
},
tableName: 'documents',
columns: {
idColumnName: 'id',
vectorColumnName: 'embedding',
contentColumnName: 'content',
metadataColumnName: 'metadata',
}
});
console.log('Векторное хранилище инициализировано');
// Инициализируем ботов только после успешной инициализации хранилища
if (!telegramBot) {
telegramBot = new TelegramBotService(process.env.TELEGRAM_BOT_TOKEN, vectorStore);
await telegramBot.initialize().catch(error => {
if (error.code === 409) {
console.log('Telegram бот уже запущен в другом процессе');
} else {
throw error;
}
});
}
if (!emailBot) {
emailBot = new EmailBotService(vectorStore);
}
return vectorStore;
} catch (error) {
console.error('Ошибка при инициализации:', error);
throw error;
}
}
// Запускаем инициализацию
console.log('Начинаем инициализацию векторного хранилища...');
initializeVectorStore().catch(error => {
console.error('Критическая ошибка при инициализации:', error);
process.exit(1);
});

View File

@@ -0,0 +1,277 @@
const nodemailer = require('nodemailer');
const { ChatOllama } = require('@langchain/ollama');
const { PGVectorStore } = require('@langchain/community/vectorstores/pgvector');
const { Pool } = require('pg');
const Imap = require('imap');
const { simpleParser } = require('mailparser');
const { checkMailServer } = require('../utils/checkMail');
require('dotenv').config();
class EmailBotService {
constructor(vectorStore) {
if (!vectorStore) {
throw new Error('Vector store is required');
}
console.log('Инициализация Email бота...');
console.log('Проверяем настройки почты:', {
smtp: {
host: process.env.EMAIL_SMTP_HOST,
port: process.env.EMAIL_SMTP_PORT
},
imap: {
host: process.env.EMAIL_IMAP_HOST,
port: process.env.EMAIL_IMAP_PORT
}
});
// Инициализация базы данных
this.pool = new Pool({
connectionString: process.env.DATABASE_URL
});
this.vectorStore = vectorStore;
// Инициализация LLM
this.chat = new ChatOllama({
model: 'mistral',
baseUrl: 'http://localhost:11434'
});
// Настройка почтового клиента для отправки
this.transporter = nodemailer.createTransport({
host: process.env.EMAIL_SMTP_HOST,
port: process.env.EMAIL_SMTP_PORT,
secure: true,
auth: {
user: process.env.EMAIL_USER,
pass: process.env.EMAIL_PASSWORD
},
tls: {
rejectUnauthorized: false,
minVersion: 'TLSv1',
ciphers: 'HIGH:MEDIUM:!aNULL:!eNULL:!NULL:!DH:!EDH:!EXP:!LOW:!SSLv2:!MD5'
},
debug: true,
logger: true
});
// Проверяем подключение к SMTP
this.transporter.verify((error, success) => {
if (error) {
console.error('Ошибка подключения к SMTP:', {
name: error.name,
message: error.message,
code: error.code,
command: error.command,
stack: error.stack
});
setTimeout(() => this.initSMTP(), 30000);
} else {
console.log('SMTP сервер готов к отправке сообщений');
}
});
// Настройка IMAP для получения писем
const imapConfig = {
user: process.env.EMAIL_USER,
password: process.env.EMAIL_PASSWORD,
host: process.env.EMAIL_IMAP_HOST,
port: process.env.EMAIL_IMAP_PORT,
tls: true,
tlsOptions: { rejectUnauthorized: false },
keepalive: true,
authTimeout: 30000,
connTimeout: 30000
}
this.imap = new Imap(imapConfig);
// Добавляем обработчик для всех событий IMAP
this.imap.on('*', function(event, data) {
console.log('IMAP Event:', event, data);
});
// Проверяем MX записи
const domain = process.env.EMAIL_USER ? process.env.EMAIL_USER.split('@')[1] : '';
if (domain) {
checkMailServer(domain).then(records => {
if (!records) {
console.error('Не удалось найти MX записи для домена');
}
});
} else {
console.error('EMAIL_USER не настроен в .env файле');
}
this.isRunning = false;
this.initSMTP();
this.initIMAP();
console.log('Email bot service initialized');
}
async initSMTP() {
try {
console.log('Попытка подключения к SMTP...');
await this.transporter.verify();
console.log('SMTP подключение установлено');
} catch (error) {
console.error('Ошибка подключения к SMTP:', {
name: error.name,
message: error.message,
code: error.code,
command: error.command,
stack: error.stack
});
}
}
async initIMAP() {
try {
await this.initEmailListener();
console.log('IMAP подключение установлено');
} catch (error) {
console.error('Ошибка инициализации IMAP:', error);
}
}
async initEmailListener() {
try {
this.imap.on('ready', () => {
this.imap.openBox('INBOX', false, (err, box) => {
if (err) throw err;
// Слушаем новые письма
this.imap.on('mail', () => {
this.checkNewEmails();
});
});
});
this.imap.on('error', (err) => {
console.log('IMAP ошибка:', err);
if (err.source === 'timeout-auth') {
setTimeout(() => {
console.log('Попытка переподключения к IMAP...');
this.imap.connect();
}, 5000);
}
});
this.imap.connect();
} catch (error) {
console.error('Ошибка при инициализации IMAP:', error);
}
}
async processEmail(message) {
try {
// Проверяем, не является ли отправитель no-reply адресом
if (message.from.toLowerCase().includes('no-reply') ||
message.from.toLowerCase().includes('noreply')) {
console.log('Пропускаем письмо от no-reply адреса:', message.from);
return;
}
// Проверяем валидность домена получателя
const domain = message.from.split('@')[1];
try {
const records = await checkMailServer(domain);
if (!records || records.length === 0) {
console.log('Пропускаем письмо - домен не найден:', domain);
return;
}
} catch (err) {
console.error('Ошибка проверки домена:', err);
return;
}
// Получаем ответ от Ollama
const result = await this.chat.invoke(message.text);
// Формируем и отправляем ответ
await this.transporter.sendMail({
from: process.env.EMAIL_USER,
to: message.from,
subject: `Re: ${message.subject}`,
text: result.content
});
console.log('Ответ отправлен:', {
to: message.from,
subject: message.subject
});
} catch (error) {
console.error('Ошибка при обработке email:', error);
}
}
async checkNewEmails() {
try {
const messages = await new Promise((resolve, reject) => {
this.imap.search(['UNSEEN'], (err, results) => {
if (err) reject(err);
if (!results || !results.length) {
resolve([]);
return;
}
const fetch = this.imap.fetch(results, {
bodies: '',
markSeen: true
});
const messages = [];
fetch.on('message', (msg) => {
msg.on('body', (stream) => {
let buffer = '';
stream.on('data', (chunk) => {
buffer += chunk.toString('utf8');
});
stream.once('end', () => {
messages.push({
from: buffer.match(/From: (.*)/i)?.[1],
subject: buffer.match(/Subject: (.*)/i)?.[1],
text: buffer.split('\n\n').slice(1).join('\n\n')
});
});
});
});
fetch.once('error', reject);
fetch.once('end', () => resolve(messages));
});
});
// Добавляем задержку между обработкой писем
for (const message of messages) {
await this.processEmail(message);
await new Promise(resolve => setTimeout(resolve, 1000)); // 1 секунда между письмами
}
} catch (error) {
console.error('Ошибка при проверке новых писем:', error);
}
}
async stop() {
if (this.isRunning) {
console.log('Останавливаем Email бота...');
// Закрываем SMTP соединение
if (this.transporter) {
await this.transporter.close();
}
// Закрываем IMAP соединение
if (this.imap) {
this.imap.end();
}
this.isRunning = false;
console.log('Email бот остановлен');
}
}
}
module.exports = EmailBotService;

View File

@@ -0,0 +1,347 @@
const TelegramBot = require('node-telegram-bot-api');
const { ChatOllama } = require('@langchain/ollama');
const axios = require('axios');
const dns = require('dns').promises;
require('dotenv').config();
const { sleep } = require('../utils/helpers');
class TelegramBotService {
constructor(token) {
if (!token) {
throw new Error('Token is required');
}
this.isRunning = false;
this.maxRetries = 3;
this.retryDelay = 5000; // 5 секунд между попытками
// Создаем бота без polling
this.bot = new TelegramBot(token, {
polling: false,
request: {
proxy: null,
agentOptions: {
rejectUnauthorized: true,
minVersion: 'TLSv1.2'
},
timeout: 30000
}
});
this.token = token;
this.chat = new ChatOllama({
model: 'mistral',
baseUrl: 'http://localhost:11434'
});
// Добавляем настройки прокси для axios
this.axiosConfig = {
timeout: 5000,
proxy: false,
httpsAgent: new (require('https').Agent)({
rejectUnauthorized: true,
minVersion: 'TLSv1.2'
})
};
}
setupHandlers() {
this.bot.onText(/.*/, async (msg) => {
try {
const chatId = msg.chat.id;
const userQuestion = msg.text;
// Пропускаем команды
if (userQuestion.startsWith('/')) {
return;
}
console.log('Получен вопрос:', userQuestion);
// Используем локальную модель
const result = await this.chat.invoke(userQuestion);
const assistantResponse = result.content;
await this.bot.sendMessage(chatId, assistantResponse);
} catch (error) {
console.error('Telegram bot error:', error);
await this.bot.sendMessage(msg.chat.id,
'Извините, произошла ошибка при обработке вашего запроса. ' +
'Попробуйте повторить позже или обратитесь к администратору.'
);
}
});
}
setupCommands() {
this.bot.onText(/\/start/, async (msg) => {
const welcomeMessage = `
👋 Здравствуйте! Я - ассистент DApp for Business.
Я готов помочь вам с вопросами о:
• Разработке dApps
• Блокчейн-технологиях
• Web3 и криптовалютах
Просто задавайте вопросы, а если нужна помощь -
используйте команду /help
`;
await this.bot.sendMessage(msg.chat.id, welcomeMessage);
});
this.bot.onText(/\/help/, async (msg) => {
const helpMessage = `
🤖 Я - ассистент DApp for Business
Я могу помочь вам с:
• Разработкой децентрализованных приложений
• Интеграцией блокчейн-технологий в бизнес
• Консультациями по Web3 и криптовалютам
Команды:
/start - начать работу с ботом
/help - показать это сообщение
/status - проверить состояние бота
Просто задавайте вопросы на русском или английском языке!
`;
await this.bot.sendMessage(msg.chat.id, helpMessage);
});
this.bot.onText(/\/status/, async (msg) => {
try {
const status = {
isRunning: this.isRunning,
uptime: process.uptime(),
memoryUsage: process.memoryUsage(),
connections: {
telegram: Boolean(this.bot),
ollama: Boolean(this.chat)
}
};
const statusMessage = `
📊 Статус бота:
🟢 Статус: ${status.isRunning ? 'Работает' : 'Остановлен'}
⏱ Время работы: ${Math.floor(status.uptime / 60)} мин
🔗 Подключения:
• Telegram API: ${status.connections.telegram ? '✅' : '❌'}
• Ollama: ${status.connections.ollama ? '✅' : '❌'}
💾 Использование памяти:
• Heap: ${Math.round(status.memoryUsage.heapUsed / 1024 / 1024)}MB
• RSS: ${Math.round(status.memoryUsage.rss / 1024 / 1024)}MB
`;
await this.bot.sendMessage(msg.chat.id, statusMessage);
} catch (error) {
console.error('Ошибка при получении статуса:', error);
await this.bot.sendMessage(msg.chat.id, 'Ошибка при получении статуса бота');
}
});
}
async initialize() {
let retries = 0;
while (retries < this.maxRetries) {
try {
console.log(`Попытка инициализации Telegram бота (${retries + 1}/${this.maxRetries})...`);
// Сначала проверяем DNS и доступность
try {
console.log('Проверка DNS для api.telegram.org...');
const addresses = await dns.resolve4('api.telegram.org');
console.log('IP адреса api.telegram.org:', addresses);
// Пинг для проверки доступности
const { exec } = require('child_process');
exec(`ping -c 1 api.telegram.org`, (error, stdout, stderr) => {
console.log('Результат ping:', stdout);
});
} catch (error) {
console.error('Ошибка DNS резолвинга:', error);
throw error; // Прерываем инициализацию если DNS недоступен
}
// Затем проверяем API
try {
const response = await axios.get(
`https://api.telegram.org/bot${process.env.TELEGRAM_BOT_TOKEN}/getMe`,
this.axiosConfig
);
if (response.status !== 200) {
throw new Error(`HTTP error! status: ${response.status}`);
}
console.log('Успешное подключение к API Telegram:', {
botInfo: response.data.result
});
} catch (error) {
console.error('Ошибка при проверке API Telegram:', {
message: error.message,
code: error.code,
response: error.response?.data,
config: {
url: error.config?.url,
method: error.config?.method,
timeout: error.config?.timeout
}
});
throw error;
}
// Основная инициализация бота
await this.initBot();
console.log('Telegram bot service initialized');
return;
} catch (error) {
retries++;
console.error('Ошибка при инициализации Telegram бота:', {
name: error.name,
message: error.message,
code: error.code,
response: error.response?.data,
stack: error.stack
});
if (retries < this.maxRetries) {
console.log(`Повторная попытка через ${this.retryDelay/1000} секунд...`);
await sleep(this.retryDelay);
} else {
console.error('Превышено максимальное количество попыток подключения к Telegram');
throw error;
}
}
}
}
async initBot() {
try {
// Проверяем, не запущен ли уже бот
const webhookInfo = await this.bot.getWebHookInfo();
// Если есть webhook или активный polling, пробуем остановить
if (webhookInfo.url || webhookInfo.has_custom_certificate) {
console.log('Удаляем существующий webhook...');
await this.bot.deleteWebHook();
await new Promise(resolve => setTimeout(resolve, 2000));
}
// Пробуем получить обновления с большим таймаутом
try {
console.log('Проверяем наличие других экземпляров бота...');
const updates = await this.bot.getUpdates({
offset: -1,
limit: 1,
timeout: 0
});
console.log('Проверка существующих подключений:', updates);
} catch (error) {
if (error.code === 409) {
console.log('Обнаружен активный бот, пробуем остановить...');
await this.stop();
await new Promise(resolve => setTimeout(resolve, 5000));
// Повторная попытка получить обновления
await this.bot.getUpdates({ offset: -1, limit: 1, timeout: 0 });
}
}
// Небольшая пауза перед запуском поллинга
await new Promise(resolve => setTimeout(resolve, 1000));
// Запускаем polling
console.log('Запускаем polling...');
await this.bot.startPolling({
interval: 2000,
params: {
timeout: 10
}
});
this.isRunning = true;
this.setupHandlers();
this.setupErrorHandlers();
this.setupCommands();
} catch (error) {
if (error.code === 409) {
console.log('Бот уже запущен в другом процессе');
this.isRunning = false;
} else {
console.error('Ошибка при инициализации Telegram бота:', error);
throw error;
}
}
}
setupErrorHandlers() {
this.bot.on('polling_error', (error) => {
console.error('Telegram polling error:', {
code: error.code,
message: error.message,
stack: error.stack
});
// Обработка различных ошибок
if (this.isRunning && (error.code === 'EFATAL' || error.code === 'ETELEGRAM')) {
console.log('Переподключение к Telegram через 5 секунд...');
setTimeout(async () => {
try {
await this.stop();
await this.initBot();
} catch (err) {
console.error('Ошибка при перезапуске бота:', err);
}
}, 5000);
} else if (error.code === 'ECONNRESET' || error.code === 'ECONNREFUSED') {
// Для ошибок соединения пробуем сразу переподключиться
this.bot.startPolling();
}
});
// Обработка других ошибок
this.bot.on('error', (error) => {
console.error('Telegram bot error:', error);
// Пробуем переподключиться при любой ошибке
setTimeout(() => this.bot.startPolling(), 5000);
});
// Обработка webhook ошибок
this.bot.on('webhook_error', (error) => {
console.error('Telegram webhook error:', error);
});
}
async stop() {
if (this.isRunning) {
console.log('Останавливаем Telegram бота...');
try {
// Сначала отключаем обработчики
this.bot.removeAllListeners();
// Останавливаем поллинг
await this.bot.stopPolling();
// Очищаем очередь обновлений
await this.bot.getUpdates({
offset: -1,
limit: 1,
timeout: 1
});
this.isRunning = false;
console.log('Telegram бот остановлен');
} catch (error) {
console.error('Ошибка при остановке бота:', error);
// Принудительно отмечаем как остановленный
this.isRunning = false;
}
}
}
}
module.exports = TelegramBotService;

View File

@@ -0,0 +1,75 @@
const TelegramBot = require('node-telegram-bot-api');
const { ChatOllama } = require('@langchain/ollama');
const { PGVectorStore } = require('@langchain/community/vectorstores/pgvector');
class TelegramBotService {
constructor(token, vectorStore) {
this.bot = new TelegramBot(token, { polling: true });
this.vectorStore = vectorStore;
this.chat = new ChatOllama({
model: 'mistral',
baseUrl: 'http://localhost:11434'
});
this.userRequests = new Map(); // для отслеживания запросов
this.setupHandlers();
}
isRateLimited(userId) {
const now = Date.now();
const userReqs = this.userRequests.get(userId) || [];
// Очищаем старые запросы
const recentReqs = userReqs.filter(time => now - time < 60000);
// Максимум 10 запросов в минуту
if (recentReqs.length >= 10) return true;
recentReqs.push(now);
this.userRequests.set(userId, recentReqs);
return false;
}
setupHandlers() {
this.bot.on('message', async (msg) => {
const userId = msg.from.id;
if (this.isRateLimited(userId)) {
await this.bot.sendMessage(msg.chat.id,
'Пожалуйста, подождите минуту перед следующим запросом.');
return;
}
try {
const chatId = msg.chat.id;
const userQuestion = msg.text;
// Поиск релевантных документов
const relevantDocs = await this.vectorStore.similaritySearch(userQuestion, 3);
// Формируем контекст из найденных документов
const context = relevantDocs.map(doc => doc.pageContent).join('\n');
// Получаем ответ от LLM
const response = await this.chat.invoke([
{
role: 'system',
content: `You are a helpful assistant. Use this context to answer: ${context}`
},
{
role: 'user',
content: userQuestion
}
]);
await this.bot.sendMessage(chatId, response);
} catch (error) {
console.error('Telegram bot error:', error);
}
});
}
}
module.exports = TelegramBotService;

View File

@@ -0,0 +1,17 @@
const dns = require('dns');
const { promisify } = require('util');
const resolveMx = promisify(dns.resolveMx);
async function checkMailServer(domain) {
try {
console.log(`Проверяем MX записи для домена ${domain}...`);
const records = await resolveMx(domain);
console.log('Найдены MX записи:', records);
return records;
} catch (error) {
console.error('Ошибка при проверке MX записей:', error);
return null;
}
}
module.exports = { checkMailServer };

8
backend/utils/helpers.js Normal file
View File

@@ -0,0 +1,8 @@
// Функция для создания задержки
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
module.exports = {
sleep
};

File diff suppressed because it is too large Load Diff