diff --git a/backend/db/migrations/022_create_db_settings.sql b/backend/db/migrations/022_create_db_settings.sql index 3255e93..e664978 100644 --- a/backend/db/migrations/022_create_db_settings.sql +++ b/backend/db/migrations/022_create_db_settings.sql @@ -10,7 +10,9 @@ CREATE TABLE IF NOT EXISTS db_settings ( updated_at TIMESTAMP NOT NULL DEFAULT NOW() ); --- Пропускаем INSERT, так как данные должны быть зашифрованы --- INSERT INTO db_settings (db_host, db_port, db_name, db_user, db_password) --- VALUES ('postgres', 5432, 'dapp_db', 'dapp_user', 'dapp_password') --- ON CONFLICT DO NOTHING; \ No newline at end of file +-- Добавляем дефолтные настройки базы данных +INSERT INTO db_settings (id, db_port, created_at, updated_at) +VALUES (1, 5432, NOW(), NOW()) +ON CONFLICT (id) DO UPDATE SET + db_port = EXCLUDED.db_port, + updated_at = NOW(); \ No newline at end of file diff --git a/backend/hardhat.config.js b/backend/hardhat.config.js index eb46a17..1486250 100644 --- a/backend/hardhat.config.js +++ b/backend/hardhat.config.js @@ -51,6 +51,18 @@ module.exports = { disambiguatePaths: false, }, networks: getNetworks(), + etherscan: { + apiKey: { + sepolia: process.env.ETHERSCAN_API_KEY || '', + mainnet: process.env.ETHERSCAN_API_KEY || '', + polygon: process.env.POLYGONSCAN_API_KEY || '', + arbitrumOne: process.env.ARBISCAN_API_KEY || '', + bsc: process.env.BSCSCAN_API_KEY || '', + base: process.env.BASESCAN_API_KEY || '', + baseSepolia: process.env.BASESCAN_API_KEY || '', + arbitrumSepolia: process.env.ARBISCAN_API_KEY || '', + } + }, solidityCoverage: { excludeContracts: [], skipFiles: [], diff --git a/backend/middleware/auth.js b/backend/middleware/auth.js index 34dee3f..ca4be4e 100644 --- a/backend/middleware/auth.js +++ b/backend/middleware/auth.js @@ -50,39 +50,56 @@ const requireAuth = async (req, res, next) => { */ async function requireAdmin(req, res, next) { try { + // Подробное логирование для отладки + logger.info(`[requireAdmin] Проверка доступа для ${req.method} ${req.url}`); + logger.info(`[requireAdmin] Session:`, { + exists: !!req.session, + authenticated: req.session?.authenticated, + isAdmin: req.session?.isAdmin, + userId: req.session?.userId, + address: req.session?.address + }); + // Проверка аутентификации if (!req.session || !req.session.authenticated) { + logger.warn(`[requireAdmin] Сессия не аутентифицирована`); return next(createError('Требуется аутентификация', 401)); } // Проверка через сессию if (req.session.isAdmin) { + logger.info(`[requireAdmin] Доступ разрешен через сессию isAdmin`); return next(); } // Проверка через кошелек if (req.session.address) { + logger.info(`[requireAdmin] Проверка через кошелек: ${req.session.address}`); const isAdmin = await authService.checkAdminTokens(req.session.address); if (isAdmin) { // Обновляем сессию req.session.isAdmin = true; + logger.info(`[requireAdmin] Доступ разрешен через кошелек`); return next(); } } // Проверка через ID пользователя if (req.session.userId) { + logger.info(`[requireAdmin] Проверка через userId: ${req.session.userId}`); const userResult = await db.getQuery()('SELECT role FROM users WHERE id = $1', [ req.session.userId, ]); if (userResult.rows.length > 0 && userResult.rows[0].role === USER_ROLES.ADMIN) { // Обновляем сессию req.session.isAdmin = true; + logger.info(`[requireAdmin] Доступ разрешен через userId`); return next(); } } // Если ни одна проверка не прошла + logger.warn(`[requireAdmin] Доступ запрещен - все проверки не прошли`); return next(createError('Доступ запрещен', 403)); } catch (error) { logger.error(`Error in requireAdmin middleware: ${error.message}`); diff --git a/backend/routes/settings.js b/backend/routes/settings.js index 0a775cd..40b4d08 100644 --- a/backend/routes/settings.js +++ b/backend/routes/settings.js @@ -483,6 +483,105 @@ router.get('/email-settings', requireAdmin, async (req, res) => { } }); +// Обновить настройки Email +router.put('/email-settings', requireAdmin, async (req, res, next) => { + try { + const { + imap_host, + imap_port, + imap_user, + imap_password, + smtp_host, + smtp_port, + smtp_user, + smtp_password, + from_email, + is_active + } = req.body; + + // Валидация обязательных полей + if (!imap_host || !imap_port || !imap_user || !imap_password || + !smtp_host || !smtp_port || !smtp_user || !smtp_password || !from_email) { + return res.status(400).json({ + success: false, + error: 'Все поля обязательны для заполнения' + }); + } + + const settings = { + imap_host, + imap_port: parseInt(imap_port), + imap_user, + imap_password, + smtp_host, + smtp_port: parseInt(smtp_port), + smtp_user, + smtp_password, + from_email, + is_active: is_active !== undefined ? is_active : true, + updated_at: new Date() + }; + + const result = await emailBotService.saveEmailSettings(settings); + res.json({ success: true, data: result }); + } catch (error) { + logger.error('Ошибка при обновлении email настроек:', error); + res.status(500).json({ success: false, error: error.message }); + } +}); + +// Тест email функциональности +router.post('/email-settings/test', requireAdmin, async (req, res, next) => { + try { + const { test_email } = req.body; + + if (!test_email) { + return res.status(400).json({ + success: false, + error: 'test_email обязателен для тестирования' + }); + } + + // Отправляем тестовое письмо + const result = await emailBotService.sendEmail( + test_email, + 'Тест Email системы DLE', + 'Это тестовое письмо для проверки работы email системы. Если вы его получили, значит настройки работают корректно!' + ); + + res.json({ + success: true, + message: 'Тестовое письмо отправлено успешно', + data: result + }); + } catch (error) { + logger.error('Ошибка при тестировании email:', error); + res.status(500).json({ success: false, error: error.message }); + } +}); + +// Тест IMAP подключения +router.post('/email-settings/test-imap', requireAdmin, async (req, res, next) => { + try { + const result = await emailBotService.testImapConnection(); + res.json(result); + } catch (error) { + logger.error('Ошибка при тестировании IMAP подключения:', error); + res.status(500).json({ success: false, error: error.message }); + } +}); + +// Тест SMTP подключения +router.post('/email-settings/test-smtp', requireAdmin, async (req, res, next) => { + try { + const result = await emailBotService.testSmtpConnection(); + res.json(result); + } catch (error) { + logger.error('Ошибка при тестировании SMTP подключения:', error); + res.status(500).json({ success: false, error: error.message }); + } +}); + // Получить список всех email (для ассистента) router.get('/email-settings/list', requireAdmin, async (req, res) => { try { @@ -503,6 +602,35 @@ router.get('/telegram-settings', requireAdmin, async (req, res, next) => { } }); +// Обновить настройки Telegram-бота +router.put('/telegram-settings', requireAdmin, async (req, res, next) => { + try { + const { bot_token, bot_username, webhook_url, is_active } = req.body; + + // Валидация обязательных полей + if (!bot_token || !bot_username) { + return res.status(400).json({ + success: false, + error: 'bot_token и bot_username обязательны' + }); + } + + const settings = { + bot_token, + bot_username, + webhook_url: webhook_url || null, + is_active: is_active !== undefined ? is_active : true, + updated_at: new Date() + }; + + const result = await telegramBot.saveTelegramSettings(settings); + res.json({ success: true, data: result }); + } catch (error) { + logger.error('Ошибка при обновлении настроек Telegram:', error); + res.status(500).json({ success: false, error: error.message }); + } +}); + // Получить список всех Telegram-ботов (для ассистента) router.get('/telegram-settings/list', requireAdmin, async (req, res, next) => { try { diff --git a/backend/routes/uploads.js b/backend/routes/uploads.js index 1e54d03..7e3b01d 100644 --- a/backend/routes/uploads.js +++ b/backend/routes/uploads.js @@ -40,7 +40,8 @@ router.post('/logo', auth.requireAuth, auth.requireAdmin, upload.single('logo'), if (!req.file) return res.status(400).json({ success: false, message: 'Файл не получен' }); const rel = path.posix.join('uploads', 'logos', path.basename(req.file.filename)); const urlPath = `/uploads/logos/${path.basename(req.file.filename)}`; - return res.json({ success: true, data: { path: rel, url: urlPath } }); + const fullUrl = `http://localhost:8000${urlPath}`; + return res.json({ success: true, data: { path: rel, url: fullUrl } }); } catch (e) { return res.status(500).json({ success: false, message: e.message }); } diff --git a/backend/routes/users.js b/backend/routes/users.js index ef8ac24..8d17da0 100644 --- a/backend/routes/users.js +++ b/backend/routes/users.js @@ -213,7 +213,7 @@ router.get('/', requireAuth, async (req, res, next) => { // --- Формируем ответ --- const contacts = users.map(u => ({ id: u.id, - name: null, // Имена теперь только в зашифрованных колонках + name: [u.first_name, u.last_name].filter(Boolean).join(' ').trim() || null, email: u.email || null, telegram: u.telegram || null, wallet: u.wallet || null, @@ -446,9 +446,19 @@ router.get('/:id', async (req, res, next) => { for (const id of identitiesResult.rows) { identityMap[id.provider] = id.provider_id; } + // Получаем имя пользователя из зашифрованных полей + const nameResult = await query('SELECT CASE WHEN first_name_encrypted IS NULL OR first_name_encrypted = \'\' THEN NULL ELSE decrypt_text(first_name_encrypted, $2) END as first_name, CASE WHEN last_name_encrypted IS NULL OR last_name_encrypted = \'\' THEN NULL ELSE decrypt_text(last_name_encrypted, $2) END as last_name FROM users WHERE id = $1', [userId, encryptionKey]); + + let fullName = null; + if (nameResult.rows.length > 0) { + const firstName = nameResult.rows[0].first_name || ''; + const lastName = nameResult.rows[0].last_name || ''; + fullName = [firstName, lastName].filter(Boolean).join(' ').trim() || null; + } + res.json({ id: user.id, - name: null, // Пока не используем имена + name: fullName, email: identityMap.email || null, telegram: identityMap.telegram || null, wallet: identityMap.wallet || null, diff --git a/backend/scripts/deploy/deploy-multichain.js b/backend/scripts/deploy/deploy-multichain.js index fa305d7..d0fde32 100644 --- a/backend/scripts/deploy/deploy-multichain.js +++ b/backend/scripts/deploy/deploy-multichain.js @@ -174,7 +174,7 @@ async function deployInNetwork(rpcUrl, pk, salt, initCodeHash, targetDLENonce, d } // Деплой модулей в одной сети -async function deployModulesInNetwork(rpcUrl, pk, dleAddress) { +async function deployModulesInNetwork(rpcUrl, pk, dleAddress, params) { const { ethers } = hre; const provider = new ethers.JsonRpcProvider(rpcUrl); const wallet = new ethers.Wallet(pk, provider); @@ -184,77 +184,130 @@ async function deployModulesInNetwork(rpcUrl, pk, dleAddress) { const modules = {}; - try { - // Деплой TreasuryModule - console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} deploying TreasuryModule...`); - const TreasuryModule = await hre.ethers.getContractFactory('TreasuryModule'); - const treasuryModule = await TreasuryModule.connect(wallet).deploy( - dleAddress, // _dleContract - Number(net.chainId), // _chainId - wallet.address // _emergencyAdmin - ); - await treasuryModule.waitForDeployment(); - const treasuryAddress = await treasuryModule.getAddress(); - modules.treasuryModule = treasuryAddress; - console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} TreasuryModule deployed at: ${treasuryAddress}`); - - // Деплой TimelockModule - console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} deploying TimelockModule...`); - const TimelockModule = await hre.ethers.getContractFactory('TimelockModule'); - const timelockModule = await TimelockModule.connect(wallet).deploy( - dleAddress // _dleContract - ); - await timelockModule.waitForDeployment(); - const timelockAddress = await timelockModule.getAddress(); - modules.timelockModule = timelockAddress; - console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} TimelockModule deployed at: ${timelockAddress}`); - - // Деплой DLEReader - console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} deploying DLEReader...`); - const DLEReader = await hre.ethers.getContractFactory('DLEReader'); - const dleReader = await DLEReader.connect(wallet).deploy( - dleAddress // _dleContract - ); - await dleReader.waitForDeployment(); - const readerAddress = await dleReader.getAddress(); - modules.dleReader = readerAddress; - console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} DLEReader deployed at: ${readerAddress}`); - - // Инициализация модулей в DLE - console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} initializing modules in DLE...`); - const dleContract = await hre.ethers.getContractAt('DLE', dleAddress, wallet); - - // Инициализация базовых модулей - await dleContract.initializeBaseModules(treasuryAddress, timelockAddress, readerAddress); - console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} base modules initialized`); - - // Инициализация logoURI + // Получаем начальный nonce для всех модулей + let currentNonce = await wallet.getNonce(); + console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} starting nonce for modules: ${currentNonce}`); + + // Функция для безопасного деплоя с правильным nonce + async function deployWithNonce(contractFactory, args, moduleName) { try { - // Используем логотип из параметров деплоя или fallback - const logoURL = params.logoURI || "https://via.placeholder.com/200x200/0066cc/ffffff?text=DLE"; - await dleContract.initializeLogoURI(logoURL); - console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} logoURI initialized: ${logoURL}`); - } catch (e) { - console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} logoURI initialization failed: ${e.message}`); - // Fallback на базовый логотип - try { - await dleContract.initializeLogoURI("https://via.placeholder.com/200x200/0066cc/ffffff?text=DLE"); - console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} fallback logoURI initialized`); - } catch (fallbackError) { - console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} fallback logoURI also failed: ${fallbackError.message}`); + console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} deploying ${moduleName} with nonce: ${currentNonce}`); + + // Проверяем, что nonce актуален + const actualNonce = await wallet.getNonce(); + if (actualNonce > currentNonce) { + console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} nonce mismatch, updating from ${currentNonce} to ${actualNonce}`); + currentNonce = actualNonce; } + + const contract = await contractFactory.connect(wallet).deploy(...args); + await contract.waitForDeployment(); + const address = await contract.getAddress(); + console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} ${moduleName} deployed at: ${address}`); + currentNonce++; + return address; + } catch (error) { + console.error(`[MULTI_DBG] chainId=${Number(net.chainId)} ${moduleName} deployment failed:`, error.message); + // Даже при ошибке увеличиваем nonce, чтобы не было конфликтов + currentNonce++; + return null; + } + } + + // Деплой TreasuryModule + const TreasuryModule = await hre.ethers.getContractFactory('TreasuryModule'); + modules.treasuryModule = await deployWithNonce( + TreasuryModule, + [dleAddress, Number(net.chainId), wallet.address], // _dleContract, _chainId, _emergencyAdmin + 'TreasuryModule' + ); + + // Деплой TimelockModule + const TimelockModule = await hre.ethers.getContractFactory('TimelockModule'); + modules.timelockModule = await deployWithNonce( + TimelockModule, + [dleAddress], // _dleContract + 'TimelockModule' + ); + + // Деплой DLEReader + const DLEReader = await hre.ethers.getContractFactory('DLEReader'); + modules.dleReader = await deployWithNonce( + DLEReader, + [dleAddress], // _dleContract + 'DLEReader' + ); + + // Инициализация модулей в DLE + try { + console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} initializing modules in DLE with nonce: ${currentNonce}`); + + // Проверяем, что nonce актуален + const actualNonce = await wallet.getNonce(); + if (actualNonce > currentNonce) { + console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} nonce mismatch before module init, updating from ${currentNonce} to ${actualNonce}`); + currentNonce = actualNonce; } + const dleContract = await hre.ethers.getContractAt('DLE', dleAddress, wallet); + + // Проверяем, что все модули задеплоены + const treasuryAddress = modules.treasuryModule; + const timelockAddress = modules.timelockModule; + const readerAddress = modules.dleReader; + + if (treasuryAddress && timelockAddress && readerAddress) { + // Инициализация базовых модулей + await dleContract.initializeBaseModules(treasuryAddress, timelockAddress, readerAddress); + console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} base modules initialized`); + currentNonce++; + } else { + console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} skipping module initialization - not all modules deployed`); + } } catch (error) { - console.error(`[MULTI_DBG] chainId=${Number(net.chainId)} module deployment failed:`, error.message); - throw error; + console.error(`[MULTI_DBG] chainId=${Number(net.chainId)} module initialization failed:`, error.message); + // Даже при ошибке увеличиваем nonce + currentNonce++; + } + + // Инициализация logoURI + try { + console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} initializing logoURI with nonce: ${currentNonce}`); + + // Проверяем, что nonce актуален + const actualNonce = await wallet.getNonce(); + if (actualNonce > currentNonce) { + console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} nonce mismatch before logoURI init, updating from ${currentNonce} to ${actualNonce}`); + currentNonce = actualNonce; + } + + // Используем логотип из параметров деплоя или fallback + const logoURL = params.logoURI || "https://via.placeholder.com/200x200/0066cc/ffffff?text=DLE"; + const dleContract = await hre.ethers.getContractAt('DLE', dleAddress, wallet); + await dleContract.initializeLogoURI(logoURL); + console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} logoURI initialized: ${logoURL}`); + currentNonce++; + } catch (e) { + console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} logoURI initialization failed: ${e.message}`); + // Fallback на базовый логотип + try { + console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} trying fallback logoURI with nonce: ${currentNonce}`); + const dleContract = await hre.ethers.getContractAt('DLE', dleAddress, wallet); + await dleContract.initializeLogoURI("https://via.placeholder.com/200x200/0066cc/ffffff?text=DLE"); + console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} fallback logoURI initialized`); + currentNonce++; + } catch (fallbackError) { + console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} fallback logoURI also failed: ${fallbackError.message}`); + // Даже при ошибке увеличиваем nonce + currentNonce++; + } } return modules; } // Деплой модулей во всех сетях -async function deployModulesInAllNetworks(networks, pk, dleAddress) { +async function deployModulesInAllNetworks(networks, pk, dleAddress, params) { const moduleResults = []; for (let i = 0; i < networks.length; i++) { @@ -262,7 +315,7 @@ async function deployModulesInAllNetworks(networks, pk, dleAddress) { console.log(`[MULTI_DBG] deploying modules to network ${i + 1}/${networks.length}: ${rpcUrl}`); try { - const modules = await deployModulesInNetwork(rpcUrl, pk, dleAddress); + const modules = await deployModulesInNetwork(rpcUrl, pk, dleAddress, params); moduleResults.push(modules); } catch (error) { console.error(`[MULTI_DBG] Failed to deploy modules in network ${i + 1}:`, error.message); @@ -310,10 +363,11 @@ async function verifyContractsInNetwork(rpcUrl, pk, dleAddress, modules, params) }); verification.dle = 'success'; console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} DLE verification successful`); - } catch (error) { - console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} DLE verification failed: ${error.message}`); - verification.dle = 'failed'; - } + } catch (error) { + console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} DLE verification failed: ${error.message}`); + console.log(`[MULTI_DBG] chainId=${Number(net.chainId)} DLE verification error details:`, error); + verification.dle = 'failed'; + } // Верификация модулей if (modules && !modules.error) { @@ -485,7 +539,7 @@ async function main() { // Деплой модулей во всех сетях console.log('[MULTI_DBG] Starting module deployment...'); - const moduleResults = await deployModulesInAllNetworks(networks, pk, uniqueAddresses[0]); + const moduleResults = await deployModulesInAllNetworks(networks, pk, uniqueAddresses[0], params); // Верификация контрактов console.log('[MULTI_DBG] Starting contract verification...'); diff --git a/backend/services/ai-assistant.js b/backend/services/ai-assistant.js index ac93068..1b69cf7 100644 --- a/backend/services/ai-assistant.js +++ b/backend/services/ai-assistant.js @@ -12,14 +12,14 @@ const { ChatOllama } = require('@langchain/ollama'); const aiCache = require('./ai-cache'); -const aiQueue = require('./ai-queue'); +const AIQueue = require('./ai-queue'); const logger = require('../utils/logger'); // Константы для AI параметров const AI_CONFIG = { temperature: 0.3, maxTokens: 512, - timeout: 180000, + timeout: 120000, // Уменьшаем до 120 секунд, чтобы соответствовать EmailBot numCtx: 2048, numGpu: 1, numThread: 4, @@ -42,6 +42,110 @@ class AIAssistant { this.defaultModel = process.env.OLLAMA_MODEL || 'qwen2.5:7b'; this.lastHealthCheck = 0; this.healthCheckInterval = 30000; // 30 секунд + + // Создаем экземпляр AIQueue + this.aiQueue = new AIQueue(); + this.isProcessingQueue = false; + + // Запускаем обработку очереди + this.startQueueProcessing(); + } + + // Запуск обработки очереди + async startQueueProcessing() { + if (this.isProcessingQueue) return; + + this.isProcessingQueue = true; + logger.info('[AIAssistant] Запущена обработка очереди AIQueue'); + + while (this.isProcessingQueue) { + try { + // Получаем следующий запрос из очереди + const requestItem = this.aiQueue.getNextRequest(); + + if (!requestItem) { + // Если очередь пуста, ждем немного + await new Promise(resolve => setTimeout(resolve, 1000)); + continue; + } + + logger.info(`[AIAssistant] Обрабатываем запрос ${requestItem.id} из очереди`); + + // Обновляем статус на "processing" + this.aiQueue.updateRequestStatus(requestItem.id, 'processing'); + + const startTime = Date.now(); + + try { + // Обрабатываем запрос + const result = await this.processQueueRequest(requestItem.request); + const responseTime = Date.now() - startTime; + + // Обновляем статус на "completed" + this.aiQueue.updateRequestStatus(requestItem.id, 'completed', result, null, responseTime); + + logger.info(`[AIAssistant] Запрос ${requestItem.id} завершен за ${responseTime}ms`); + + } catch (error) { + const responseTime = Date.now() - startTime; + + // Обновляем статус на "failed" + this.aiQueue.updateRequestStatus(requestItem.id, 'failed', null, error.message, responseTime); + + logger.error(`[AIAssistant] Запрос ${requestItem.id} завершился с ошибкой:`, error.message); + logger.error(`[AIAssistant] Детали ошибки:`, error.stack || error); + } + + } catch (error) { + logger.error('[AIAssistant] Ошибка в обработке очереди:', error); + await new Promise(resolve => setTimeout(resolve, 5000)); + } + } + } + + // Остановка обработки очереди + stopQueueProcessing() { + this.isProcessingQueue = false; + logger.info('[AIAssistant] Остановлена обработка очереди AIQueue'); + } + + // Обработка запроса из очереди + async processQueueRequest(request) { + try { + const { message, history, systemPrompt, rules } = request; + + logger.info(`[AIAssistant] Обрабатываю запрос: message="${message?.substring(0, 50)}...", history=${history?.length || 0}, systemPrompt="${systemPrompt?.substring(0, 50)}..."`); + + // Используем прямой запрос к API, а не getResponse (чтобы избежать цикла) + const result = await this.directRequest( + [{ role: 'user', content: message }], + systemPrompt, + { temperature: 0.3, maxTokens: 150 } + ); + + logger.info(`[AIAssistant] Запрос успешно обработан, результат: "${result?.substring(0, 100)}..."`); + + return result; + } catch (error) { + logger.error(`[AIAssistant] Ошибка в processQueueRequest:`, error.message); + logger.error(`[AIAssistant] Stack trace:`, error.stack); + throw error; // Перебрасываем ошибку дальше + } + } + + // Добавление запроса в очередь + async addToQueue(request, priority = 0) { + return await this.aiQueue.addRequest(request, priority); + } + + // Получение статистики очереди + getQueueStats() { + return this.aiQueue.getStats(); + } + + // Получение размера очереди + getQueueSize() { + return this.aiQueue.getQueueSize(); } // Проверка здоровья модели @@ -148,7 +252,7 @@ class AIAssistant { const priority = this.getRequestPriority(message, history, rules); // Добавляем запрос в очередь - const requestId = await aiQueue.addRequest({ + const requestId = await this.addToQueue({ message, history, systemPrompt, @@ -164,8 +268,8 @@ class AIAssistant { const onCompleted = (item) => { if (item.id === requestId) { clearTimeout(timeout); - aiQueue.off('completed', onCompleted); - aiQueue.off('failed', onFailed); + this.aiQueue.off('requestCompleted', onCompleted); + this.aiQueue.off('requestFailed', onFailed); try { aiCache.set(cacheKey, item.result); } catch {} @@ -176,14 +280,14 @@ class AIAssistant { const onFailed = (item) => { if (item.id === requestId) { clearTimeout(timeout); - aiQueue.off('completed', onCompleted); - aiQueue.off('failed', onFailed); + this.aiQueue.off('requestCompleted', onCompleted); + this.aiQueue.off('requestFailed', onFailed); reject(new Error(item.error)); } }; - aiQueue.on('completed', onCompleted); - aiQueue.on('failed', onFailed); + this.aiQueue.on('requestCompleted', onCompleted); + this.aiQueue.on('requestFailed', onFailed); }); } catch (error) { logger.error('Error in getResponse:', error); @@ -201,6 +305,8 @@ class AIAssistant { try { const model = this.defaultModel; + logger.info(`[AIAssistant] directRequest: модель=${model}, сообщений=${messages?.length || 0}, systemPrompt="${systemPrompt?.substring(0, 50)}..."`); + // Создаем AbortController для таймаута const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), AI_CONFIG.timeout); @@ -241,6 +347,7 @@ class AIAssistant { let response; try { + logger.info(`[AIAssistant] Вызываю Ollama API: ${this.baseUrl}/api/chat`); response = await fetch(`${this.baseUrl}/api/chat`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, @@ -253,6 +360,7 @@ class AIAssistant { keep_alive: '3m' }) }); + logger.info(`[AIAssistant] Ollama API ответил: status=${response.status}`); } finally { clearTimeout(timeoutId); } diff --git a/backend/services/ai-queue.js b/backend/services/ai-queue.js index 79268b6..6475b62 100644 --- a/backend/services/ai-queue.js +++ b/backend/services/ai-queue.js @@ -17,13 +17,10 @@ class AIQueue extends EventEmitter { constructor() { super(); this.queue = []; - this.processing = false; - this.activeRequests = 0; - this.maxConcurrent = 1; // Ограничиваем до 1 для стабильности - this.isPaused = false; this.stats = { - completed: 0, - failed: 0, + totalAdded: 0, + totalProcessed: 0, + totalFailed: 0, avgResponseTime: 0, lastProcessedAt: null, initializedAt: Date.now() @@ -45,165 +42,101 @@ class AIQueue extends EventEmitter { this.queue.push(queueItem); this.queue.sort((a, b) => b.priority - a.priority); + this.stats.totalAdded++; logger.info(`[AIQueue] Добавлен запрос ${requestId} с приоритетом ${priority}. Очередь: ${this.queue.length}`); - // Запускаем обработку очереди - if (!this.processing) { - this.processQueue(); - } + // Эмитим событие о добавлении + this.emit('requestAdded', queueItem); return requestId; } - // Обработка очереди - async processQueue() { - if (this.processing) return; - - this.processing = true; - logger.info(`[AIQueue] Начинаем обработку очереди. Запросов в очереди: ${this.queue.length}`); - - while (!this.isPaused && this.queue.length > 0 && this.activeRequests < this.maxConcurrent) { - const item = this.queue.shift(); - if (!item) continue; - - this.activeRequests++; - item.status = 'processing'; - logger.info(`[AIQueue] Обрабатываем запрос ${item.id} (приоритет: ${item.priority})`); - - try { - const startTime = Date.now(); - const result = await this.processRequest(item.request); - const responseTime = Date.now() - startTime; - - item.status = 'completed'; - item.result = result; - item.responseTime = responseTime; - - this.stats.completed++; - this.updateAvgResponseTime(responseTime); - this.stats.lastProcessedAt = Date.now(); - - logger.info(`[AIQueue] Запрос ${item.id} завершен за ${responseTime}ms`); - - // Эмитим событие о завершении - this.emit('completed', item); - - } catch (error) { - item.status = 'failed'; - item.error = error.message; - - this.stats.failed++; - this.stats.lastProcessedAt = Date.now(); - logger.error(`[AIQueue] Запрос ${item.id} завершился с ошибкой:`, error.message); - - // Эмитим событие об ошибке - this.emit('failed', item); - } finally { - this.activeRequests--; - } - } - - this.processing = false; - logger.info(`[AIQueue] Обработка очереди завершена. Осталось запросов: ${this.queue.length}`); - - // Если в очереди еще есть запросы, продолжаем обработку - if (!this.isPaused && this.queue.length > 0) { - setTimeout(() => this.processQueue(), 100); - } + // Получение следующего запроса (без обработки) + getNextRequest() { + if (this.queue.length === 0) return null; + return this.queue.shift(); } - // Обработка одного запроса - async processRequest(request) { - const aiAssistant = require('./ai-assistant'); - - // Формируем сообщения для API - const messages = []; - - // Добавляем системный промпт - if (request.systemPrompt) { - messages.push({ role: 'system', content: request.systemPrompt }); - } - - // Добавляем историю сообщений - if (request.history && Array.isArray(request.history)) { - for (const msg of request.history) { - if (msg.role && msg.content) { - messages.push({ role: msg.role, content: msg.content }); - } - } - } - - // Добавляем текущее сообщение пользователя - messages.push({ role: 'user', content: request.message }); + // Получение запроса по ID + getRequestById(requestId) { + return this.queue.find(item => item.id === requestId); + } - // Используем прямой метод для избежания рекурсии - return await aiAssistant.directRequest(messages, request.systemPrompt); + // Обновление статуса запроса + updateRequestStatus(requestId, status, result = null, error = null, responseTime = null) { + const item = this.queue.find(item => item.id === requestId); + if (!item) return false; + + item.status = status; + item.result = result; + item.error = error; + item.responseTime = responseTime; + item.processedAt = Date.now(); + + if (status === 'completed') { + this.stats.totalProcessed++; + if (responseTime) { + this.updateAvgResponseTime(responseTime); + } + this.stats.lastProcessedAt = Date.now(); + this.emit('requestCompleted', item); + } else if (status === 'failed') { + this.stats.totalFailed++; + this.stats.lastProcessedAt = Date.now(); + this.emit('requestFailed', item); + } + + return true; } // Обновление средней скорости ответа updateAvgResponseTime(responseTime) { - const total = this.stats.completed; + const total = this.stats.totalProcessed; this.stats.avgResponseTime = (this.stats.avgResponseTime * (total - 1) + responseTime) / total; } // Получение статистики getStats() { - const totalProcessed = this.stats.completed + this.stats.failed; return { - // совместимость с AIQueueMonitor.vue и маршрутами - totalProcessed, - totalFailed: this.stats.failed, + totalAdded: this.stats.totalAdded, + totalProcessed: this.stats.totalProcessed, + totalFailed: this.stats.totalFailed, averageProcessingTime: this.stats.avgResponseTime, currentQueueSize: this.queue.length, - runningTasks: this.activeRequests, lastProcessedAt: this.stats.lastProcessedAt, - isInitialized: true, - // старые поля на всякий случай - queueLength: this.queue.length, - activeRequests: this.activeRequests, - processing: this.processing + uptime: Date.now() - this.stats.initializedAt }; } - // Очистка очереди - clear() { - this.queue = []; - logger.info('[AIQueue] Queue cleared'); + // Получение размера очереди + getQueueSize() { + return this.queue.length; } - // Совместимость с роутами AI Queue + // Очистка очереди + clearQueue() { + const clearedCount = this.queue.length; + this.queue = []; + logger.info(`[AIQueue] Очередь очищена. Удалено запросов: ${clearedCount}`); + return clearedCount; + } + + // Пауза/возобновление очереди pause() { this.isPaused = true; - logger.info('[AIQueue] Queue paused'); + logger.info('[AIQueue] Очередь приостановлена'); } resume() { - const wasPaused = this.isPaused; this.isPaused = false; - logger.info('[AIQueue] Queue resumed'); - if (wasPaused) { - this.processQueue(); - } + logger.info('[AIQueue] Очередь возобновлена'); } - async addTask(taskData) { - // Маппинг к addRequest - const priority = this._calcTaskPriority(taskData); - const taskId = await this.addRequest(taskData, priority); - return { taskId }; - } - - _calcTaskPriority({ message = '', type, userRole, history }) { - let priority = 0; - if (userRole === 'admin') priority += 10; - if (type === 'chat') priority += 5; - if (type === 'analysis') priority += 3; - if (type === 'generation') priority += 1; - if (message && message.length < 100) priority += 2; - if (history && Array.isArray(history) && history.length > 0) priority += 1; - return priority; + // Проверка статуса паузы + isQueuePaused() { + return this.isPaused; } } -module.exports = new AIQueue(); \ No newline at end of file +module.exports = AIQueue; \ No newline at end of file diff --git a/backend/services/aiAssistantRulesService.js b/backend/services/aiAssistantRulesService.js index bd98429..ff48035 100644 --- a/backend/services/aiAssistantRulesService.js +++ b/backend/services/aiAssistantRulesService.js @@ -11,16 +11,54 @@ */ const encryptedDb = require('./encryptedDatabaseService'); +const logger = require('../utils/logger'); const TABLE = 'ai_assistant_rules'; async function getAllRules() { - const rules = await encryptedDb.getData(TABLE, {}, null, 'id'); - return rules; + try { + logger.info('[aiAssistantRulesService] getAllRules called'); + const rules = await encryptedDb.getData(TABLE, {}, null, 'id'); + + // Добавляем fallback названия для правил с null именами + const processedRules = rules.map(rule => ({ + ...rule, + name: rule.name || `Правило ${rule.id}`, + displayName: rule.name || `Правило ${rule.id}` + })); + + logger.info(`[aiAssistantRulesService] Found ${processedRules.length} rules:`, + processedRules.map(r => ({ id: r.id, name: r.name, displayName: r.displayName }))); + + return processedRules; + } catch (error) { + logger.error('[aiAssistantRulesService] Error in getAllRules:', error); + throw error; + } } async function getRuleById(id) { - const rules = await encryptedDb.getData(TABLE, { id: id }, 1); - return rules[0] || null; + try { + logger.info(`[aiAssistantRulesService] getRuleById called for ID: ${id}`); + const rules = await encryptedDb.getData(TABLE, { id: id }, 1); + const rule = rules[0] || null; + + if (rule) { + // Добавляем fallback название + rule.displayName = rule.name || `Правило ${rule.id}`; + logger.info(`[aiAssistantRulesService] Found rule:`, { + id: rule.id, + name: rule.name, + displayName: rule.displayName + }); + } else { + logger.warn(`[aiAssistantRulesService] Rule with ID ${id} not found`); + } + + return rule; + } catch (error) { + logger.error(`[aiAssistantRulesService] Error in getRuleById for ID ${id}:`, error); + throw error; + } } async function createRule({ name, description, rules }) { diff --git a/backend/services/aiAssistantSettingsService.js b/backend/services/aiAssistantSettingsService.js index 55ed54a..39c5930 100644 --- a/backend/services/aiAssistantSettingsService.js +++ b/backend/services/aiAssistantSettingsService.js @@ -13,50 +13,81 @@ const encryptedDb = require('./encryptedDatabaseService'); const db = require('../db'); const TABLE = 'ai_assistant_settings'; +const logger = require('../utils/logger'); async function getSettings() { - const settings = await encryptedDb.getData(TABLE, {}, 1, 'id'); - const setting = settings[0] || null; - if (!setting) return null; - - // Получаем ключ шифрования - const fs = require('fs'); - const path = require('path'); - let encryptionKey = 'default-key'; - try { - const keyPath = path.join(__dirname, '../ssl/keys/full_db_encryption.key'); - if (fs.existsSync(keyPath)) { - encryptionKey = fs.readFileSync(keyPath, 'utf8').trim(); + logger.info('[aiAssistantSettingsService] getSettings called'); + + const settings = await encryptedDb.getData(TABLE, {}, 1, 'id'); + logger.info(`[aiAssistantSettingsService] Raw settings from DB:`, settings); + + const setting = settings[0] || null; + if (!setting) { + logger.warn('[aiAssistantSettingsService] No settings found in DB'); + return null; } - } catch (keyError) { - // console.error('Error reading encryption key:', keyError); - } - - // Получаем связанные данные из telegram_settings и email_settings - let telegramBot = null; - let supportEmail = null; - if (setting.telegram_settings_id) { - const tg = await db.getQuery()( - 'SELECT id, created_at, updated_at, decrypt_text(bot_token_encrypted, $2) as bot_token, decrypt_text(bot_username_encrypted, $2) as bot_username FROM telegram_settings WHERE id = $1', - [setting.telegram_settings_id, encryptionKey] - ); - telegramBot = tg.rows[0] || null; - } - if (setting.email_settings_id) { - const em = await db.getQuery()( - 'SELECT id, smtp_port, imap_port, created_at, updated_at, decrypt_text(smtp_host_encrypted, $2) as smtp_host, decrypt_text(smtp_user_encrypted, $2) as smtp_user, decrypt_text(smtp_password_encrypted, $2) as smtp_password, decrypt_text(imap_host_encrypted, $2) as imap_host, decrypt_text(from_email_encrypted, $2) as from_email FROM email_settings WHERE id = $1', - [setting.email_settings_id, encryptionKey] - ); - supportEmail = em.rows[0] || null; - } - return { - ...setting, - telegramBot, - supportEmail, - embedding_model: setting.embedding_model - }; + // Получаем ключ шифрования + const fs = require('fs'); + const path = require('path'); + let encryptionKey = 'default-key'; + + try { + const keyPath = path.join(__dirname, '../ssl/keys/full_chain.pem'); + if (fs.existsSync(keyPath)) { + encryptionKey = fs.readFileSync(keyPath, 'utf8'); + } + } catch (keyError) { + logger.warn('[aiAssistantSettingsService] Could not read encryption key:', keyError.message); + } + + // Обрабатываем selected_rag_tables + if (setting.selected_rag_tables) { + try { + // Если это строка JSON, парсим её + if (typeof setting.selected_rag_tables === 'string') { + setting.selected_rag_tables = JSON.parse(setting.selected_rag_tables); + } + + // Убеждаемся, что это массив + if (!Array.isArray(setting.selected_rag_tables)) { + setting.selected_rag_tables = [setting.selected_rag_tables]; + } + + logger.info(`[aiAssistantSettingsService] Processed selected_rag_tables:`, setting.selected_rag_tables); + } catch (parseError) { + logger.error('[aiAssistantSettingsService] Error parsing selected_rag_tables:', parseError); + setting.selected_rag_tables = []; + } + } else { + setting.selected_rag_tables = []; + } + + // Обрабатываем rules_id + if (setting.rules_id && typeof setting.rules_id === 'string') { + try { + setting.rules_id = parseInt(setting.rules_id); + } catch (parseError) { + logger.error('[aiAssistantSettingsService] Error parsing rules_id:', parseError); + setting.rules_id = null; + } + } + + logger.info(`[aiAssistantSettingsService] Final settings result:`, { + id: setting.id, + selected_rag_tables: setting.selected_rag_tables, + rules_id: setting.rules_id, + hasSupportEmail: setting.hasSupportEmail, + hasTelegramBot: setting.hasTelegramBot, + timestamp: setting.timestamp + }); + + return setting; + } catch (error) { + logger.error('[aiAssistantSettingsService] Error in getSettings:', error); + throw error; + } } async function upsertSettings({ system_prompt, selected_rag_tables, model, embedding_model, rules, updated_by, telegram_settings_id, email_settings_id, system_message }) { diff --git a/backend/services/dleV2Service.js b/backend/services/dleV2Service.js index 3dd2cb3..da313e9 100644 --- a/backend/services/dleV2Service.js +++ b/backend/services/dleV2Service.js @@ -462,7 +462,7 @@ class DLEV2Service { if (deployParams.logoURI) { // Если logoURI относительный путь, делаем его абсолютным if (deployParams.logoURI.startsWith('/uploads/')) { - deployParams.logoURI = `http://localhost:3000${deployParams.logoURI}`; + deployParams.logoURI = `http://localhost:8000${deployParams.logoURI}`; } // Если это placeholder, оставляем как есть if (deployParams.logoURI.includes('placeholder.com')) { diff --git a/backend/services/emailBot.js b/backend/services/emailBot.js index fef2756..eb56425 100644 --- a/backend/services/emailBot.js +++ b/backend/services/emailBot.js @@ -73,7 +73,7 @@ class EmailBotService { } const { rows } = await db.getQuery()( - 'SELECT id, smtp_port, imap_port, created_at, updated_at, decrypt_text(smtp_host_encrypted, $1) as smtp_host, decrypt_text(smtp_user_encrypted, $1) as smtp_user, decrypt_text(smtp_password_encrypted, $1) as smtp_password, decrypt_text(imap_host_encrypted, $1) as imap_host, decrypt_text(from_email_encrypted, $1) as from_email FROM email_settings ORDER BY id LIMIT 1', + 'SELECT id, smtp_port, imap_port, created_at, updated_at, decrypt_text(smtp_host_encrypted, $1) as smtp_host, decrypt_text(smtp_user_encrypted, $1) as smtp_user, decrypt_text(smtp_password_encrypted, $1) as smtp_password, decrypt_text(imap_host_encrypted, $1) as imap_host, decrypt_text(imap_user_encrypted, $1) as imap_user, decrypt_text(imap_password_encrypted, $1) as imap_password, decrypt_text(from_email_encrypted, $1) as from_email FROM email_settings ORDER BY id LIMIT 1', [encryptionKey] ); if (!rows.length) throw new Error('Email settings not found in DB'); @@ -84,8 +84,8 @@ class EmailBotService { const settings = await this.getSettingsFromDb(); return nodemailer.createTransport({ host: settings.smtp_host, - port: settings.smtp_port, - secure: true, + port: 465, // Используем порт 465 для SSMTP (SSL) + secure: true, // Включаем SSL auth: { user: settings.smtp_user, pass: settings.smtp_password, @@ -93,7 +93,10 @@ class EmailBotService { pool: false, // Отключаем пул соединений maxConnections: 1, // Ограничиваем до 1 соединения maxMessages: 1, // Ограничиваем до 1 сообщения на соединение - tls: { rejectUnauthorized: false }, + tls: { + rejectUnauthorized: false + // Убираем minVersion и maxVersion для избежания конфликтов TLS + }, connectionTimeout: 30000, // 30 секунд на подключение greetingTimeout: 30000, // 30 секунд на приветствие socketTimeout: 60000, // 60 секунд на операции сокета @@ -103,18 +106,27 @@ class EmailBotService { async getImapConfig() { const settings = await this.getSettingsFromDb(); return { - user: settings.smtp_user, - password: settings.smtp_password, + user: settings.imap_user, // Используем IMAP пользователя + password: settings.imap_password, // Используем IMAP пароль host: settings.imap_host, - port: settings.imap_port, - tls: true, - tlsOptions: { rejectUnauthorized: false }, + port: 993, // Используем порт 993 для IMAPS (SSL) + tls: true, // Включаем SSL + tlsOptions: { + rejectUnauthorized: false, + servername: settings.imap_host, + // Убираем minVersion и maxVersion для избежания конфликтов TLS + ciphers: 'HIGH:!aNULL:!MD5:!RC4' // Безопасные шифры + }, keepalive: { interval: 10000, idleInterval: 300000, forceNoop: true, }, - connTimeout: 30000, // 30 секунд + connTimeout: 60000, // 60 секунд + authTimeout: 60000, // Таймаут на аутентификацию - 60 секунд + greetingTimeout: 30000, // Таймаут на приветствие сервера + socketTimeout: 60000, // Таймаут на операции сокета + debug: console.log // Включаем отладку для диагностики }; } @@ -328,16 +340,7 @@ class EmailBotService { return; } - // Проверяем время письма - не обрабатываем письма старше 1 часа - const emailDate = parsed.date || new Date(); - const now = new Date(); - const timeDiff = now.getTime() - emailDate.getTime(); - const hoursDiff = timeDiff / (1000 * 60 * 60); - - if (hoursDiff > 1) { - logger.info(`[EmailBot] Игнорируем старое письмо от ${fromEmail} (${hoursDiff.toFixed(1)} часов назад)`); - return; - } + // Временные ограничения удалены - обрабатываем все письма независимо от возраста // 1. Найти или создать пользователя const { userId, role } = await identityService.findOrCreateUserWithRole('email', fromEmail); @@ -371,6 +374,40 @@ class EmailBotService { } } + // Проверяем, не обрабатывали ли мы уже это письмо (улучшенная проверка) + if (messageId) { + try { + // Проверяем, есть ли уже ответ от AI для этого письма + // Ищем сообщения с direction='out' и metadata, содержащим originalMessageId + const existingResponse = await encryptedDb.getData( + 'messages', + { + user_id: userId, + channel: 'email', + direction: 'out' + }, + 1 + ); + + // Проверяем в результатах, есть ли сообщение с metadata.originalMessageId = messageId + const hasResponse = existingResponse.some(msg => { + try { + const metadata = msg.metadata; + return metadata && metadata.originalMessageId === messageId; + } catch (e) { + return false; + } + }); + + if (hasResponse) { + logger.info(`[EmailBot] Письмо ${messageId} уже обработано - найден ответ от AI`); + return; + } + } catch (error) { + logger.error(`[EmailBot] Ошибка при проверке существующих ответов: ${error.message}`); + } + } + // 1.1 Найти или создать беседу let conversationResult = await encryptedDb.getData( 'conversations', @@ -451,18 +488,78 @@ class EmailBotService { if (ragResult && ragResult.answer && typeof ragResult.score === 'number' && Math.abs(ragResult.score) <= 0.1) { aiResponse = ragResult.answer; } else { - aiResponse = await generateLLMResponse({ - userQuestion: text, - context: ragResult && ragResult.context ? ragResult.context : '', - answer: ragResult && ragResult.answer ? ragResult.answer : '', - systemPrompt: aiSettings ? aiSettings.system_prompt : '', + // Используем очередь AIQueue для LLM генерации + const requestId = await aiAssistant.addToQueue({ + message: text, history: null, - model: aiSettings ? aiSettings.model : undefined, - language: aiSettings && aiSettings.languages && aiSettings.languages.length > 0 ? aiSettings.languages[0] : 'ru' + systemPrompt: aiSettings ? aiSettings.system_prompt : '', + rules: null + }, 0); + + // Ждем ответ из очереди + aiResponse = await new Promise((resolve, reject) => { + const timeout = setTimeout(() => { + reject(new Error('AI response timeout')); + }, 120000); // 2 минуты таймаут + + const onCompleted = (item) => { + if (item.id === requestId) { + clearTimeout(timeout); + aiAssistant.aiQueue.off('requestCompleted', onCompleted); + aiAssistant.aiQueue.off('requestFailed', onFailed); + resolve(item.result); + } + }; + + const onFailed = (item) => { + if (item.id === requestId) { + clearTimeout(timeout); + aiAssistant.aiQueue.off('requestCompleted', onCompleted); + aiAssistant.aiQueue.off('requestFailed', onFailed); + reject(new Error(item.error)); + } + }; + + aiAssistant.aiQueue.on('requestCompleted', onCompleted); + aiAssistant.aiQueue.on('requestFailed', onFailed); }); } } else { - aiResponse = await aiAssistant.getResponse(text, 'auto'); + // Используем очередь AIQueue для обработки + const requestId = await aiAssistant.addToQueue({ + message: text, + history: null, + systemPrompt: aiSettings ? aiSettings.system_prompt : '', + rules: null + }, 0); + + // Ждем ответ из очереди + aiResponse = await new Promise((resolve, reject) => { + const timeout = setTimeout(() => { + reject(new Error('AI response timeout')); + }, 120000); // 2 минуты таймаут + + const onCompleted = (item) => { + if (item.id === requestId) { + clearTimeout(timeout); + aiAssistant.aiQueue.off('requestCompleted', onCompleted); + aiAssistant.aiQueue.off('requestFailed', onFailed); + resolve(item.result); + } + }; + + const onFailed = (item) => { + if (item.id === requestId) { + clearTimeout(timeout); + aiAssistant.aiQueue.off('requestCompleted', onCompleted); + aiAssistant.aiQueue.off('requestFailed', onFailed); + reject(new Error(item.error)); + } + }; + + aiAssistant.aiQueue.on('requestCompleted', onCompleted); + aiAssistant.aiQueue.on('requestFailed', onFailed); + }); } if (await isUserBlocked(userId)) { @@ -588,11 +685,36 @@ class EmailBotService { this.imap.once('error', (err) => { logger.error(`[EmailBot] IMAP connection error: ${err.message}`); + logger.error(`[EmailBot] Error details:`, { + code: err.code, + errno: err.errno, + syscall: err.syscall, + hostname: err.hostname, + port: err.port, + stack: err.stack + }); this.cleanupImapConnection(); - if (err.message && err.message.toLowerCase().includes('timed out') && attempt < maxAttempts) { - logger.warn(`[EmailBot] IMAP reconnecting in 10 seconds (attempt ${attempt + 1})...`); - setTimeout(tryConnect, 10000); + // Более детальная логика переподключения + if (attempt < maxAttempts) { + let reconnectDelay = 10000; + let reconnectReason = 'default'; + + if (err.message && err.message.toLowerCase().includes('timed out')) { + reconnectDelay = 15000; // Увеличиваем задержку для таймаутов + reconnectReason = 'timeout'; + } else if (err.code === 'ECONNREFUSED') { + reconnectDelay = 30000; // Дольше ждем для отказа в соединении + reconnectReason = 'connection refused'; + } else if (err.code === 'ENOTFOUND') { + reconnectDelay = 60000; // Еще дольше для проблем с DNS + reconnectReason = 'DNS resolution failed'; + } + + logger.warn(`[EmailBot] IMAP reconnecting in ${reconnectDelay/1000} seconds (attempt ${attempt + 1}/${maxAttempts}, reason: ${reconnectReason})...`); + setTimeout(tryConnect, reconnectDelay); + } else { + logger.error(`[EmailBot] Max reconnection attempts reached (${maxAttempts}). Stopping reconnection.`); } }); @@ -611,6 +733,112 @@ class EmailBotService { const settings = await encryptedDb.getData('email_settings', {}, null, 'id'); return settings; } + + // Сохранение email настроек + async saveEmailSettings(settings) { + try { + // Проверяем, существуют ли уже настройки + const existingSettings = await encryptedDb.getData('email_settings', {}, 1); + + let result; + if (existingSettings.length > 0) { + // Если настройки существуют, обновляем их + const existingId = existingSettings[0].id; + result = await encryptedDb.saveData('email_settings', settings, { id: existingId }); + } else { + // Если настроек нет, создаем новые + result = await encryptedDb.saveData('email_settings', settings, null); + } + + logger.info('Email settings saved successfully'); + return { success: true, data: result }; + } catch (error) { + logger.error('Error saving email settings:', error); + throw error; + } + } + + // Тест IMAP подключения + async testImapConnection() { + return new Promise(async (resolve, reject) => { + try { + logger.info('[EmailBot] Testing IMAP connection...'); + + // Получаем конфигурацию IMAP + const imapConfig = await this.getImapConfig(); + + // Создаем временное IMAP соединение для теста + const testImap = new Imap(imapConfig); + + let connectionTimeout = setTimeout(() => { + testImap.end(); + reject(new Error('IMAP connection timeout after 30 seconds')); + }, 30000); + + testImap.once('ready', () => { + clearTimeout(connectionTimeout); + logger.info('[EmailBot] IMAP connection test successful'); + testImap.end(); + resolve({ + success: true, + message: 'IMAP подключение успешно установлено', + details: { + host: imapConfig.host, + port: imapConfig.port, + user: imapConfig.user + } + }); + }); + + testImap.once('error', (err) => { + clearTimeout(connectionTimeout); + logger.error(`[EmailBot] IMAP connection test failed: ${err.message}`); + testImap.end(); + reject(new Error(`IMAP подключение не удалось: ${err.message}`)); + }); + + testImap.once('end', () => { + clearTimeout(connectionTimeout); + logger.info('[EmailBot] IMAP connection test ended'); + }); + + testImap.connect(); + + } catch (error) { + reject(new Error(`Ошибка при тестировании IMAP: ${error.message}`)); + } + }); + } + + // Тест SMTP подключения + async testSmtpConnection() { + return new Promise(async (resolve, reject) => { + try { + logger.info('[EmailBot] Testing SMTP connection...'); + + // Получаем транспортер SMTP + const transporter = await this.getTransporter(); + + // Тестируем подключение + await transporter.verify(); + + logger.info('[EmailBot] SMTP connection test successful'); + resolve({ + success: true, + message: 'SMTP подключение успешно установлено', + details: { + host: transporter.options.host, + port: transporter.options.port, + secure: transporter.options.secure + } + }); + + } catch (error) { + logger.error(`[EmailBot] SMTP connection test failed: ${error.message}`); + reject(new Error(`SMTP подключение не удалось: ${error.message}`)); + } + }); + } } // console.log('[EmailBot] module.exports = EmailBotService'); diff --git a/backend/services/telegramBot.js b/backend/services/telegramBot.js index 3cbdb61..55f5614 100644 --- a/backend/services/telegramBot.js +++ b/backend/services/telegramBot.js @@ -432,20 +432,77 @@ async function getBot() { if (ragResult && ragResult.answer && typeof ragResult.score === 'number' && Math.abs(ragResult.score) <= 0.1) { aiResponse = ragResult.answer; } else { - aiResponse = await generateLLMResponse({ - userQuestion: content, - context: ragResult && ragResult.context ? ragResult.context : '', - answer: ragResult && ragResult.answer ? ragResult.answer : '', - systemPrompt: aiSettings ? aiSettings.system_prompt : '', + // Используем очередь AIQueue для LLM генерации + const requestId = await aiAssistant.addToQueue({ + message: content, history: history, - model: aiSettings ? aiSettings.model : undefined, - language: aiSettings && aiSettings.languages && aiSettings.languages.length > 0 ? aiSettings.languages[0] : 'ru' + systemPrompt: aiSettings ? aiSettings.system_prompt : '', + rules: null + }, 0); + + // Ждем ответ из очереди + aiResponse = await new Promise((resolve, reject) => { + const timeout = setTimeout(() => { + reject(new Error('AI response timeout')); + }, 120000); // 2 минуты таймаут + + const onCompleted = (item) => { + if (item.id === requestId) { + clearTimeout(timeout); + aiAssistant.aiQueue.off('requestCompleted', onCompleted); + aiAssistant.aiQueue.off('requestFailed', onFailed); + resolve(item.result); + } + }; + + const onFailed = (item) => { + if (item.id === requestId) { + clearTimeout(timeout); + aiAssistant.aiQueue.off('requestCompleted', onCompleted); + aiAssistant.aiQueue.off('requestFailed', onFailed); + reject(new Error(item.error)); + } + }; + + aiAssistant.aiQueue.on('requestCompleted', onCompleted); + aiAssistant.aiQueue.on('requestFailed', onFailed); }); } } else { - // Используем системный промпт из настроек, если RAG не используется - const systemPrompt = aiSettings ? aiSettings.system_prompt : ''; - aiResponse = await aiAssistant.getResponse(content, history, systemPrompt); + // Используем очередь AIQueue для обработки + const requestId = await aiAssistant.addToQueue({ + message: content, + history: history, + systemPrompt: aiSettings ? aiSettings.system_prompt : '', + rules: null + }, 0); + + // Ждем ответ из очереди + aiResponse = await new Promise((resolve, reject) => { + const timeout = setTimeout(() => { + reject(new Error('AI response timeout')); + }, 120000); // 2 минуты таймаут + + const onCompleted = (item) => { + if (item.id === requestId) { + clearTimeout(timeout); + aiAssistant.aiQueue.off('requestCompleted', onCompleted); + aiAssistant.aiQueue.off('requestFailed', onFailed); + resolve(item.result); + } + }; + + const onFailed = (item) => { + if (item.id === requestId) { + clearTimeout(timeout); + aiAssistant.aiQueue.off('requestFailed', onFailed); + reject(new Error(item.error)); + } + }; + + aiAssistant.aiQueue.on('requestCompleted', onCompleted); + aiAssistant.aiQueue.on('requestFailed', onFailed); + }); } return aiResponse; @@ -576,6 +633,36 @@ function clearSettingsCache() { telegramSettingsCache = null; } +// Сохранение настроек Telegram +async function saveTelegramSettings(settings) { + try { + // Очищаем кэш настроек + clearSettingsCache(); + + // Проверяем, существуют ли уже настройки + const existingSettings = await encryptedDb.getData('telegram_settings', {}, 1); + + let result; + if (existingSettings.length > 0) { + // Если настройки существуют, обновляем их + const existingId = existingSettings[0].id; + result = await encryptedDb.saveData('telegram_settings', settings, { id: existingId }); + } else { + // Если настроек нет, создаем новые + result = await encryptedDb.saveData('telegram_settings', settings, null); + } + + // Обновляем кэш + telegramSettingsCache = settings; + + logger.info('Telegram settings saved successfully'); + return { success: true, data: result }; + } catch (error) { + logger.error('Error saving Telegram settings:', error); + throw error; + } +} + async function getAllBots() { const settings = await encryptedDb.getData('telegram_settings', {}, 1, 'id'); return settings; @@ -587,5 +674,6 @@ module.exports = { stopBot, initTelegramAuth, clearSettingsCache, + saveTelegramSettings, getAllBots, }; diff --git a/docker-compose.yml b/docker-compose.yml index 13bd16f..81166e1 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -53,7 +53,7 @@ services: environment: - OLLAMA_HOST=0.0.0.0 - OLLAMA_ORIGINS=* - - OLLAMA_NUM_PARALLEL=1 + - OLLAMA_NUM_PARALLEL=2 - OLLAMA_NUM_GPU=0 - OLLAMA_KEEP_ALIVE=-1 - OLLAMA_MODEL_TIMEOUT=0 @@ -64,8 +64,8 @@ services: timeout: 10s retries: 5 start_period: 120s - # Современные версии ollama не поддерживают флаг --keep-alive; используем переменные окружения - # command: ["serve"] + # Предзагружаем модель при запуске контейнера + entrypoint: ["/bin/sh", "-c", "ollama serve & sleep 10 && ollama run --keepalive 0 qwen2.5:7b & wait"] vector-search: build: context: ./vector-search diff --git a/frontend/nginx.conf b/frontend/nginx.conf index da398ff..d7235b4 100644 --- a/frontend/nginx.conf +++ b/frontend/nginx.conf @@ -20,6 +20,12 @@ http { proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-Host $host; proxy_set_header X-Forwarded-Port $server_port; + + # Передача cookies и сессии + proxy_pass_request_headers on; + proxy_pass_request_body on; + proxy_set_header Cookie $http_cookie; + proxy_set_header Authorization $http_authorization; } # Проксирование к development серверу frontend diff --git a/frontend/src/components/ai-assistant/SystemMonitoring.vue b/frontend/src/components/ai-assistant/SystemMonitoring.vue index b1bcf3a..34d6a3d 100644 --- a/frontend/src/components/ai-assistant/SystemMonitoring.vue +++ b/frontend/src/components/ai-assistant/SystemMonitoring.vue @@ -46,28 +46,68 @@
Для тестирования RAG необходимо создать таблицу и установить её как источник для ИИ-ассистента.
+Перейдите в