ваше сообщение коммита

This commit is contained in:
2025-09-02 15:33:18 +03:00
parent c007c0b296
commit a6360ccd2e
21 changed files with 1269 additions and 316 deletions

View File

@@ -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);
}

View File

@@ -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();
module.exports = AIQueue;

View File

@@ -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 }) {

View File

@@ -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 }) {

View File

@@ -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')) {

View File

@@ -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');

View File

@@ -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,
};