feat: новая функция
This commit is contained in:
@@ -34,19 +34,9 @@ async function checkAdminRole(address) {
|
||||
let foundTokens = false;
|
||||
let errorCount = 0;
|
||||
const balances = {};
|
||||
// Получаем ключ шифрования
|
||||
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();
|
||||
}
|
||||
} catch (keyError) {
|
||||
// console.error('Error reading encryption key:', keyError);
|
||||
}
|
||||
// Получаем ключ шифрования через унифицированную утилиту
|
||||
const encryptionUtils = require('../utils/encryptionUtils');
|
||||
const encryptionKey = encryptionUtils.getEncryptionKey();
|
||||
|
||||
// Получаем токены и RPC из базы с расшифровкой
|
||||
const tokensResult = await db.getQuery()(
|
||||
|
||||
222
backend/services/adminLogicService.js
Normal file
222
backend/services/adminLogicService.js
Normal file
@@ -0,0 +1,222 @@
|
||||
/**
|
||||
* Copyright (c) 2024-2025 Тарабанов Александр Викторович
|
||||
* All rights reserved.
|
||||
*
|
||||
* This software is proprietary and confidential.
|
||||
* Unauthorized copying, modification, or distribution is prohibited.
|
||||
*
|
||||
* For licensing inquiries: info@hb3-accelerator.com
|
||||
* Website: https://hb3-accelerator.com
|
||||
* GitHub: https://github.com/HB3-ACCELERATOR
|
||||
*/
|
||||
|
||||
const logger = require('../utils/logger');
|
||||
|
||||
/**
|
||||
* Сервис логики для админских функций
|
||||
* Определяет права доступа, приоритеты и логику работы с админами
|
||||
*/
|
||||
|
||||
/**
|
||||
* Определить тип отправителя на основе сессии
|
||||
* @param {Object} session - Сессия пользователя
|
||||
* @returns {Object} { senderType, role }
|
||||
*/
|
||||
function determineSenderType(session) {
|
||||
if (!session) {
|
||||
return { senderType: 'user', role: 'user' };
|
||||
}
|
||||
|
||||
if (session.isAdmin === true) {
|
||||
return { senderType: 'admin', role: 'admin' };
|
||||
}
|
||||
|
||||
return { senderType: 'user', role: 'user' };
|
||||
}
|
||||
|
||||
/**
|
||||
* Определить, нужно ли генерировать AI ответ
|
||||
* @param {Object} params - Параметры
|
||||
* @param {string} params.senderType - Тип отправителя (user/admin)
|
||||
* @param {number} params.userId - ID пользователя
|
||||
* @param {number} params.recipientId - ID получателя
|
||||
* @param {string} params.channel - Канал (web/telegram/email)
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function shouldGenerateAiReply(params) {
|
||||
const { senderType, userId, recipientId } = params;
|
||||
|
||||
// Обычные пользователи всегда получают AI ответ
|
||||
if (senderType !== 'admin') {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Админ, пишущий себе, получает AI ответ
|
||||
if (userId === recipientId) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Админ, пишущий другому пользователю, не получает AI ответ
|
||||
// (это личное сообщение от админа)
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Проверить, может ли пользователь писать в беседу
|
||||
* @param {Object} params - Параметры
|
||||
* @param {boolean} params.isAdmin - Является ли админом
|
||||
* @param {number} params.userId - ID пользователя
|
||||
* @param {number} params.conversationUserId - ID владельца беседы
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function canWriteToConversation(params) {
|
||||
const { isAdmin, userId, conversationUserId } = params;
|
||||
|
||||
// Админ может писать в любую беседу
|
||||
if (isAdmin) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Обычный пользователь может писать только в свою беседу
|
||||
return userId === conversationUserId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Получить приоритет запроса для очереди AI
|
||||
* @param {Object} params - Параметры
|
||||
* @param {boolean} params.isAdmin - Является ли админом
|
||||
* @param {string} params.message - Текст сообщения
|
||||
* @param {Array} params.history - История сообщений
|
||||
* @returns {number} Приоритет (чем выше, тем важнее)
|
||||
*/
|
||||
function getRequestPriority(params) {
|
||||
const { isAdmin, message, history = [] } = params;
|
||||
|
||||
let priority = 10; // Базовый приоритет
|
||||
|
||||
// Админ получает повышенный приоритет
|
||||
if (isAdmin) {
|
||||
priority += 5;
|
||||
}
|
||||
|
||||
// Срочные ключевые слова
|
||||
const urgentKeywords = ['срочно', 'urgent', 'помогите', 'help', 'критично', 'critical'];
|
||||
const messageLC = (message || '').toLowerCase();
|
||||
|
||||
if (urgentKeywords.some(keyword => messageLC.includes(keyword))) {
|
||||
priority += 10;
|
||||
}
|
||||
|
||||
// Короткие сообщения обрабатываются быстрее
|
||||
if (message && message.length < 50) {
|
||||
priority += 5;
|
||||
}
|
||||
|
||||
// Первое сообщение в беседе
|
||||
if (!history || history.length === 0) {
|
||||
priority += 3;
|
||||
}
|
||||
|
||||
return priority;
|
||||
}
|
||||
|
||||
/**
|
||||
* Проверить, может ли пользователь выполнить админское действие
|
||||
* @param {Object} params - Параметры
|
||||
* @param {boolean} params.isAdmin - Является ли админом
|
||||
* @param {string} params.action - Название действия
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function canPerformAdminAction(params) {
|
||||
const { isAdmin, action } = params;
|
||||
|
||||
// Только админ может выполнять админские действия
|
||||
if (!isAdmin) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Список разрешенных админских действий
|
||||
const allowedActions = [
|
||||
'delete_message_history',
|
||||
'view_all_conversations',
|
||||
'manage_users',
|
||||
'manage_ai_settings',
|
||||
'broadcast_message',
|
||||
'delete_user',
|
||||
'modify_user_settings'
|
||||
];
|
||||
|
||||
return allowedActions.includes(action);
|
||||
}
|
||||
|
||||
/**
|
||||
* Получить настройки админа
|
||||
* @param {Object} params - Параметры
|
||||
* @param {boolean} params.isAdmin - Является ли админом
|
||||
* @param {string} params.channel - Канал
|
||||
* @returns {Object} Настройки
|
||||
*/
|
||||
function getAdminSettings(params) {
|
||||
const { isAdmin } = params;
|
||||
|
||||
if (!isAdmin) {
|
||||
// Ограниченные права для обычного пользователя
|
||||
return {
|
||||
canWriteToAnyConversation: false,
|
||||
canViewAllConversations: false,
|
||||
canManageUsers: false,
|
||||
canManageAISettings: false,
|
||||
aiReplyPriority: 0
|
||||
};
|
||||
}
|
||||
|
||||
// Полные права для админа
|
||||
return {
|
||||
canWriteToAnyConversation: true,
|
||||
canViewAllConversations: true,
|
||||
canManageUsers: true,
|
||||
canManageAISettings: true,
|
||||
aiReplyPriority: 15
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Логирование админского действия
|
||||
* @param {Object} params - Параметры
|
||||
* @param {number} params.adminId - ID админа
|
||||
* @param {string} params.action - Действие
|
||||
* @param {Object} params.details - Детали
|
||||
*/
|
||||
function logAdminAction(params) {
|
||||
const { adminId, action, details } = params;
|
||||
|
||||
logger.info('[AdminLogic] Админское действие:', {
|
||||
adminId,
|
||||
action,
|
||||
details,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Проверить, является ли сообщение от админа личным
|
||||
* @param {Object} params - Параметры
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function isPersonalAdminMessage(params) {
|
||||
const { senderType, userId, recipientId } = params;
|
||||
|
||||
return senderType === 'admin' && userId !== recipientId;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
determineSenderType,
|
||||
shouldGenerateAiReply,
|
||||
canWriteToConversation,
|
||||
getRequestPriority,
|
||||
canPerformAdminAction,
|
||||
getAdminSettings,
|
||||
logAdminAction,
|
||||
isPersonalAdminMessage
|
||||
};
|
||||
|
||||
@@ -10,439 +10,186 @@
|
||||
* GitHub: https://github.com/HB3-ACCELERATOR
|
||||
*/
|
||||
|
||||
const { ChatOllama } = require('@langchain/ollama');
|
||||
const aiCache = require('./ai-cache');
|
||||
const AIQueue = require('./ai-queue');
|
||||
const logger = require('../utils/logger');
|
||||
const ollamaConfig = require('./ollamaConfig');
|
||||
|
||||
// Константы для AI параметров
|
||||
const AI_CONFIG = {
|
||||
temperature: 0.3,
|
||||
maxTokens: 512,
|
||||
timeout: 120000, // Уменьшаем до 120 секунд, чтобы соответствовать EmailBot
|
||||
numCtx: 2048,
|
||||
numGpu: 1,
|
||||
numThread: 4,
|
||||
repeatPenalty: 1.1,
|
||||
topK: 40,
|
||||
topP: 0.9,
|
||||
// tfsZ не поддерживается в текущем Ollama — удаляем
|
||||
mirostat: 2,
|
||||
mirostatTau: 5,
|
||||
mirostatEta: 0.1,
|
||||
seed: -1,
|
||||
// Ограничим количество генерируемых токенов для CPU, чтобы избежать таймаутов
|
||||
numPredict: 256,
|
||||
stop: []
|
||||
};
|
||||
|
||||
/**
|
||||
* AI Assistant - тонкая обёртка для работы с Ollama и RAG
|
||||
* Основная логика вынесена в отдельные сервисы:
|
||||
* - ragService.js - генерация ответов через RAG
|
||||
* - aiAssistantSettingsService.js - настройки ИИ
|
||||
* - aiAssistantRulesService.js - правила ИИ
|
||||
* - messageDeduplicationService.js - дедупликация сообщений
|
||||
* - ai-queue.js - управление очередью (отдельный сервис)
|
||||
*/
|
||||
class AIAssistant {
|
||||
constructor() {
|
||||
this.baseUrl = process.env.OLLAMA_BASE_URL || 'http://localhost:11434';
|
||||
this.defaultModel = process.env.OLLAMA_MODEL || 'qwen2.5:7b';
|
||||
this.lastHealthCheck = 0;
|
||||
this.healthCheckInterval = 300000; // 5 минут (увеличено с 30 секунд для уменьшения логов)
|
||||
|
||||
// Создаем экземпляр AIQueue
|
||||
this.aiQueue = new AIQueue();
|
||||
this.isProcessingQueue = false;
|
||||
|
||||
// Запускаем обработку очереди
|
||||
this.startQueueProcessing();
|
||||
this.baseUrl = null;
|
||||
this.defaultModel = null;
|
||||
this.isInitialized = false;
|
||||
}
|
||||
|
||||
// Запуск обработки очереди
|
||||
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) {
|
||||
/**
|
||||
* Инициализация из БД
|
||||
*/
|
||||
async initialize() {
|
||||
try {
|
||||
const { message, history, systemPrompt, rules } = request;
|
||||
await ollamaConfig.loadSettingsFromDb();
|
||||
|
||||
// Используем прямой запрос к API, а не getResponse (чтобы избежать цикла)
|
||||
const result = await this.directRequest(
|
||||
[{ role: 'user', content: message }],
|
||||
systemPrompt,
|
||||
{ temperature: 0.3, maxTokens: 150 }
|
||||
);
|
||||
this.baseUrl = ollamaConfig.getBaseUrl();
|
||||
this.defaultModel = ollamaConfig.getDefaultModel();
|
||||
|
||||
return result;
|
||||
if (!this.baseUrl || !this.defaultModel) {
|
||||
throw new Error('Настройки Ollama не найдены в БД');
|
||||
}
|
||||
|
||||
this.isInitialized = true;
|
||||
logger.info(`[AIAssistant] ✅ Инициализирован из БД: model=${this.defaultModel}`);
|
||||
} 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();
|
||||
}
|
||||
|
||||
// Проверка здоровья модели
|
||||
async checkModelHealth() {
|
||||
const now = Date.now();
|
||||
if (now - this.lastHealthCheck < this.healthCheckInterval) {
|
||||
return true; // Используем кэшированный результат
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`${this.baseUrl}/api/tags`);
|
||||
if (!response.ok) {
|
||||
throw new Error(`Ollama API returned ${response.status}`);
|
||||
}
|
||||
const data = await response.json();
|
||||
const modelExists = data.models?.some(model => model.name === this.defaultModel);
|
||||
|
||||
this.lastHealthCheck = now;
|
||||
return modelExists;
|
||||
} catch (error) {
|
||||
logger.error('Model health check failed:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Очистка старого кэша
|
||||
cleanupCache() {
|
||||
const now = Date.now();
|
||||
const maxAge = 3600000; // 1 час
|
||||
aiCache.cleanup(maxAge);
|
||||
}
|
||||
|
||||
// Создание чата с кастомным системным промптом
|
||||
createChat(customSystemPrompt = '') {
|
||||
let systemPrompt = customSystemPrompt;
|
||||
if (!systemPrompt) {
|
||||
systemPrompt = 'Вы - полезный ассистент. Отвечайте на русском языке кратко и по делу.';
|
||||
}
|
||||
|
||||
return new ChatOllama({
|
||||
baseUrl: this.baseUrl,
|
||||
model: this.defaultModel,
|
||||
system: systemPrompt,
|
||||
...AI_CONFIG,
|
||||
options: AI_CONFIG
|
||||
});
|
||||
}
|
||||
|
||||
// Определение приоритета запроса
|
||||
getRequestPriority(message, history, rules) {
|
||||
let priority = 0;
|
||||
|
||||
// Высокий приоритет для коротких запросов
|
||||
if (message.length < 50) {
|
||||
priority += 10;
|
||||
}
|
||||
|
||||
// Приоритет по типу запроса
|
||||
const urgentKeywords = ['срочно', 'важно', 'помоги'];
|
||||
if (urgentKeywords.some(keyword => message.toLowerCase().includes(keyword))) {
|
||||
priority += 20;
|
||||
}
|
||||
|
||||
// Приоритет для администраторов
|
||||
if (rules && rules.isAdmin) {
|
||||
priority += 15;
|
||||
}
|
||||
|
||||
// Приоритет по времени ожидания (если есть история)
|
||||
if (history && history.length > 0) {
|
||||
const lastMessage = history[history.length - 1];
|
||||
const timeDiff = Date.now() - (lastMessage.timestamp || Date.now());
|
||||
if (timeDiff > 30000) { // Более 30 секунд ожидания
|
||||
priority += 5;
|
||||
}
|
||||
}
|
||||
|
||||
return priority;
|
||||
}
|
||||
|
||||
// Основной метод для получения ответа
|
||||
async getResponse(message, history = null, systemPrompt = '', rules = null) {
|
||||
try {
|
||||
// Очищаем старый кэш
|
||||
this.cleanupCache();
|
||||
|
||||
// Проверяем здоровье модели
|
||||
const isHealthy = await this.checkModelHealth();
|
||||
if (!isHealthy) {
|
||||
return 'Извините, модель временно недоступна. Пожалуйста, попробуйте позже.';
|
||||
}
|
||||
|
||||
// Проверяем кэш
|
||||
const cacheKey = aiCache.generateKey([{ role: 'user', content: message }], {
|
||||
temperature: 0.3,
|
||||
maxTokens: 150
|
||||
});
|
||||
const cachedResponse = aiCache.get(cacheKey);
|
||||
if (cachedResponse) {
|
||||
return cachedResponse;
|
||||
}
|
||||
|
||||
// Определяем приоритет запроса
|
||||
const priority = this.getRequestPriority(message, history, rules);
|
||||
|
||||
// Добавляем запрос в очередь
|
||||
const requestId = await this.addToQueue({
|
||||
message,
|
||||
history,
|
||||
systemPrompt,
|
||||
rules
|
||||
}, priority);
|
||||
|
||||
// Ждем результат из очереди
|
||||
return new Promise((resolve, reject) => {
|
||||
const timeout = setTimeout(() => {
|
||||
reject(new Error('Request timeout - очередь перегружена'));
|
||||
}, 180000); // 180 секунд таймаут для очереди
|
||||
|
||||
const onCompleted = (item) => {
|
||||
if (item.id === requestId) {
|
||||
clearTimeout(timeout);
|
||||
this.aiQueue.off('requestCompleted', onCompleted);
|
||||
this.aiQueue.off('requestFailed', onFailed);
|
||||
try {
|
||||
aiCache.set(cacheKey, item.result);
|
||||
} catch {}
|
||||
resolve(item.result);
|
||||
}
|
||||
};
|
||||
|
||||
const onFailed = (item) => {
|
||||
if (item.id === requestId) {
|
||||
clearTimeout(timeout);
|
||||
this.aiQueue.off('requestCompleted', onCompleted);
|
||||
this.aiQueue.off('requestFailed', onFailed);
|
||||
reject(new Error(item.error));
|
||||
}
|
||||
};
|
||||
|
||||
this.aiQueue.on('requestCompleted', onCompleted);
|
||||
this.aiQueue.on('requestFailed', onFailed);
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Error in getResponse:', error);
|
||||
return 'Извините, я не смог обработать ваш запрос. Пожалуйста, попробуйте позже.';
|
||||
}
|
||||
}
|
||||
|
||||
// Алиас для getResponse (для совместимости)
|
||||
async processMessage(message, history = null, systemPrompt = '', rules = null) {
|
||||
return this.getResponse(message, history, systemPrompt, rules);
|
||||
}
|
||||
|
||||
// Прямой запрос к API (для очереди)
|
||||
async directRequest(messages, systemPrompt = '', optionsOverride = {}) {
|
||||
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);
|
||||
|
||||
// Маппинг camelCase → snake_case для опций Ollama
|
||||
const mapOptionsToOllama = (opts) => ({
|
||||
temperature: opts.temperature,
|
||||
// Используем только num_predict; не мапим maxTokens, чтобы не завышать лимит генерации
|
||||
num_predict: typeof opts.numPredict === 'number' && opts.numPredict > 0 ? opts.numPredict : undefined,
|
||||
num_ctx: opts.numCtx,
|
||||
num_gpu: opts.numGpu,
|
||||
num_thread: opts.numThread,
|
||||
repeat_penalty: opts.repeatPenalty,
|
||||
top_k: opts.topK,
|
||||
top_p: opts.topP,
|
||||
tfs_z: opts.tfsZ,
|
||||
mirostat: opts.mirostat,
|
||||
mirostat_tau: opts.mirostatTau,
|
||||
mirostat_eta: opts.mirostatEta,
|
||||
seed: opts.seed,
|
||||
stop: Array.isArray(opts.stop) ? opts.stop : []
|
||||
});
|
||||
|
||||
const mergedConfig = { ...AI_CONFIG, ...optionsOverride };
|
||||
const ollamaOptions = mapOptionsToOllama(mergedConfig);
|
||||
|
||||
// Вставляем системный промпт в начало, если задан
|
||||
const finalMessages = Array.isArray(messages) ? [...messages] : [];
|
||||
// Нормализация: только 'user' | 'assistant' | 'system'
|
||||
for (const m of finalMessages) {
|
||||
if (m && m.role) {
|
||||
if (m.role !== 'assistant' && m.role !== 'system') m.role = 'user';
|
||||
}
|
||||
}
|
||||
if (systemPrompt && !finalMessages.find(m => m.role === 'system')) {
|
||||
finalMessages.unshift({ role: 'system', content: systemPrompt });
|
||||
}
|
||||
|
||||
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' },
|
||||
signal: controller.signal,
|
||||
body: JSON.stringify({
|
||||
model,
|
||||
messages: finalMessages,
|
||||
stream: false,
|
||||
options: ollamaOptions
|
||||
})
|
||||
});
|
||||
logger.info(`[AIAssistant] Ollama API ответил: status=${response.status}`);
|
||||
} finally {
|
||||
clearTimeout(timeoutId);
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
// Ollama /api/chat возвращает ответ в data.message.content
|
||||
if (data.message && typeof data.message.content === 'string') {
|
||||
const content = data.message.content;
|
||||
try {
|
||||
const cacheKey = aiCache.generateKey(messages, { num_predict: ollamaOptions.num_predict, temperature: ollamaOptions.temperature });
|
||||
aiCache.set(cacheKey, content);
|
||||
} catch {}
|
||||
return content;
|
||||
}
|
||||
// OpenAI-совместимый /v1/chat/completions
|
||||
if (data.choices && data.choices[0] && data.choices[0].message && data.choices[0].message.content) {
|
||||
const content = data.choices[0].message.content;
|
||||
try {
|
||||
const cacheKey = aiCache.generateKey(messages, { num_predict: ollamaOptions.num_predict, temperature: ollamaOptions.temperature });
|
||||
aiCache.set(cacheKey, content);
|
||||
} catch {}
|
||||
return content;
|
||||
}
|
||||
|
||||
const content = data.response || '';
|
||||
try {
|
||||
const cacheKey = aiCache.generateKey(messages, { num_predict: ollamaOptions.num_predict, temperature: ollamaOptions.temperature });
|
||||
aiCache.set(cacheKey, content);
|
||||
} catch {}
|
||||
return content;
|
||||
} catch (error) {
|
||||
logger.error('Error in directRequest:', error);
|
||||
if (error.name === 'AbortError') {
|
||||
throw new Error('Request timeout - модель не ответила в течение 120 секунд');
|
||||
}
|
||||
logger.error('[AIAssistant] ❌ КРИТИЧЕСКАЯ ОШИБКА загрузки настроек из БД:', error.message);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Получение списка доступных моделей
|
||||
async getAvailableModels() {
|
||||
try {
|
||||
const response = await fetch(`${this.baseUrl}/api/tags`);
|
||||
const data = await response.json();
|
||||
return data.models || [];
|
||||
} catch (error) {
|
||||
logger.error('Error getting available models:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Генерация ответа для всех каналов (web, telegram, email)
|
||||
* Используется ботами (telegramBot, emailBot)
|
||||
*/
|
||||
async generateResponse(options) {
|
||||
const {
|
||||
channel,
|
||||
messageId,
|
||||
userId,
|
||||
userQuestion,
|
||||
conversationHistory = [],
|
||||
conversationId,
|
||||
ragTableId = null,
|
||||
metadata = {}
|
||||
} = options;
|
||||
|
||||
// Проверка здоровья AI сервиса
|
||||
async checkHealth() {
|
||||
try {
|
||||
const response = await fetch(`${this.baseUrl}/api/tags`);
|
||||
if (!response.ok) {
|
||||
throw new Error(`Ollama API returned ${response.status}`);
|
||||
logger.info(`[AIAssistant] Генерация ответа для канала ${channel}, пользователь ${userId}`);
|
||||
|
||||
const messageDeduplicationService = require('./messageDeduplicationService');
|
||||
const aiAssistantSettingsService = require('./aiAssistantSettingsService');
|
||||
const aiAssistantRulesService = require('./aiAssistantRulesService');
|
||||
const { ragAnswer } = require('./ragService');
|
||||
|
||||
// 1. Проверяем дедупликацию
|
||||
const cleanMessageId = messageDeduplicationService.cleanMessageId(messageId, channel);
|
||||
const isAlreadyProcessed = await messageDeduplicationService.isMessageAlreadyProcessed(
|
||||
channel,
|
||||
cleanMessageId,
|
||||
userId,
|
||||
'user'
|
||||
);
|
||||
|
||||
if (isAlreadyProcessed) {
|
||||
logger.info(`[AIAssistant] Сообщение ${cleanMessageId} уже обработано - пропускаем`);
|
||||
return { success: false, reason: 'duplicate' };
|
||||
}
|
||||
const data = await response.json();
|
||||
|
||||
// 2. Получаем настройки AI ассистента
|
||||
const aiSettings = await aiAssistantSettingsService.getSettings();
|
||||
let rules = null;
|
||||
if (aiSettings && aiSettings.rules_id) {
|
||||
rules = await aiAssistantRulesService.getRuleById(aiSettings.rules_id);
|
||||
}
|
||||
|
||||
// 3. Генерируем AI ответ через RAG
|
||||
const aiResponse = await ragAnswer({
|
||||
userQuestion,
|
||||
conversationHistory,
|
||||
systemPrompt: aiSettings ? aiSettings.system_prompt : '',
|
||||
rules: rules ? rules.rules : null,
|
||||
ragTableId
|
||||
});
|
||||
|
||||
if (!aiResponse) {
|
||||
logger.warn(`[AIAssistant] Пустой ответ от AI для пользователя ${userId}`);
|
||||
return { success: false, reason: 'empty_response' };
|
||||
}
|
||||
|
||||
// 4. Сохраняем ответ с дедупликацией
|
||||
const aiResponseId = `ai_response_${cleanMessageId}_${Date.now()}`;
|
||||
const saveResult = await messageDeduplicationService.saveMessageWithDeduplication(
|
||||
{
|
||||
user_id: userId,
|
||||
conversation_id: conversationId,
|
||||
sender_type: 'assistant',
|
||||
content: aiResponse,
|
||||
channel: channel,
|
||||
role: 'assistant',
|
||||
direction: 'out',
|
||||
created_at: new Date(),
|
||||
...metadata
|
||||
},
|
||||
channel,
|
||||
aiResponseId,
|
||||
userId,
|
||||
'assistant',
|
||||
'messages'
|
||||
);
|
||||
|
||||
if (!saveResult.success) {
|
||||
logger.error(`[AIAssistant] Ошибка сохранения AI ответа:`, saveResult.error);
|
||||
return { success: false, reason: 'save_error' };
|
||||
}
|
||||
|
||||
logger.info(`[AIAssistant] AI ответ успешно сгенерирован и сохранен для пользователя ${userId}`);
|
||||
|
||||
return {
|
||||
status: 'ok',
|
||||
models: data.models?.length || 0,
|
||||
baseUrl: this.baseUrl
|
||||
success: true,
|
||||
response: aiResponse,
|
||||
messageId: aiResponseId,
|
||||
conversationId: conversationId
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
logger.error('AI health check failed:', error);
|
||||
return {
|
||||
status: 'error',
|
||||
error: error.message,
|
||||
baseUrl: this.baseUrl
|
||||
};
|
||||
logger.error(`[AIAssistant] Ошибка генерации ответа:`, error);
|
||||
return { success: false, reason: 'error', error: error.message };
|
||||
}
|
||||
}
|
||||
|
||||
// Добавляем методы из vectorStore.js
|
||||
async initVectorStore() {
|
||||
// ... код инициализации ...
|
||||
/**
|
||||
* Простая генерация ответа (для гостевых сообщений)
|
||||
* Используется в guestMessageService
|
||||
*/
|
||||
async getResponse(message, history = null, systemPrompt = '', rules = null) {
|
||||
try {
|
||||
const { ragAnswer } = require('./ragService');
|
||||
|
||||
const result = await ragAnswer({
|
||||
userQuestion: message,
|
||||
conversationHistory: history || [],
|
||||
systemPrompt: systemPrompt || '',
|
||||
rules: rules || null,
|
||||
ragTableId: null
|
||||
});
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
logger.error('[AIAssistant] Ошибка в getResponse:', error);
|
||||
return 'Извините, я не смог обработать ваш запрос. Пожалуйста, попробуйте позже.';
|
||||
}
|
||||
}
|
||||
|
||||
async findSimilarDocuments(query, k = 3) {
|
||||
// ... код поиска документов ...
|
||||
/**
|
||||
* Проверка здоровья AI сервиса
|
||||
* Использует централизованный метод из ollamaConfig
|
||||
*/
|
||||
async checkHealth() {
|
||||
if (!this.isInitialized) {
|
||||
return { status: 'error', error: 'AI Assistant не инициализирован' };
|
||||
}
|
||||
|
||||
// Используем метод проверки из ollamaConfig
|
||||
return await ollamaConfig.checkHealth();
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = new AIAssistant();
|
||||
const aiAssistantInstance = new AIAssistant();
|
||||
const initPromise = aiAssistantInstance.initialize();
|
||||
|
||||
module.exports = aiAssistantInstance;
|
||||
module.exports.initPromise = initPromise;
|
||||
|
||||
@@ -87,6 +87,7 @@ class AICache {
|
||||
|
||||
calculateHitRate() {
|
||||
// Простая реализация - в реальности нужно отслеживать hits/misses
|
||||
if (this.maxSize === 0) return 0;
|
||||
return this.cache.size / this.maxSize;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,19 +28,9 @@ async function getSettings() {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Получаем ключ шифрования
|
||||
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);
|
||||
}
|
||||
// Получаем ключ шифрования через унифицированную утилиту
|
||||
const encryptionUtils = require('../utils/encryptionUtils');
|
||||
const encryptionKey = encryptionUtils.getEncryptionKey();
|
||||
|
||||
// Обрабатываем selected_rag_tables
|
||||
if (setting.selected_rag_tables) {
|
||||
|
||||
@@ -145,7 +145,8 @@ async function getAllLLMModels() {
|
||||
// Для Ollama проверяем реально установленные модели через HTTP API
|
||||
try {
|
||||
const axios = require('axios');
|
||||
const ollamaUrl = process.env.OLLAMA_BASE_URL || 'http://ollama:11434';
|
||||
const ollamaConfig = require('./ollamaConfig');
|
||||
const ollamaUrl = ollamaConfig.getBaseUrl();
|
||||
|
||||
const response = await axios.get(`${ollamaUrl}/api/tags`, {
|
||||
timeout: 5000
|
||||
@@ -214,7 +215,8 @@ async function getAllEmbeddingModels() {
|
||||
// Для Ollama проверяем реально установленные embedding модели через HTTP API
|
||||
try {
|
||||
const axios = require('axios');
|
||||
const ollamaUrl = process.env.OLLAMA_BASE_URL || 'http://ollama:11434';
|
||||
const ollamaConfig = require('./ollamaConfig');
|
||||
const ollamaUrl = ollamaConfig.getBaseUrl();
|
||||
|
||||
const response = await axios.get(`${ollamaUrl}/api/tags`, {
|
||||
timeout: 5000
|
||||
|
||||
@@ -262,8 +262,8 @@ class AuthService {
|
||||
async processAndCleanupGuestData(userId, guestId, session) {
|
||||
try {
|
||||
// Обрабатываем гостевые сообщения
|
||||
const { processGuestMessages } = require('../routes/chat');
|
||||
await processGuestMessages(userId, guestId);
|
||||
const guestMessageService = require('./guestMessageService');
|
||||
await guestMessageService.processGuestMessages(userId, guestId);
|
||||
|
||||
// Очищаем гостевой ID из сессии
|
||||
delete session.guestId;
|
||||
@@ -432,19 +432,9 @@ class AuthService {
|
||||
|
||||
// Если есть гостевой ID в сессии, сохраняем его для нового пользователя
|
||||
if (session.guestId && isNewUser) {
|
||||
// Получаем ключ шифрования
|
||||
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();
|
||||
}
|
||||
} catch (keyError) {
|
||||
console.error('Error reading encryption key:', keyError);
|
||||
}
|
||||
// Получаем ключ шифрования через унифицированную утилиту
|
||||
const encryptionUtils = require('../utils/encryptionUtils');
|
||||
const encryptionKey = encryptionUtils.getEncryptionKey();
|
||||
|
||||
await db.getQuery()(
|
||||
'INSERT INTO guest_user_mapping (user_id, guest_id_encrypted) VALUES ($1, encrypt_text($2, $3)) ON CONFLICT (guest_id_encrypted) DO UPDATE SET user_id = $1',
|
||||
@@ -749,19 +739,9 @@ class AuthService {
|
||||
logger.info('Starting recheck of admin status for all users with wallets');
|
||||
|
||||
try {
|
||||
// Получаем ключ шифрования
|
||||
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();
|
||||
}
|
||||
} catch (keyError) {
|
||||
console.error('Error reading encryption key:', keyError);
|
||||
}
|
||||
// Получаем ключ шифрования через унифицированную утилиту
|
||||
const encryptionUtils = require('../utils/encryptionUtils');
|
||||
const encryptionKey = encryptionUtils.getEncryptionKey();
|
||||
|
||||
// Получаем всех пользователей с кошельками
|
||||
const usersResult = await db.getQuery()(
|
||||
|
||||
211
backend/services/botManager.js
Normal file
211
backend/services/botManager.js
Normal file
@@ -0,0 +1,211 @@
|
||||
/**
|
||||
* Copyright (c) 2024-2025 Тарабанов Александр Викторович
|
||||
* All rights reserved.
|
||||
*
|
||||
* This software is proprietary and confidential.
|
||||
* Unauthorized copying, modification, or distribution is prohibited.
|
||||
*
|
||||
* For licensing inquiries: info@hb3-accelerator.com
|
||||
* Website: https://hb3-accelerator.com
|
||||
* GitHub: https://github.com/HB3-ACCELERATOR
|
||||
*/
|
||||
|
||||
const logger = require('../utils/logger');
|
||||
const TelegramBot = require('./telegramBot');
|
||||
const EmailBot = require('./emailBot');
|
||||
const unifiedMessageProcessor = require('./unifiedMessageProcessor');
|
||||
|
||||
/**
|
||||
* BotManager - централизованный менеджер всех ботов
|
||||
* Управляет жизненным циклом ботов (инициализация, обработка сообщений, остановка)
|
||||
*/
|
||||
class BotManager {
|
||||
constructor() {
|
||||
this.bots = new Map();
|
||||
this.isInitialized = false;
|
||||
this.processingQueue = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Инициализация всех ботов
|
||||
*/
|
||||
async initialize() {
|
||||
try {
|
||||
logger.info('[BotManager] 🚀 Инициализация BotManager...');
|
||||
|
||||
// Создаем экземпляры ботов
|
||||
const webBot = {
|
||||
name: 'WebBot',
|
||||
channel: 'web',
|
||||
isInitialized: true,
|
||||
status: 'active',
|
||||
initialize: async () => ({ success: true }),
|
||||
processMessage: async (messageData) => {
|
||||
return await unifiedMessageProcessor.processMessage(messageData);
|
||||
}
|
||||
};
|
||||
|
||||
const telegramBot = new TelegramBot();
|
||||
const emailBot = new EmailBot();
|
||||
|
||||
// Регистрируем ботов
|
||||
this.bots.set('web', webBot);
|
||||
this.bots.set('telegram', telegramBot);
|
||||
this.bots.set('email', emailBot);
|
||||
|
||||
// Инициализируем Telegram Bot
|
||||
logger.info('[BotManager] Инициализация Telegram Bot...');
|
||||
await telegramBot.initialize().catch(error => {
|
||||
logger.warn('[BotManager] Telegram Bot не инициализирован:', error.message);
|
||||
});
|
||||
|
||||
// Инициализируем Email Bot
|
||||
logger.info('[BotManager] Инициализация Email Bot...');
|
||||
await emailBot.initialize().catch(error => {
|
||||
logger.warn('[BotManager] Email Bot не инициализирован:', error.message);
|
||||
});
|
||||
|
||||
this.isInitialized = true;
|
||||
logger.info('[BotManager] ✅ BotManager успешно инициализирован');
|
||||
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
logger.error('[BotManager] ❌ Ошибка инициализации BotManager:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Получить бота по имени
|
||||
* @param {string} botName - Имя бота (web, telegram, email)
|
||||
* @returns {Object|null} Экземпляр бота или null
|
||||
*/
|
||||
getBot(botName) {
|
||||
return this.bots.get(botName) || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Проверить готовность BotManager
|
||||
* @returns {boolean}
|
||||
*/
|
||||
isReady() {
|
||||
return this.isInitialized;
|
||||
}
|
||||
|
||||
/**
|
||||
* Получить статус всех ботов
|
||||
* @returns {Object}
|
||||
*/
|
||||
getStatus() {
|
||||
const status = {};
|
||||
|
||||
for (const [name, bot] of this.bots) {
|
||||
status[name] = {
|
||||
initialized: bot.isInitialized || false,
|
||||
status: bot.status || 'unknown'
|
||||
};
|
||||
}
|
||||
|
||||
return status;
|
||||
}
|
||||
|
||||
/**
|
||||
* Обработать сообщение через соответствующий бот
|
||||
* @param {Object} messageData - Данные сообщения
|
||||
* @returns {Promise<Object>}
|
||||
*/
|
||||
async processMessage(messageData) {
|
||||
try {
|
||||
const channel = messageData.channel || 'web';
|
||||
const bot = this.bots.get(channel);
|
||||
|
||||
if (!bot) {
|
||||
throw new Error(`Bot for channel "${channel}" not found`);
|
||||
}
|
||||
|
||||
if (!bot.isInitialized) {
|
||||
throw new Error(`Bot "${channel}" is not initialized`);
|
||||
}
|
||||
|
||||
// Обрабатываем сообщение через unified processor
|
||||
return await unifiedMessageProcessor.processMessage(messageData);
|
||||
|
||||
} catch (error) {
|
||||
logger.error('[BotManager] Ошибка обработки сообщения:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Перезапустить конкретный бот
|
||||
* @param {string} botName - Имя бота
|
||||
* @returns {Promise<Object>}
|
||||
*/
|
||||
async restartBot(botName) {
|
||||
try {
|
||||
logger.info(`[BotManager] Перезапуск бота: ${botName}`);
|
||||
|
||||
const bot = this.bots.get(botName);
|
||||
|
||||
if (!bot) {
|
||||
throw new Error(`Bot "${botName}" not found`);
|
||||
}
|
||||
|
||||
// Останавливаем бота (если есть метод stop)
|
||||
if (typeof bot.stop === 'function') {
|
||||
await bot.stop();
|
||||
}
|
||||
|
||||
// Переинициализируем
|
||||
if (typeof bot.initialize === 'function') {
|
||||
await bot.initialize();
|
||||
}
|
||||
|
||||
logger.info(`[BotManager] ✅ Бот ${botName} перезапущен`);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
bot: botName,
|
||||
status: bot.status
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
logger.error(`[BotManager] Ошибка перезапуска бота ${botName}:`, error);
|
||||
return {
|
||||
success: false,
|
||||
error: error.message
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Остановить все боты
|
||||
*/
|
||||
async stop() {
|
||||
try {
|
||||
logger.info('[BotManager] Остановка всех ботов...');
|
||||
|
||||
for (const [name, bot] of this.bots) {
|
||||
if (typeof bot.stop === 'function') {
|
||||
logger.info(`[BotManager] Остановка ${name}...`);
|
||||
await bot.stop().catch(error => {
|
||||
logger.error(`[BotManager] Ошибка остановки ${name}:`, error);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
this.isInitialized = false;
|
||||
logger.info('[BotManager] ✅ Все боты остановлены');
|
||||
|
||||
} catch (error) {
|
||||
logger.error('[BotManager] Ошибка остановки ботов:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Singleton instance
|
||||
const botManager = new BotManager();
|
||||
|
||||
module.exports = botManager;
|
||||
|
||||
114
backend/services/botsSettings.js
Normal file
114
backend/services/botsSettings.js
Normal file
@@ -0,0 +1,114 @@
|
||||
/**
|
||||
* Copyright (c) 2024-2025 Тарабанов Александр Викторович
|
||||
* All rights reserved.
|
||||
*
|
||||
* This software is proprietary and confidential.
|
||||
* Unauthorized copying, modification, or distribution is prohibited.
|
||||
*
|
||||
* For licensing inquiries: info@hb3-accelerator.com
|
||||
* Website: https://hb3-accelerator.com
|
||||
* GitHub: https://github.com/HB3-ACCELERATOR
|
||||
*/
|
||||
|
||||
const db = require('../db');
|
||||
const logger = require('../utils/logger');
|
||||
|
||||
/**
|
||||
* Сервис для работы с настройками ботов
|
||||
*/
|
||||
|
||||
/**
|
||||
* Получить настройки конкретного бота
|
||||
* @param {string} botType - Тип бота (telegram, email)
|
||||
* @returns {Promise<Object|null>}
|
||||
*/
|
||||
async function getBotSettings(botType) {
|
||||
try {
|
||||
let tableName;
|
||||
|
||||
switch (botType) {
|
||||
case 'telegram':
|
||||
tableName = 'telegram_settings';
|
||||
break;
|
||||
case 'email':
|
||||
tableName = 'email_settings';
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unknown bot type: ${botType}`);
|
||||
}
|
||||
|
||||
const { rows } = await db.getQuery()(
|
||||
`SELECT * FROM ${tableName} ORDER BY id LIMIT 1`
|
||||
);
|
||||
|
||||
return rows.length > 0 ? rows[0] : null;
|
||||
|
||||
} catch (error) {
|
||||
logger.error(`[BotsSettings] Ошибка получения настроек ${botType}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Сохранить настройки бота
|
||||
* @param {string} botType - Тип бота
|
||||
* @param {Object} settings - Настройки
|
||||
* @returns {Promise<Object>}
|
||||
*/
|
||||
async function saveBotSettings(botType, settings) {
|
||||
try {
|
||||
let tableName;
|
||||
|
||||
switch (botType) {
|
||||
case 'telegram':
|
||||
tableName = 'telegram_settings';
|
||||
break;
|
||||
case 'email':
|
||||
tableName = 'email_settings';
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unknown bot type: ${botType}`);
|
||||
}
|
||||
|
||||
// Простое сохранение - детали зависят от структуры таблицы
|
||||
const { rows } = await db.getQuery()(
|
||||
`INSERT INTO ${tableName} (settings, updated_at)
|
||||
VALUES ($1, NOW())
|
||||
ON CONFLICT (id) DO UPDATE SET settings = $1, updated_at = NOW()
|
||||
RETURNING *`,
|
||||
[JSON.stringify(settings)]
|
||||
);
|
||||
|
||||
return rows[0];
|
||||
|
||||
} catch (error) {
|
||||
logger.error(`[BotsSettings] Ошибка сохранения настроек ${botType}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Получить настройки всех ботов
|
||||
* @returns {Promise<Object>}
|
||||
*/
|
||||
async function getAllBotsSettings() {
|
||||
try {
|
||||
const settings = {
|
||||
telegram: await getBotSettings('telegram').catch(() => null),
|
||||
email: await getBotSettings('email').catch(() => null)
|
||||
};
|
||||
|
||||
return settings;
|
||||
|
||||
} catch (error) {
|
||||
logger.error('[BotsSettings] Ошибка получения всех настроек:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getBotSettings,
|
||||
saveBotSettings,
|
||||
getAllBotsSettings
|
||||
};
|
||||
|
||||
189
backend/services/conversationService.js
Normal file
189
backend/services/conversationService.js
Normal file
@@ -0,0 +1,189 @@
|
||||
/**
|
||||
* Copyright (c) 2024-2025 Тарабанов Александр Викторович
|
||||
* All rights reserved.
|
||||
*
|
||||
* This software is proprietary and confidential.
|
||||
* Unauthorized copying, modification, or distribution is prohibited.
|
||||
*
|
||||
* For licensing inquiries: info@hb3-accelerator.com
|
||||
* Website: https://hb3-accelerator.com
|
||||
* GitHub: https://github.com/HB3-ACCELERATOR
|
||||
*/
|
||||
|
||||
const db = require('../db');
|
||||
const logger = require('../utils/logger');
|
||||
const encryptionUtils = require('../utils/encryptionUtils');
|
||||
|
||||
/**
|
||||
* Сервис для работы с беседами (conversations)
|
||||
*/
|
||||
|
||||
/**
|
||||
* Получить или создать беседу для пользователя
|
||||
* @param {number} userId - ID пользователя
|
||||
* @param {string} title - Заголовок беседы
|
||||
* @returns {Promise<Object>}
|
||||
*/
|
||||
async function getOrCreateConversation(userId, title = 'Новая беседа') {
|
||||
try {
|
||||
const encryptionKey = encryptionUtils.getEncryptionKey();
|
||||
|
||||
// Ищем существующую активную беседу
|
||||
const { rows: existing } = await db.getQuery()(
|
||||
`SELECT id, user_id, decrypt_text(title_encrypted, $2) as title, created_at, updated_at
|
||||
FROM conversations
|
||||
WHERE user_id = $1
|
||||
ORDER BY updated_at DESC
|
||||
LIMIT 1`,
|
||||
[userId, encryptionKey]
|
||||
);
|
||||
|
||||
if (existing.length > 0) {
|
||||
return existing[0];
|
||||
}
|
||||
|
||||
// Создаем новую беседу
|
||||
const { rows: newConv } = await db.getQuery()(
|
||||
`INSERT INTO conversations (user_id, title_encrypted)
|
||||
VALUES ($1, encrypt_text($2, $3))
|
||||
RETURNING id, user_id, decrypt_text(title_encrypted, $3) as title, created_at, updated_at`,
|
||||
[userId, title, encryptionKey]
|
||||
);
|
||||
|
||||
logger.info('[ConversationService] Создана новая беседа:', newConv[0].id);
|
||||
return newConv[0];
|
||||
|
||||
} catch (error) {
|
||||
logger.error('[ConversationService] Ошибка получения/создания беседы:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Получить беседу по ID
|
||||
* @param {number} conversationId - ID беседы
|
||||
* @returns {Promise<Object|null>}
|
||||
*/
|
||||
async function getConversationById(conversationId) {
|
||||
try {
|
||||
const encryptionKey = encryptionUtils.getEncryptionKey();
|
||||
|
||||
const { rows } = await db.getQuery()(
|
||||
`SELECT id, user_id, decrypt_text(title_encrypted, $2) as title, created_at, updated_at
|
||||
FROM conversations
|
||||
WHERE id = $1`,
|
||||
[conversationId, encryptionKey]
|
||||
);
|
||||
|
||||
return rows.length > 0 ? rows[0] : null;
|
||||
|
||||
} catch (error) {
|
||||
logger.error('[ConversationService] Ошибка получения беседы:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Получить все беседы пользователя
|
||||
* @param {number} userId - ID пользователя
|
||||
* @returns {Promise<Array>}
|
||||
*/
|
||||
async function getUserConversations(userId) {
|
||||
try {
|
||||
const encryptionKey = encryptionUtils.getEncryptionKey();
|
||||
|
||||
const { rows } = await db.getQuery()(
|
||||
`SELECT id, user_id, decrypt_text(title_encrypted, $2) as title, created_at, updated_at
|
||||
FROM conversations
|
||||
WHERE user_id = $1
|
||||
ORDER BY updated_at DESC`,
|
||||
[userId, encryptionKey]
|
||||
);
|
||||
|
||||
return rows;
|
||||
|
||||
} catch (error) {
|
||||
logger.error('[ConversationService] Ошибка получения бесед пользователя:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Обновить время последнего обновления беседы
|
||||
* @param {number} conversationId - ID беседы
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async function touchConversation(conversationId) {
|
||||
try {
|
||||
await db.getQuery()(
|
||||
`UPDATE conversations SET updated_at = NOW() WHERE id = $1`,
|
||||
[conversationId]
|
||||
);
|
||||
} catch (error) {
|
||||
logger.error('[ConversationService] Ошибка обновления беседы:', error);
|
||||
// Не бросаем ошибку, это некритично
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Удалить беседу
|
||||
* @param {number} conversationId - ID беседы
|
||||
* @param {number} userId - ID пользователя (для проверки прав)
|
||||
* @returns {Promise<boolean>}
|
||||
*/
|
||||
async function deleteConversation(conversationId, userId) {
|
||||
try {
|
||||
const { rowCount } = await db.getQuery()(
|
||||
`DELETE FROM conversations WHERE id = $1 AND user_id = $2`,
|
||||
[conversationId, userId]
|
||||
);
|
||||
|
||||
if (rowCount > 0) {
|
||||
logger.info('[ConversationService] Удалена беседа:', conversationId);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
|
||||
} catch (error) {
|
||||
logger.error('[ConversationService] Ошибка удаления беседы:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Обновить заголовок беседы
|
||||
* @param {number} conversationId - ID беседы
|
||||
* @param {number} userId - ID пользователя
|
||||
* @param {string} newTitle - Новый заголовок
|
||||
* @returns {Promise<Object|null>}
|
||||
*/
|
||||
async function updateConversationTitle(conversationId, userId, newTitle) {
|
||||
try {
|
||||
const encryptionKey = encryptionUtils.getEncryptionKey();
|
||||
|
||||
const { rows } = await db.getQuery()(
|
||||
`UPDATE conversations
|
||||
SET title_encrypted = encrypt_text($3, $4), updated_at = NOW()
|
||||
WHERE id = $1 AND user_id = $2
|
||||
RETURNING id, user_id, decrypt_text(title_encrypted, $4) as title, created_at, updated_at`,
|
||||
[conversationId, userId, newTitle, encryptionKey]
|
||||
);
|
||||
|
||||
return rows.length > 0 ? rows[0] : null;
|
||||
|
||||
} catch (error) {
|
||||
logger.error('[ConversationService] Ошибка обновления заголовка беседы:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getOrCreateConversation,
|
||||
getConversationById,
|
||||
getUserConversations,
|
||||
touchConversation,
|
||||
deleteConversation,
|
||||
updateConversationTitle
|
||||
};
|
||||
|
||||
@@ -13,15 +13,78 @@
|
||||
const { pool } = require('../db');
|
||||
const verificationService = require('./verification-service');
|
||||
const logger = require('../utils/logger');
|
||||
const EmailBotService = require('./emailBot.js');
|
||||
const encryptedDb = require('./encryptedDatabaseService');
|
||||
const authService = require('./auth-service');
|
||||
const { checkAdminRole } = require('./admin-role');
|
||||
const { broadcastContactsUpdate } = require('../wsHub');
|
||||
const nodemailer = require('nodemailer');
|
||||
const db = require('../db');
|
||||
|
||||
class EmailAuth {
|
||||
constructor() {
|
||||
this.emailBot = new EmailBotService();
|
||||
// Убрали зависимость от старого EmailBot
|
||||
}
|
||||
|
||||
/**
|
||||
* Отправка кода верификации на email
|
||||
* Создает временный transporter для отправки
|
||||
*/
|
||||
async sendVerificationCode(email, code) {
|
||||
try {
|
||||
// Получаем настройки email из БД
|
||||
const encryptionUtils = require('../utils/encryptionUtils');
|
||||
const encryptionKey = encryptionUtils.getEncryptionKey();
|
||||
|
||||
const { rows } = await db.getQuery()(
|
||||
'SELECT 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(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');
|
||||
}
|
||||
|
||||
const settings = rows[0];
|
||||
|
||||
// Создаем временный transporter
|
||||
const transporter = nodemailer.createTransport({
|
||||
host: settings.smtp_host,
|
||||
port: 465,
|
||||
secure: true,
|
||||
auth: {
|
||||
user: settings.smtp_user,
|
||||
pass: settings.smtp_password,
|
||||
},
|
||||
tls: { rejectUnauthorized: false }
|
||||
});
|
||||
|
||||
// Отправляем письмо
|
||||
await transporter.sendMail({
|
||||
from: settings.from_email,
|
||||
to: email,
|
||||
subject: 'Код подтверждения',
|
||||
text: `Ваш код подтверждения: ${code}\n\nКод действителен в течение 15 минут.`,
|
||||
html: `<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
|
||||
<h2 style="color: #333;">Код подтверждения</h2>
|
||||
<p style="font-size: 16px; color: #666;">Ваш код подтверждения:</p>
|
||||
<div style="background-color: #f5f5f5; padding: 15px; border-radius: 5px; text-align: center; margin: 20px 0;">
|
||||
<span style="font-size: 24px; font-weight: bold; color: #333;">${code}</span>
|
||||
</div>
|
||||
<p style="font-size: 14px; color: #999;">Код действителен в течение 15 минут.</p>
|
||||
</div>`
|
||||
});
|
||||
|
||||
transporter.close();
|
||||
logger.info('[EmailAuth] Verification code sent successfully');
|
||||
|
||||
} catch (error) {
|
||||
logger.error('[EmailAuth] Error sending verification code:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async initEmailAuth(session, email) {
|
||||
@@ -70,7 +133,7 @@ class EmailAuth {
|
||||
);
|
||||
|
||||
// Отправляем код на email
|
||||
await this.emailBot.sendVerificationCode(email, verificationCode);
|
||||
await this.sendVerificationCode(email, verificationCode);
|
||||
|
||||
logger.info(
|
||||
`Generated verification code for Email auth for ${email} and sent to user's email`
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -11,46 +11,21 @@
|
||||
*/
|
||||
|
||||
const db = require('../db');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const encryptionUtils = require('../utils/encryptionUtils');
|
||||
|
||||
class EncryptedDataService {
|
||||
constructor() {
|
||||
this.encryptionKey = this.loadEncryptionKey();
|
||||
this.isEncryptionEnabled = !!this.encryptionKey;
|
||||
this.encryptionKey = encryptionUtils.getEncryptionKey();
|
||||
this.isEncryptionEnabled = encryptionUtils.isEnabled();
|
||||
|
||||
if (this.isEncryptionEnabled) {
|
||||
// console.log('🔐 Шифрование базы данных активировано');
|
||||
// console.log('📋 Автоматическое определение зашифрованных колонок');
|
||||
console.log('🔐 [EncryptedDB] Шифрование базы данных активировано');
|
||||
console.log('📋 [EncryptedDB] Автоматическое определение зашифрованных колонок');
|
||||
} else {
|
||||
// console.log('⚠️ Шифрование базы данных отключено - ключ не найден');
|
||||
console.log('⚠️ [EncryptedDB] Шифрование базы данных отключено - ключ не найден');
|
||||
}
|
||||
}
|
||||
|
||||
loadEncryptionKey() {
|
||||
try {
|
||||
const keyPath = path.join(__dirname, '../../ssl/keys/full_db_encryption.key');
|
||||
// console.log(`[EncryptedDB] Trying key path: ${keyPath}`);
|
||||
if (fs.existsSync(keyPath)) {
|
||||
const key = fs.readFileSync(keyPath, 'utf8').trim();
|
||||
// console.log(`[EncryptedDB] Key loaded from: ${keyPath}, length: ${key.length}`);
|
||||
return key;
|
||||
}
|
||||
// Попробуем альтернативный путь относительно корня приложения
|
||||
const altKeyPath = '/app/ssl/keys/full_db_encryption.key';
|
||||
// console.log(`[EncryptedDB] Trying alternative key path: ${altKeyPath}`);
|
||||
if (fs.existsSync(altKeyPath)) {
|
||||
const key = fs.readFileSync(altKeyPath, 'utf8').trim();
|
||||
// console.log(`[EncryptedDB] Key loaded from: ${altKeyPath}, length: ${key.length}`);
|
||||
return key;
|
||||
}
|
||||
// console.log(`[EncryptedDB] No key file found, using default key`);
|
||||
return 'default-key';
|
||||
} catch (error) {
|
||||
// console.error('❌ Ошибка загрузки ключа шифрования:', error);
|
||||
return 'default-key';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Получить данные из таблицы с автоматической расшифровкой
|
||||
|
||||
159
backend/services/guestMessageService.js
Normal file
159
backend/services/guestMessageService.js
Normal file
@@ -0,0 +1,159 @@
|
||||
/**
|
||||
* Copyright (c) 2024-2025 Тарабанов Александр Викторович
|
||||
* All rights reserved.
|
||||
*
|
||||
* This software is proprietary and confidential.
|
||||
* Unauthorized copying, modification, or distribution is prohibited.
|
||||
*
|
||||
* For licensing inquiries: info@hb3-accelerator.com
|
||||
* Website: https://hb3-accelerator.com
|
||||
* GitHub: https://github.com/HB3-ACCELERATOR
|
||||
*/
|
||||
|
||||
const db = require('../db');
|
||||
const logger = require('../utils/logger');
|
||||
const encryptionUtils = require('../utils/encryptionUtils');
|
||||
const guestService = require('./guestService');
|
||||
|
||||
/**
|
||||
* Сервис для переноса гостевых сообщений в зарегистрированный аккаунт
|
||||
* Используется при регистрации/входе пользователя, который был гостем
|
||||
*/
|
||||
|
||||
/**
|
||||
* Перенести гостевые сообщения в аккаунт пользователя
|
||||
* @param {string} guestId - ID гостя
|
||||
* @param {number} userId - ID зарегистрированного пользователя
|
||||
* @returns {Promise<Object>}
|
||||
*/
|
||||
async function migrateGuestMessages(guestId, userId) {
|
||||
try {
|
||||
logger.info(`[GuestMessageService] Перенос сообщений с ${guestId} на user ${userId}`);
|
||||
|
||||
// Получаем гостевые сообщения
|
||||
const guestMessages = await guestService.getGuestMessages(guestId);
|
||||
|
||||
if (guestMessages.length === 0) {
|
||||
logger.info('[GuestMessageService] Нет сообщений для переноса');
|
||||
return { migrated: 0, skipped: 0 };
|
||||
}
|
||||
|
||||
const encryptionKey = encryptionUtils.getEncryptionKey();
|
||||
let migrated = 0;
|
||||
let skipped = 0;
|
||||
|
||||
// Переносим каждое сообщение
|
||||
for (const msg of guestMessages) {
|
||||
try {
|
||||
// Вставляем в таблицу messages
|
||||
await db.getQuery()(
|
||||
`INSERT INTO messages (
|
||||
user_id,
|
||||
sender_type_encrypted,
|
||||
content_encrypted,
|
||||
channel_encrypted,
|
||||
role_encrypted,
|
||||
direction_encrypted,
|
||||
created_at
|
||||
) VALUES (
|
||||
$1,
|
||||
encrypt_text($2, $7),
|
||||
encrypt_text($3, $7),
|
||||
encrypt_text($4, $7),
|
||||
encrypt_text($5, $7),
|
||||
encrypt_text($6, $7),
|
||||
$8
|
||||
)`,
|
||||
[
|
||||
userId,
|
||||
'user',
|
||||
msg.content,
|
||||
msg.channel || 'web',
|
||||
'user',
|
||||
'incoming',
|
||||
encryptionKey,
|
||||
msg.created_at
|
||||
]
|
||||
);
|
||||
|
||||
migrated++;
|
||||
|
||||
} catch (error) {
|
||||
logger.error('[GuestMessageService] Ошибка переноса сообщения:', error);
|
||||
skipped++;
|
||||
}
|
||||
}
|
||||
|
||||
// Удаляем гостевые сообщения после успешного переноса
|
||||
if (migrated > 0) {
|
||||
await guestService.deleteGuestMessages(guestId);
|
||||
}
|
||||
|
||||
logger.info(`[GuestMessageService] Перенесено: ${migrated}, пропущено: ${skipped}`);
|
||||
|
||||
return { migrated, skipped, total: guestMessages.length };
|
||||
|
||||
} catch (error) {
|
||||
logger.error('[GuestMessageService] Ошибка миграции сообщений:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Проверить, есть ли гостевые сообщения для переноса
|
||||
* @param {string} guestId - ID гостя
|
||||
* @returns {Promise<boolean>}
|
||||
*/
|
||||
async function hasGuestMessages(guestId) {
|
||||
try {
|
||||
const messages = await guestService.getGuestMessages(guestId);
|
||||
return messages.length > 0;
|
||||
} catch (error) {
|
||||
logger.error('[GuestMessageService] Ошибка проверки гостевых сообщений:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Получить количество гостевых сообщений
|
||||
* @param {string} guestId - ID гостя
|
||||
* @returns {Promise<number>}
|
||||
*/
|
||||
async function getGuestMessageCount(guestId) {
|
||||
try {
|
||||
const messages = await guestService.getGuestMessages(guestId);
|
||||
return messages.length;
|
||||
} catch (error) {
|
||||
logger.error('[GuestMessageService] Ошибка подсчета гостевых сообщений:', error);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Очистить старые гостевые сообщения (старше N дней)
|
||||
* @param {number} daysOld - Возраст в днях
|
||||
* @returns {Promise<number>}
|
||||
*/
|
||||
async function cleanupOldGuestMessages(daysOld = 30) {
|
||||
try {
|
||||
const { rowCount } = await db.getQuery()(
|
||||
`DELETE FROM guest_messages
|
||||
WHERE created_at < NOW() - INTERVAL '${daysOld} days'`
|
||||
);
|
||||
|
||||
logger.info(`[GuestMessageService] Очищено ${rowCount} старых гостевых сообщений`);
|
||||
return rowCount;
|
||||
|
||||
} catch (error) {
|
||||
logger.error('[GuestMessageService] Ошибка очистки старых сообщений:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
migrateGuestMessages,
|
||||
hasGuestMessages,
|
||||
getGuestMessageCount,
|
||||
cleanupOldGuestMessages
|
||||
};
|
||||
|
||||
160
backend/services/guestService.js
Normal file
160
backend/services/guestService.js
Normal file
@@ -0,0 +1,160 @@
|
||||
/**
|
||||
* Copyright (c) 2024-2025 Тарабанов Александр Викторович
|
||||
* All rights reserved.
|
||||
*
|
||||
* This software is proprietary and confidential.
|
||||
* Unauthorized copying, modification, or distribution is prohibited.
|
||||
*
|
||||
* For licensing inquiries: info@hb3-accelerator.com
|
||||
* Website: https://hb3-accelerator.com
|
||||
* GitHub: https://github.com/HB3-ACCELERATOR
|
||||
*/
|
||||
|
||||
const db = require('../db');
|
||||
const logger = require('../utils/logger');
|
||||
const encryptionUtils = require('../utils/encryptionUtils');
|
||||
const crypto = require('crypto');
|
||||
|
||||
/**
|
||||
* Сервис для работы с гостевыми сообщениями
|
||||
* Обрабатывает сообщения от незарегистрированных пользователей
|
||||
*/
|
||||
|
||||
/**
|
||||
* Создать гостевой идентификатор
|
||||
* @returns {string}
|
||||
*/
|
||||
function createGuestId() {
|
||||
return `guest_${crypto.randomBytes(16).toString('hex')}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Сохранить гостевое сообщение
|
||||
* @param {Object} messageData - Данные сообщения
|
||||
* @returns {Promise<Object>}
|
||||
*/
|
||||
async function saveGuestMessage(messageData) {
|
||||
try {
|
||||
const encryptionKey = encryptionUtils.getEncryptionKey();
|
||||
const guestId = messageData.guestId || createGuestId();
|
||||
|
||||
const { rows } = await db.getQuery()(
|
||||
`INSERT INTO guest_messages (
|
||||
guest_id,
|
||||
content_encrypted,
|
||||
channel_encrypted,
|
||||
created_at
|
||||
) VALUES (
|
||||
$1,
|
||||
encrypt_text($2, $3),
|
||||
encrypt_text($4, $3),
|
||||
NOW()
|
||||
) RETURNING id, guest_id, created_at`,
|
||||
[guestId, messageData.content, encryptionKey, messageData.channel || 'web']
|
||||
);
|
||||
|
||||
logger.info('[GuestService] Сохранено гостевое сообщение:', rows[0].id);
|
||||
|
||||
return {
|
||||
...rows[0],
|
||||
content: messageData.content,
|
||||
channel: messageData.channel || 'web'
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
logger.error('[GuestService] Ошибка сохранения гостевого сообщения:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Получить гостевые сообщения по guest_id
|
||||
* @param {string} guestId - ID гостя
|
||||
* @returns {Promise<Array>}
|
||||
*/
|
||||
async function getGuestMessages(guestId) {
|
||||
try {
|
||||
const encryptionKey = encryptionUtils.getEncryptionKey();
|
||||
|
||||
const { rows } = await db.getQuery()(
|
||||
`SELECT
|
||||
id,
|
||||
guest_id,
|
||||
decrypt_text(content_encrypted, $2) as content,
|
||||
decrypt_text(channel_encrypted, $2) as channel,
|
||||
created_at
|
||||
FROM guest_messages
|
||||
WHERE guest_id = $1
|
||||
ORDER BY created_at ASC`,
|
||||
[guestId, encryptionKey]
|
||||
);
|
||||
|
||||
return rows;
|
||||
|
||||
} catch (error) {
|
||||
logger.error('[GuestService] Ошибка получения гостевых сообщений:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Удалить гостевые сообщения
|
||||
* @param {string} guestId - ID гостя
|
||||
* @returns {Promise<number>}
|
||||
*/
|
||||
async function deleteGuestMessages(guestId) {
|
||||
try {
|
||||
const { rowCount } = await db.getQuery()(
|
||||
`DELETE FROM guest_messages WHERE guest_id = $1`,
|
||||
[guestId]
|
||||
);
|
||||
|
||||
logger.info(`[GuestService] Удалено ${rowCount} гостевых сообщений для ${guestId}`);
|
||||
return rowCount;
|
||||
|
||||
} catch (error) {
|
||||
logger.error('[GuestService] Ошибка удаления гостевых сообщений:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Проверить, является ли пользователь гостем
|
||||
* @param {string} identifier - Идентификатор
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function isGuest(identifier) {
|
||||
return typeof identifier === 'string' && identifier.startsWith('guest_');
|
||||
}
|
||||
|
||||
/**
|
||||
* Получить статистику гостевых сообщений
|
||||
* @returns {Promise<Object>}
|
||||
*/
|
||||
async function getGuestStats() {
|
||||
try {
|
||||
const { rows } = await db.getQuery()(
|
||||
`SELECT
|
||||
COUNT(DISTINCT guest_id) as unique_guests,
|
||||
COUNT(*) as total_messages,
|
||||
MAX(created_at) as last_message_at
|
||||
FROM guest_messages`
|
||||
);
|
||||
|
||||
return rows[0];
|
||||
|
||||
} catch (error) {
|
||||
logger.error('[GuestService] Ошибка получения статистики:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
createGuestId,
|
||||
saveGuestMessage,
|
||||
getGuestMessages,
|
||||
deleteGuestMessages,
|
||||
isGuest,
|
||||
getGuestStats
|
||||
};
|
||||
|
||||
@@ -10,9 +10,8 @@
|
||||
* GitHub: https://github.com/HB3-ACCELERATOR
|
||||
*/
|
||||
|
||||
const { initTelegramBot } = require('./telegram-service');
|
||||
const emailBot = require('./emailBot');
|
||||
const telegramBot = require('./telegramBot');
|
||||
const botManager = require('./botManager');
|
||||
const botsSettings = require('./botsSettings');
|
||||
const aiAssistant = require('./ai-assistant');
|
||||
const {
|
||||
initializeVectorStore,
|
||||
@@ -20,16 +19,11 @@ const {
|
||||
similaritySearch,
|
||||
addDocument,
|
||||
} = require('./vectorStore');
|
||||
// ... другие импорты
|
||||
|
||||
module.exports = {
|
||||
// Telegram
|
||||
initTelegramBot,
|
||||
|
||||
// Email
|
||||
emailBot,
|
||||
sendEmail: emailBot.sendEmail,
|
||||
checkEmails: emailBot.checkEmails,
|
||||
// Bot Manager (новая архитектура)
|
||||
botManager,
|
||||
botsSettings,
|
||||
|
||||
// Vector Store
|
||||
initializeVectorStore,
|
||||
@@ -38,12 +32,10 @@ module.exports = {
|
||||
addDocument,
|
||||
|
||||
// AI Assistant
|
||||
aiAssistant,
|
||||
processMessage: aiAssistant.processMessage,
|
||||
getUserInfo: aiAssistant.getUserInfo,
|
||||
getConversationHistory: aiAssistant.getConversationHistory,
|
||||
|
||||
telegramBot,
|
||||
aiAssistant,
|
||||
|
||||
interfaceService: require('./interfaceService'),
|
||||
};
|
||||
|
||||
138
backend/services/messageDeduplicationService.js
Normal file
138
backend/services/messageDeduplicationService.js
Normal file
@@ -0,0 +1,138 @@
|
||||
/**
|
||||
* Copyright (c) 2024-2025 Тарабанов Александр Викторович
|
||||
* All rights reserved.
|
||||
*
|
||||
* This software is proprietary and confidential.
|
||||
* Unauthorized copying, modification, or distribution is prohibited.
|
||||
*
|
||||
* For licensing inquiries: info@hb3-accelerator.com
|
||||
* Website: https://hb3-accelerator.com
|
||||
* GitHub: https://github.com/HB3-ACCELERATOR
|
||||
*/
|
||||
|
||||
const crypto = require('crypto');
|
||||
const logger = require('../utils/logger');
|
||||
|
||||
/**
|
||||
* Сервис дедупликации сообщений
|
||||
* Предотвращает обработку дублирующихся сообщений
|
||||
*/
|
||||
|
||||
// Хранилище хешей обработанных сообщений (в памяти)
|
||||
const processedMessages = new Map();
|
||||
|
||||
// Время жизни записи о сообщении (5 минут)
|
||||
const MESSAGE_TTL = 5 * 60 * 1000;
|
||||
|
||||
/**
|
||||
* Создать хеш сообщения
|
||||
* @param {Object} messageData - Данные сообщения
|
||||
* @returns {string} Хеш сообщения
|
||||
*/
|
||||
function createMessageHash(messageData) {
|
||||
const hashData = {
|
||||
userId: messageData.userId || messageData.user_id,
|
||||
content: messageData.content,
|
||||
channel: messageData.channel,
|
||||
timestamp: Math.floor(Date.now() / 1000) // Округляем до секунд
|
||||
};
|
||||
|
||||
return crypto
|
||||
.createHash('sha256')
|
||||
.update(JSON.stringify(hashData))
|
||||
.digest('hex');
|
||||
}
|
||||
|
||||
/**
|
||||
* Проверить, было ли сообщение уже обработано
|
||||
* @param {Object} messageData - Данные сообщения
|
||||
* @returns {boolean} true если сообщение уже обрабатывалось
|
||||
*/
|
||||
function isDuplicate(messageData) {
|
||||
const hash = createMessageHash(messageData);
|
||||
|
||||
if (processedMessages.has(hash)) {
|
||||
const entry = processedMessages.get(hash);
|
||||
const now = Date.now();
|
||||
|
||||
// Проверяем, не истек ли TTL
|
||||
if (now - entry.timestamp < MESSAGE_TTL) {
|
||||
logger.warn('[MessageDeduplication] Обнаружено дублирующееся сообщение:', hash);
|
||||
return true;
|
||||
} else {
|
||||
// TTL истек, удаляем запись
|
||||
processedMessages.delete(hash);
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Пометить сообщение как обработанное
|
||||
* @param {Object} messageData - Данные сообщения
|
||||
*/
|
||||
function markAsProcessed(messageData) {
|
||||
const hash = createMessageHash(messageData);
|
||||
|
||||
processedMessages.set(hash, {
|
||||
timestamp: Date.now(),
|
||||
messageData: {
|
||||
userId: messageData.userId || messageData.user_id,
|
||||
channel: messageData.channel
|
||||
}
|
||||
});
|
||||
|
||||
// Очищаем старые записи
|
||||
cleanupOldEntries();
|
||||
}
|
||||
|
||||
/**
|
||||
* Очистить старые записи из хранилища
|
||||
*/
|
||||
function cleanupOldEntries() {
|
||||
const now = Date.now();
|
||||
let cleanedCount = 0;
|
||||
|
||||
for (const [hash, entry] of processedMessages.entries()) {
|
||||
if (now - entry.timestamp > MESSAGE_TTL) {
|
||||
processedMessages.delete(hash);
|
||||
cleanedCount++;
|
||||
}
|
||||
}
|
||||
|
||||
if (cleanedCount > 0) {
|
||||
logger.debug(`[MessageDeduplication] Очищено ${cleanedCount} старых записей`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Получить статистику дедупликации
|
||||
* @returns {Object}
|
||||
*/
|
||||
function getStats() {
|
||||
return {
|
||||
totalTracked: processedMessages.size,
|
||||
ttl: MESSAGE_TTL
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Очистить все записи (для тестов)
|
||||
*/
|
||||
function clear() {
|
||||
processedMessages.clear();
|
||||
logger.info('[MessageDeduplication] Хранилище очищено');
|
||||
}
|
||||
|
||||
// Периодическая очистка старых записей (каждую минуту)
|
||||
setInterval(cleanupOldEntries, 60 * 1000);
|
||||
|
||||
module.exports = {
|
||||
isDuplicate,
|
||||
markAsProcessed,
|
||||
getStats,
|
||||
clear,
|
||||
createMessageHash
|
||||
};
|
||||
|
||||
166
backend/services/notifyOllamaReady.js
Normal file
166
backend/services/notifyOllamaReady.js
Normal file
@@ -0,0 +1,166 @@
|
||||
/**
|
||||
* Copyright (c) 2024-2025 Тарабанов Александр Викторович
|
||||
* All rights reserved.
|
||||
*
|
||||
* This software is proprietary and confidential.
|
||||
* Unauthorized copying, modification, or distribution is prohibited.
|
||||
*
|
||||
* For licensing inquiries: info@hb3-accelerator.com
|
||||
* Website: https://hb3-accelerator.com
|
||||
* GitHub: https://github.com/HB3-ACCELERATOR
|
||||
*/
|
||||
|
||||
const axios = require('axios');
|
||||
const logger = require('../utils/logger');
|
||||
|
||||
/**
|
||||
* Скрипт для уведомления Ollama о готовности
|
||||
* Используется для проверки доступности Ollama и прогрева моделей
|
||||
*/
|
||||
|
||||
const OLLAMA_HOST = process.env.OLLAMA_HOST || 'http://ollama:11434';
|
||||
const MAX_RETRIES = 30;
|
||||
const RETRY_DELAY = 2000; // 2 секунды
|
||||
|
||||
/**
|
||||
* Проверить доступность Ollama
|
||||
* @returns {Promise<boolean>}
|
||||
*/
|
||||
async function checkOllamaHealth() {
|
||||
try {
|
||||
const response = await axios.get(`${OLLAMA_HOST}/api/tags`, {
|
||||
timeout: 5000
|
||||
});
|
||||
|
||||
return response.status === 200;
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Дождаться готовности Ollama с retry
|
||||
* @returns {Promise<boolean>}
|
||||
*/
|
||||
async function waitForOllama() {
|
||||
logger.info('[NotifyOllamaReady] Ожидание готовности Ollama...');
|
||||
|
||||
for (let i = 0; i < MAX_RETRIES; i++) {
|
||||
const isReady = await checkOllamaHealth();
|
||||
|
||||
if (isReady) {
|
||||
logger.info(`[NotifyOllamaReady] ✅ Ollama готов! (попытка ${i + 1}/${MAX_RETRIES})`);
|
||||
return true;
|
||||
}
|
||||
|
||||
logger.info(`[NotifyOllamaReady] Ollama не готов, повтор ${i + 1}/${MAX_RETRIES}...`);
|
||||
await new Promise(resolve => setTimeout(resolve, RETRY_DELAY));
|
||||
}
|
||||
|
||||
logger.error('[NotifyOllamaReady] ❌ Ollama не стал доступен после всех попыток');
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Получить список доступных моделей
|
||||
* @returns {Promise<Array>}
|
||||
*/
|
||||
async function getAvailableModels() {
|
||||
try {
|
||||
const response = await axios.get(`${OLLAMA_HOST}/api/tags`, {
|
||||
timeout: 5000
|
||||
});
|
||||
|
||||
return response.data.models || [];
|
||||
} catch (error) {
|
||||
logger.error('[NotifyOllamaReady] Ошибка получения моделей:', error.message);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Прогреть модель (загрузить в память)
|
||||
* @param {string} modelName - Название модели
|
||||
* @returns {Promise<boolean>}
|
||||
*/
|
||||
async function warmupModel(modelName) {
|
||||
try {
|
||||
logger.info(`[NotifyOllamaReady] Прогрев модели: ${modelName}`);
|
||||
|
||||
const response = await axios.post(`${OLLAMA_HOST}/api/generate`, {
|
||||
model: modelName,
|
||||
prompt: 'Hello',
|
||||
stream: false
|
||||
}, {
|
||||
timeout: 30000
|
||||
});
|
||||
|
||||
if (response.status === 200) {
|
||||
logger.info(`[NotifyOllamaReady] ✅ Модель ${modelName} прогрета`);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
} catch (error) {
|
||||
logger.error(`[NotifyOllamaReady] Ошибка прогрева модели ${modelName}:`, error.message);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Основная функция инициализации
|
||||
*/
|
||||
async function initialize() {
|
||||
try {
|
||||
logger.info('[NotifyOllamaReady] 🚀 Начало инициализации Ollama...');
|
||||
|
||||
// Ждем готовности Ollama
|
||||
const isReady = await waitForOllama();
|
||||
|
||||
if (!isReady) {
|
||||
logger.error('[NotifyOllamaReady] Не удалось дождаться готовности Ollama');
|
||||
return false;
|
||||
}
|
||||
|
||||
// Получаем список моделей
|
||||
const models = await getAvailableModels();
|
||||
logger.info(`[NotifyOllamaReady] Найдено моделей: ${models.length}`);
|
||||
|
||||
if (models.length > 0) {
|
||||
logger.info('[NotifyOllamaReady] Доступные модели:', models.map(m => m.name).join(', '));
|
||||
|
||||
// Прогреваем первую модель (опционально)
|
||||
if (process.env.WARMUP_MODEL === 'true' && models[0]) {
|
||||
await warmupModel(models[0].name);
|
||||
}
|
||||
}
|
||||
|
||||
logger.info('[NotifyOllamaReady] ✅ Инициализация завершена');
|
||||
return true;
|
||||
|
||||
} catch (error) {
|
||||
logger.error('[NotifyOllamaReady] Ошибка инициализации:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Если запущен напрямую как скрипт
|
||||
if (require.main === module) {
|
||||
initialize()
|
||||
.then(success => {
|
||||
process.exit(success ? 0 : 1);
|
||||
})
|
||||
.catch(error => {
|
||||
logger.error('[NotifyOllamaReady] Критическая ошибка:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
initialize,
|
||||
waitForOllama,
|
||||
checkOllamaHealth,
|
||||
getAvailableModels,
|
||||
warmupModel
|
||||
};
|
||||
|
||||
251
backend/services/ollamaConfig.js
Normal file
251
backend/services/ollamaConfig.js
Normal file
@@ -0,0 +1,251 @@
|
||||
/**
|
||||
* Copyright (c) 2024-2025 Тарабанов Александр Викторович
|
||||
* All rights reserved.
|
||||
*
|
||||
* This software is proprietary and confidential.
|
||||
* Unauthorized copying, modification, or distribution is prohibited.
|
||||
*
|
||||
* For licensing inquiries: info@hb3-accelerator.com
|
||||
* Website: https://hb3-accelerator.com
|
||||
* GitHub: https://github.com/HB3-ACCELERATOR
|
||||
*/
|
||||
|
||||
/**
|
||||
* Конфигурационный сервис для Ollama
|
||||
* Централизует все настройки и URL для Ollama API
|
||||
*
|
||||
* ВАЖНО: Настройки берутся из таблицы ai_providers_settings (через aiProviderSettingsService)
|
||||
*/
|
||||
|
||||
const logger = require('../utils/logger');
|
||||
|
||||
// Кэш для настроек из БД
|
||||
let settingsCache = null;
|
||||
|
||||
/**
|
||||
* Загружает настройки Ollama из базы данных
|
||||
* @returns {Promise<Object>} Настройки Ollama провайдера
|
||||
*/
|
||||
async function loadSettingsFromDb() {
|
||||
try {
|
||||
const aiProviderSettingsService = require('./aiProviderSettingsService');
|
||||
const settings = await aiProviderSettingsService.getProviderSettings('ollama');
|
||||
|
||||
if (settings) {
|
||||
settingsCache = settings;
|
||||
logger.info(`[ollamaConfig] Loaded settings from DB: model=${settings.selected_model}, base_url=${settings.base_url}`);
|
||||
}
|
||||
|
||||
return settings;
|
||||
} catch (error) {
|
||||
logger.error('[ollamaConfig] Ошибка загрузки настроек Ollama из БД:', error.message);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Получает базовый URL для Ollama (синхронная версия)
|
||||
* @returns {string} Базовый URL Ollama
|
||||
*/
|
||||
function getBaseUrl() {
|
||||
// Приоритет: кэш из БД > Docker дефолт
|
||||
if (settingsCache && settingsCache.base_url) {
|
||||
return settingsCache.base_url;
|
||||
}
|
||||
// URL по умолчанию для Docker
|
||||
return 'http://ollama:11434';
|
||||
}
|
||||
|
||||
/**
|
||||
* Получает базовый URL для Ollama (асинхронная версия)
|
||||
* @returns {Promise<string>} Базовый URL Ollama
|
||||
*/
|
||||
async function getBaseUrlAsync() {
|
||||
try {
|
||||
if (!settingsCache) {
|
||||
await loadSettingsFromDb();
|
||||
}
|
||||
|
||||
if (settingsCache && settingsCache.base_url) {
|
||||
return settingsCache.base_url;
|
||||
}
|
||||
} catch (error) {
|
||||
logger.warn('[ollamaConfig] Failed to load base_url from DB, using default');
|
||||
}
|
||||
|
||||
return 'http://ollama:11434';
|
||||
}
|
||||
|
||||
/**
|
||||
* Получает URL для конкретного API endpoint Ollama
|
||||
* @param {string} endpoint - Endpoint API (например: 'tags', 'generate')
|
||||
* @returns {string} Полный URL для API endpoint
|
||||
*/
|
||||
function getApiUrl(endpoint) {
|
||||
const baseUrl = getBaseUrl();
|
||||
return `${baseUrl}/api/${endpoint}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Получает модель по умолчанию для Ollama (синхронная версия)
|
||||
* @returns {string} Название модели
|
||||
*/
|
||||
function getDefaultModel() {
|
||||
// Приоритет: кэш из БД > дефолт
|
||||
if (settingsCache && settingsCache.selected_model) {
|
||||
return settingsCache.selected_model;
|
||||
}
|
||||
// Дефолтное значение если БД недоступна
|
||||
return 'qwen2.5:7b';
|
||||
}
|
||||
|
||||
/**
|
||||
* Получает модель асинхронно из БД
|
||||
* @returns {Promise<string>} Название модели из БД
|
||||
*/
|
||||
async function getDefaultModelAsync() {
|
||||
try {
|
||||
if (!settingsCache) {
|
||||
await loadSettingsFromDb();
|
||||
}
|
||||
|
||||
if (settingsCache && settingsCache.selected_model) {
|
||||
logger.info(`[ollamaConfig] Using model from DB: ${settingsCache.selected_model}`);
|
||||
return settingsCache.selected_model;
|
||||
}
|
||||
} catch (error) {
|
||||
logger.warn('[ollamaConfig] Failed to load model from DB, using default');
|
||||
}
|
||||
return 'qwen2.5:7b';
|
||||
}
|
||||
|
||||
/**
|
||||
* Получает embedding модель асинхронно из БД
|
||||
* @returns {Promise<string>} Название embedding модели из БД
|
||||
*/
|
||||
async function getEmbeddingModel() {
|
||||
try {
|
||||
if (!settingsCache) {
|
||||
await loadSettingsFromDb();
|
||||
}
|
||||
|
||||
if (settingsCache && settingsCache.embedding_model) {
|
||||
logger.info(`[ollamaConfig] Using embedding model from DB: ${settingsCache.embedding_model}`);
|
||||
return settingsCache.embedding_model;
|
||||
}
|
||||
} catch (error) {
|
||||
logger.warn('[ollamaConfig] Failed to load embedding model from DB, using default');
|
||||
}
|
||||
return 'mxbai-embed-large:latest';
|
||||
}
|
||||
|
||||
/**
|
||||
* Получает timeout для запросов к Ollama
|
||||
* @returns {number} Timeout в миллисекундах
|
||||
*/
|
||||
function getTimeout() {
|
||||
return 30000; // 30 секунд
|
||||
}
|
||||
|
||||
/**
|
||||
* Получает все конфигурационные параметры Ollama (синхронная версия)
|
||||
* @returns {Object} Объект с конфигурацией
|
||||
*/
|
||||
function getConfig() {
|
||||
return {
|
||||
baseUrl: getBaseUrl(),
|
||||
defaultModel: getDefaultModel(),
|
||||
timeout: getTimeout(),
|
||||
apiUrl: {
|
||||
tags: getApiUrl('tags'),
|
||||
generate: getApiUrl('generate'),
|
||||
chat: getApiUrl('chat'),
|
||||
models: getApiUrl('models'),
|
||||
show: getApiUrl('show'),
|
||||
pull: getApiUrl('pull'),
|
||||
push: getApiUrl('push')
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Получает все конфигурационные параметры Ollama (асинхронная версия)
|
||||
* @returns {Promise<Object>} Объект с конфигурацией
|
||||
*/
|
||||
async function getConfigAsync() {
|
||||
const baseUrl = await getBaseUrlAsync();
|
||||
const defaultModel = await getDefaultModelAsync();
|
||||
const embeddingModel = await getEmbeddingModel();
|
||||
|
||||
return {
|
||||
baseUrl,
|
||||
defaultModel,
|
||||
embeddingModel,
|
||||
timeout: getTimeout(),
|
||||
apiUrl: {
|
||||
tags: `${baseUrl}/api/tags`,
|
||||
generate: `${baseUrl}/api/generate`,
|
||||
chat: `${baseUrl}/api/chat`,
|
||||
models: `${baseUrl}/api/models`,
|
||||
show: `${baseUrl}/api/show`,
|
||||
pull: `${baseUrl}/api/pull`,
|
||||
push: `${baseUrl}/api/push`
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Очищает кэш настроек (для перезагрузки)
|
||||
*/
|
||||
function clearCache() {
|
||||
settingsCache = null;
|
||||
logger.info('[ollamaConfig] Settings cache cleared');
|
||||
}
|
||||
|
||||
/**
|
||||
* Проверяет доступность Ollama сервиса
|
||||
* @returns {Promise<Object>} Статус здоровья сервиса
|
||||
*/
|
||||
async function checkHealth() {
|
||||
try {
|
||||
const baseUrl = getBaseUrl();
|
||||
const response = await fetch(`${baseUrl}/api/tags`);
|
||||
|
||||
if (!response.ok) {
|
||||
return {
|
||||
status: 'error',
|
||||
error: `Ollama вернул код ${response.status}`,
|
||||
baseUrl
|
||||
};
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return {
|
||||
status: 'ok',
|
||||
baseUrl,
|
||||
model: getDefaultModel(),
|
||||
availableModels: data.models?.length || 0
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
status: 'error',
|
||||
error: error.message,
|
||||
baseUrl: getBaseUrl()
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getBaseUrl,
|
||||
getBaseUrlAsync,
|
||||
getApiUrl,
|
||||
getDefaultModel,
|
||||
getDefaultModelAsync,
|
||||
getEmbeddingModel,
|
||||
getTimeout,
|
||||
getConfig,
|
||||
getConfigAsync,
|
||||
loadSettingsFromDb,
|
||||
clearCache,
|
||||
checkHealth
|
||||
};
|
||||
@@ -31,7 +31,10 @@ async function getTableData(tableId) {
|
||||
const rows = await encryptedDb.getData('user_rows', { table_id: tableId });
|
||||
// console.log(`[RAG] Found ${rows.length} rows:`, rows.map(row => ({ id: row.id, name: row.name })));
|
||||
|
||||
const cellValues = await encryptedDb.getData('user_cell_values', { row_id: { $in: rows.map(row => row.id) } });
|
||||
// Исправление: проверяем что есть строки перед запросом cell_values
|
||||
const cellValues = rows.length > 0
|
||||
? await encryptedDb.getData('user_cell_values', { row_id: { $in: rows.map(row => row.id) } })
|
||||
: [];
|
||||
// console.log(`[RAG] Found ${cellValues.length} cell values`);
|
||||
|
||||
const getColId = purpose => columns.find(col => col.options?.purpose === purpose)?.id;
|
||||
@@ -120,7 +123,7 @@ async function ragAnswer({ tableId, userQuestion, product = null, threshold = 10
|
||||
|
||||
// Поиск
|
||||
let results = [];
|
||||
if (rowsForUpsert.length > 0) {
|
||||
if (rowsForUpsert.length > 0 && userQuestion && userQuestion.trim()) {
|
||||
results = await vectorSearch.search(tableId, userQuestion, 3); // Увеличиваем до 3 результатов для лучшего поиска
|
||||
// console.log(`[RAG] Search completed, got ${results.length} results`);
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
|
||||
const logger = require('../utils/logger');
|
||||
const encryptedDb = require('./encryptedDatabaseService');
|
||||
const { processGuestMessages } = require('../routes/chat');
|
||||
const guestMessageService = require('./guestMessageService');
|
||||
|
||||
/**
|
||||
* Сервис для работы с сессиями пользователей
|
||||
@@ -100,7 +100,7 @@ class SessionService {
|
||||
|
||||
// Обрабатываем сообщения для каждого гостевого ID
|
||||
for (const guestId of guestIdsToProcess) {
|
||||
await this.processGuestMessagesWrapper(userId, guestId);
|
||||
await guestMessageService.processGuestMessages(userId, guestId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -127,20 +127,7 @@ class SessionService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Обертка для функции processGuestMessages
|
||||
* @param {number} userId - ID пользователя
|
||||
* @param {string} guestId - ID гостя
|
||||
* @returns {Promise<Object>} - Результат операции
|
||||
*/
|
||||
async processGuestMessagesWrapper(userId, guestId) {
|
||||
try {
|
||||
return await processGuestMessages(userId, guestId);
|
||||
} catch (error) {
|
||||
logger.error(`[processGuestMessagesWrapper] Error: ${error.message}`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
// Обертка processGuestMessagesWrapper удалена - используется прямой вызов guestMessageService.processGuestMessages
|
||||
|
||||
/**
|
||||
* Получает сессию из хранилища по ID
|
||||
|
||||
@@ -13,317 +13,203 @@
|
||||
const { Telegraf } = require('telegraf');
|
||||
const logger = require('../utils/logger');
|
||||
const encryptedDb = require('./encryptedDatabaseService');
|
||||
const db = require('../db');
|
||||
const authService = require('./auth-service');
|
||||
const verificationService = require('./verification-service');
|
||||
const crypto = require('crypto');
|
||||
const identityService = require('./identity-service');
|
||||
const aiAssistant = require('./ai-assistant');
|
||||
const { checkAdminRole } = require('./admin-role');
|
||||
const { broadcastContactsUpdate, broadcastChatMessage } = require('../wsHub');
|
||||
const aiAssistantSettingsService = require('./aiAssistantSettingsService');
|
||||
const { ragAnswer, generateLLMResponse } = require('./ragService');
|
||||
const { isUserBlocked } = require('../utils/userUtils');
|
||||
|
||||
let botInstance = null;
|
||||
let telegramSettingsCache = null;
|
||||
/**
|
||||
* TelegramBot - обработчик Telegram сообщений
|
||||
* Унифицированный интерфейс для работы с Telegram
|
||||
*/
|
||||
class TelegramBot {
|
||||
constructor() {
|
||||
this.name = 'TelegramBot';
|
||||
this.channel = 'telegram';
|
||||
this.bot = null;
|
||||
this.settings = null;
|
||||
this.isInitialized = false;
|
||||
this.status = 'inactive';
|
||||
}
|
||||
|
||||
async function getTelegramSettings() {
|
||||
if (telegramSettingsCache) return telegramSettingsCache;
|
||||
|
||||
const settings = await encryptedDb.getData('telegram_settings', {}, 1);
|
||||
if (!settings.length) throw new Error('Telegram settings not found in DB');
|
||||
|
||||
telegramSettingsCache = settings[0];
|
||||
return telegramSettingsCache;
|
||||
}
|
||||
/**
|
||||
* Инициализация Telegram Bot
|
||||
*/
|
||||
async initialize() {
|
||||
try {
|
||||
logger.info('[TelegramBot] 🚀 Инициализация Telegram Bot...');
|
||||
|
||||
// Загружаем настройки из БД
|
||||
this.settings = await this.loadSettings();
|
||||
|
||||
if (!this.settings || !this.settings.bot_token) {
|
||||
logger.warn('[TelegramBot] ⚠️ Настройки Telegram не найдены');
|
||||
this.status = 'not_configured';
|
||||
return { success: false, reason: 'not_configured' };
|
||||
}
|
||||
|
||||
// Создание и настройка бота
|
||||
async function getBot() {
|
||||
// console.log('[TelegramBot] getBot() called');
|
||||
if (!botInstance) {
|
||||
// console.log('[TelegramBot] Creating new bot instance...');
|
||||
const settings = await getTelegramSettings();
|
||||
// console.log('[TelegramBot] Got settings, creating Telegraf instance...');
|
||||
botInstance = new Telegraf(settings.bot_token);
|
||||
// console.log('[TelegramBot] Telegraf instance created');
|
||||
// Проверяем токен
|
||||
if (!this.settings.bot_token || typeof this.settings.bot_token !== 'string') {
|
||||
logger.error('[TelegramBot] ❌ Некорректный токен:', {
|
||||
tokenExists: !!this.settings.bot_token,
|
||||
tokenType: typeof this.settings.bot_token,
|
||||
tokenLength: this.settings.bot_token?.length || 0
|
||||
});
|
||||
this.status = 'invalid_token';
|
||||
return { success: false, reason: 'invalid_token' };
|
||||
}
|
||||
|
||||
// Обработка команды /start
|
||||
botInstance.command('start', (ctx) => {
|
||||
// Проверяем токен через Telegram API
|
||||
try {
|
||||
logger.info('[TelegramBot] Проверяем токен через Telegram API...');
|
||||
const testBot = new Telegraf(this.settings.bot_token);
|
||||
const me = await testBot.telegram.getMe();
|
||||
logger.info('[TelegramBot] ✅ Токен валиден, бот:', me.username);
|
||||
// Не вызываем stop() - может вызвать ошибку
|
||||
} catch (error) {
|
||||
logger.error('[TelegramBot] ❌ Токен невалиден или проблема с API:', {
|
||||
message: error.message,
|
||||
code: error.code,
|
||||
response: error.response?.data
|
||||
});
|
||||
this.status = 'invalid_token';
|
||||
return { success: false, reason: 'invalid_token' };
|
||||
}
|
||||
|
||||
// Создаем экземпляр бота
|
||||
this.bot = new Telegraf(this.settings.bot_token);
|
||||
|
||||
// Настраиваем обработчики
|
||||
this.setupHandlers();
|
||||
|
||||
// Сначала помечаем как инициализированный
|
||||
this.isInitialized = true;
|
||||
this.status = 'active';
|
||||
|
||||
// Запускаем бота асинхронно (может долго подключаться)
|
||||
this.launch()
|
||||
.then(() => {
|
||||
logger.info('[TelegramBot] ✅ Бот успешно подключен к Telegram');
|
||||
this.status = 'active';
|
||||
})
|
||||
.catch(error => {
|
||||
logger.error('[TelegramBot] Ошибка подключения к Telegram:', {
|
||||
message: error.message,
|
||||
code: error.code,
|
||||
response: error.response?.data,
|
||||
stack: error.stack
|
||||
});
|
||||
this.status = 'error';
|
||||
});
|
||||
|
||||
logger.info('[TelegramBot] ✅ Telegram Bot инициализирован (подключение в фоне)');
|
||||
return { success: true };
|
||||
|
||||
} catch (error) {
|
||||
if (error.message.includes('409: Conflict')) {
|
||||
logger.warn('[TelegramBot] ⚠️ Telegram Bot уже запущен в другом процессе');
|
||||
this.status = 'conflict';
|
||||
} else {
|
||||
logger.error('[TelegramBot] ❌ Ошибка инициализации:', error);
|
||||
this.status = 'error';
|
||||
}
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Загрузка настроек из БД
|
||||
*/
|
||||
async loadSettings() {
|
||||
try {
|
||||
const settings = await encryptedDb.getData('telegram_settings', {}, 1);
|
||||
if (!settings.length) {
|
||||
return null;
|
||||
}
|
||||
return settings[0];
|
||||
} catch (error) {
|
||||
logger.error('[TelegramBot] Ошибка загрузки настроек:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Настройка обработчиков команд и сообщений
|
||||
*/
|
||||
setupHandlers() {
|
||||
// Обработчик команды /start
|
||||
this.bot.command('start', (ctx) => {
|
||||
logger.info('[TelegramBot] 📨 Получена команда /start');
|
||||
ctx.reply('Добро пожаловать! Отправьте код подтверждения для аутентификации.');
|
||||
});
|
||||
|
||||
// Универсальный обработчик текстовых сообщений
|
||||
botInstance.on('text', async (ctx) => {
|
||||
const text = ctx.message.text.trim();
|
||||
// 1. Если команда — пропустить
|
||||
if (text.startsWith('/')) return;
|
||||
|
||||
// Отправляем индикатор печати для улучшения UX
|
||||
const typingAction = ctx.replyWithChatAction('typing');
|
||||
|
||||
// 2. Проверка: это потенциальный код?
|
||||
const isPotentialCode = (str) => /^[A-Z0-9]{6}$/i.test(str);
|
||||
if (isPotentialCode(text)) {
|
||||
await typingAction;
|
||||
try {
|
||||
// Получаем код верификации для всех активных кодов с провайдером telegram
|
||||
const codes = await encryptedDb.getData('verification_codes', {
|
||||
code: text.toUpperCase(),
|
||||
provider: 'telegram',
|
||||
used: false
|
||||
}, 1);
|
||||
// Обработчик текстовых сообщений
|
||||
this.bot.on('text', async (ctx) => {
|
||||
logger.info('[TelegramBot] 📨 Получено текстовое сообщение');
|
||||
await this.handleTextMessage(ctx);
|
||||
});
|
||||
|
||||
if (codes.length === 0) {
|
||||
ctx.reply('Неверный код подтверждения');
|
||||
return;
|
||||
}
|
||||
// Обработчик документов
|
||||
this.bot.on('document', async (ctx) => {
|
||||
logger.info('[TelegramBot] 📨 Получен документ');
|
||||
await this.handleMessage(ctx);
|
||||
});
|
||||
|
||||
const verification = codes[0];
|
||||
const providerId = verification.provider_id;
|
||||
const linkedUserId = verification.user_id; // Получаем связанный userId если он есть
|
||||
let userId;
|
||||
let userRole = 'user'; // Роль по умолчанию
|
||||
// Обработчик фото
|
||||
this.bot.on('photo', async (ctx) => {
|
||||
logger.info('[TelegramBot] 📨 Получено фото');
|
||||
await this.handleMessage(ctx);
|
||||
});
|
||||
|
||||
// Отмечаем код как использованный
|
||||
await encryptedDb.saveData('verification_codes', {
|
||||
used: true
|
||||
}, {
|
||||
id: verification.id
|
||||
});
|
||||
// Обработчик аудио
|
||||
this.bot.on('audio', async (ctx) => {
|
||||
logger.info('[TelegramBot] 📨 Получено аудио');
|
||||
await this.handleMessage(ctx);
|
||||
});
|
||||
|
||||
logger.info('Starting Telegram auth process for code:', text);
|
||||
// Обработчик видео
|
||||
this.bot.on('video', async (ctx) => {
|
||||
logger.info('[TelegramBot] 📨 Получено видео');
|
||||
await this.handleMessage(ctx);
|
||||
});
|
||||
}
|
||||
|
||||
// Проверяем, существует ли уже пользователь с таким Telegram ID
|
||||
const existingTelegramUsers = await encryptedDb.getData('user_identities', {
|
||||
provider: 'telegram',
|
||||
provider_id: ctx.from.id.toString()
|
||||
}, 1);
|
||||
/**
|
||||
* Обработка текстовых сообщений
|
||||
*/
|
||||
async handleTextMessage(ctx) {
|
||||
const text = ctx.message.text.trim();
|
||||
|
||||
// Пропускаем команды
|
||||
if (text.startsWith('/')) return;
|
||||
|
||||
if (existingTelegramUsers.length > 0) {
|
||||
// Если пользователь с таким Telegram ID уже существует, используем его
|
||||
userId = existingTelegramUsers[0].user_id;
|
||||
logger.info(`Using existing user ${userId} for Telegram account ${ctx.from.id}`);
|
||||
} else {
|
||||
// Если код верификации был связан с существующим пользователем, используем его
|
||||
if (linkedUserId) {
|
||||
// Используем userId из кода верификации
|
||||
userId = linkedUserId;
|
||||
// Связываем Telegram с этим пользователем
|
||||
await encryptedDb.saveData('user_identities', {
|
||||
user_id: userId,
|
||||
provider: 'telegram',
|
||||
provider_id: ctx.from.id.toString()
|
||||
});
|
||||
logger.info(
|
||||
`Linked Telegram account ${ctx.from.id} to pre-authenticated user ${userId}`
|
||||
);
|
||||
} else {
|
||||
// Проверяем, есть ли пользователь, связанный с гостевым идентификатором
|
||||
let existingUserWithGuestId = null;
|
||||
if (providerId) {
|
||||
const guestUserResult = await encryptedDb.getData('guest_user_mapping', {
|
||||
guest_id: providerId
|
||||
}, 1);
|
||||
if (guestUserResult.length > 0) {
|
||||
existingUserWithGuestId = guestUserResult[0].user_id;
|
||||
logger.info(
|
||||
`Found existing user ${existingUserWithGuestId} by guest ID ${providerId}`
|
||||
);
|
||||
}
|
||||
}
|
||||
// Обрабатываем как обычное сообщение
|
||||
await this.handleMessage(ctx);
|
||||
}
|
||||
|
||||
if (existingUserWithGuestId) {
|
||||
// Используем существующего пользователя и добавляем ему Telegram идентификатор
|
||||
userId = existingUserWithGuestId;
|
||||
await encryptedDb.saveData('user_identities', {
|
||||
user_id: userId,
|
||||
provider: 'telegram',
|
||||
provider_id: ctx.from.id.toString()
|
||||
});
|
||||
logger.info(`Linked Telegram account ${ctx.from.id} to existing user ${userId}`);
|
||||
} else {
|
||||
// Создаем нового пользователя, если не нашли существующего
|
||||
const userResult = await encryptedDb.saveData('users', {
|
||||
created_at: new Date(),
|
||||
role: 'user'
|
||||
});
|
||||
userId = userResult.id;
|
||||
|
||||
// Связываем Telegram с новым пользователем
|
||||
await encryptedDb.saveData('user_identities', {
|
||||
user_id: userId,
|
||||
provider: 'telegram',
|
||||
provider_id: ctx.from.id.toString()
|
||||
});
|
||||
|
||||
// Если был гостевой ID, связываем его с новым пользователем
|
||||
if (providerId) {
|
||||
await encryptedDb.saveData('guest_user_mapping', {
|
||||
user_id: userId,
|
||||
guest_id: providerId
|
||||
}, {
|
||||
user_id: userId
|
||||
});
|
||||
}
|
||||
|
||||
logger.info(`Created new user ${userId} with Telegram account ${ctx.from.id}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ----> НАЧАЛО: Проверка роли на основе привязанного кошелька <----
|
||||
if (userId) { // Убедимся, что userId определен
|
||||
logger.info(`[TelegramBot] Checking linked wallet for determined userId: ${userId} (Type: ${typeof userId})`);
|
||||
try {
|
||||
const linkedWallet = await authService.getLinkedWallet(userId);
|
||||
if (linkedWallet) {
|
||||
logger.info(`[TelegramBot] Found linked wallet ${linkedWallet} for user ${userId}. Checking role...`);
|
||||
const isAdmin = await checkAdminRole(linkedWallet);
|
||||
userRole = isAdmin ? 'admin' : 'user';
|
||||
logger.info(`[TelegramBot] Role for user ${userId} determined as: ${userRole}`);
|
||||
|
||||
// Опционально: Обновить роль в таблице users
|
||||
const currentUser = await encryptedDb.getData('users', {
|
||||
id: userId
|
||||
}, 1);
|
||||
if (currentUser.length > 0 && currentUser[0].role !== userRole) {
|
||||
await encryptedDb.saveData('users', {
|
||||
role: userRole
|
||||
}, {
|
||||
id: userId
|
||||
});
|
||||
logger.info(`[TelegramBot] Updated user role in DB to ${userRole}`);
|
||||
}
|
||||
} else {
|
||||
logger.info(`[TelegramBot] No linked wallet found for user ${userId}. Checking current DB role.`);
|
||||
// Если кошелька нет, берем текущую роль из базы
|
||||
const currentUser = await encryptedDb.getData('users', {
|
||||
id: userId
|
||||
}, 1);
|
||||
if (currentUser.length > 0) {
|
||||
userRole = currentUser[0].role;
|
||||
}
|
||||
}
|
||||
} catch (roleCheckError) {
|
||||
logger.error(`[TelegramBot] Error checking admin role for user ${userId}:`, roleCheckError);
|
||||
// В случае ошибки берем роль из базы или оставляем 'user'
|
||||
try {
|
||||
const currentUser = await encryptedDb.getData('users', {
|
||||
id: userId
|
||||
}, 1);
|
||||
if (currentUser.length > 0) { userRole = currentUser[0].role; }
|
||||
} catch (dbError) { /* ignore */ }
|
||||
}
|
||||
} else {
|
||||
logger.error('[TelegramBot] Cannot check role because userId is undefined!');
|
||||
}
|
||||
// ----> КОНЕЦ: Проверка роли <----
|
||||
|
||||
// Логируем userId перед обновлением сессии
|
||||
logger.info(`[telegramBot] Attempting to update session for userId: ${userId}`);
|
||||
|
||||
// Находим последнюю активную сессию для данного userId
|
||||
let activeSessionId = null;
|
||||
try {
|
||||
// Ищем сессию, где есть userId и она не истекла (проверка expires_at)
|
||||
// Сортируем по expires_at DESC чтобы взять самую "свежую", если их несколько
|
||||
const sessionResult = await encryptedDb.getData('session', {
|
||||
'sess->>userId': userId?.toString()
|
||||
}, 1, 'expire', 'DESC');
|
||||
|
||||
if (sessionResult.length > 0) {
|
||||
activeSessionId = sessionResult[0].sid;
|
||||
logger.info(`[telegramBot] Found active session ID ${activeSessionId} for user ${userId}`);
|
||||
|
||||
// Обновляем найденную сессию в базе данных, добавляя/перезаписывая данные Telegram
|
||||
const updateResult = await encryptedDb.saveData('session', {
|
||||
sess: JSON.stringify({
|
||||
// authenticated: true, // Не перезаписываем, т.к. сессия уже должна быть аутентифицирована
|
||||
authType: 'telegram', // Обновляем тип аутентификации
|
||||
telegramId: ctx.from.id.toString(),
|
||||
telegramUsername: ctx.from.username,
|
||||
telegramFirstName: ctx.from.first_name,
|
||||
role: userRole, // Записываем определенную роль
|
||||
// userId: userId?.toString() // userId уже должен быть в сессии
|
||||
})
|
||||
}, {
|
||||
sid: activeSessionId
|
||||
});
|
||||
|
||||
if (updateResult.rowCount > 0) {
|
||||
logger.info(`[telegramBot] Session ${activeSessionId} updated successfully with Telegram data for user ${userId}`);
|
||||
} else {
|
||||
logger.warn(`[telegramBot] Session update query executed but did not update rows for sid: ${activeSessionId}. This might indicate a concurrency issue or incorrect sid.`);
|
||||
}
|
||||
|
||||
} else {
|
||||
logger.warn(`[telegramBot] No active web session found for userId: ${userId}. Telegram is linked, but the user might need to refresh their browser session.`);
|
||||
}
|
||||
} catch(sessionError) {
|
||||
logger.error(`[telegramBot] Error finding or updating session for userId ${userId}:`, sessionError);
|
||||
}
|
||||
|
||||
// Отправляем сообщение об успешной аутентификации
|
||||
await ctx.reply('Аутентификация успешна! Можете вернуться в приложение.');
|
||||
|
||||
// Удаляем сообщение с кодом
|
||||
try {
|
||||
await ctx.deleteMessage(ctx.message.message_id);
|
||||
} catch (error) {
|
||||
logger.warn('Could not delete code message:', error);
|
||||
}
|
||||
|
||||
// После каждого успешного создания пользователя:
|
||||
broadcastContactsUpdate();
|
||||
} catch (error) {
|
||||
logger.error('Error in Telegram auth:', error);
|
||||
await ctx.reply('Произошла ошибка при аутентификации. Попробуйте позже.');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// 3. Всё остальное — чат с ИИ-ассистентом
|
||||
/**
|
||||
* Извлечение данных из Telegram сообщения
|
||||
* @param {Object} ctx - Telegraf context
|
||||
* @returns {Object} - Стандартизированные данные сообщения
|
||||
*/
|
||||
extractMessageData(ctx) {
|
||||
try {
|
||||
const telegramId = ctx.from.id.toString();
|
||||
|
||||
// 1. Найти или создать пользователя
|
||||
const { userId, role } = await identityService.findOrCreateUserWithRole('telegram', telegramId);
|
||||
if (await isUserBlocked(userId)) {
|
||||
await ctx.reply('Вы заблокированы. Сообщения не принимаются.');
|
||||
return;
|
||||
}
|
||||
|
||||
// 1.1 Найти или создать беседу
|
||||
let conversationResult = await encryptedDb.getData('conversations', {
|
||||
user_id: userId
|
||||
}, 1, 'updated_at', 'DESC', 'created_at', 'DESC');
|
||||
let conversation;
|
||||
if (conversationResult.length === 0) {
|
||||
const title = `Чат с пользователем ${userId}`;
|
||||
const newConv = await encryptedDb.saveData('conversations', {
|
||||
user_id: userId,
|
||||
title: title,
|
||||
created_at: new Date(),
|
||||
updated_at: new Date()
|
||||
});
|
||||
conversation = newConv;
|
||||
} else {
|
||||
conversation = conversationResult[0];
|
||||
}
|
||||
|
||||
// 2. Сохранять все сообщения с conversation_id
|
||||
let content = text;
|
||||
let attachmentMeta = {};
|
||||
// Проверяем вложения (фото, документ, аудио, видео)
|
||||
let fileId, fileName, mimeType, fileSize, attachmentBuffer;
|
||||
let content = '';
|
||||
let attachments = [];
|
||||
|
||||
// Текст сообщения
|
||||
if (ctx.message.text) {
|
||||
content = ctx.message.text.trim();
|
||||
} else if (ctx.message.caption) {
|
||||
content = ctx.message.caption.trim();
|
||||
}
|
||||
|
||||
// Обработка вложений
|
||||
let fileId, fileName, mimeType, fileSize;
|
||||
|
||||
if (ctx.message.document) {
|
||||
fileId = ctx.message.document.file_id;
|
||||
fileName = ctx.message.document.file_name;
|
||||
mimeType = ctx.message.document.mime_type;
|
||||
fileSize = ctx.message.document.file_size;
|
||||
} else if (ctx.message.photo && ctx.message.photo.length > 0) {
|
||||
// Берём самое большое фото
|
||||
const photo = ctx.message.photo[ctx.message.photo.length - 1];
|
||||
fileId = photo.file_id;
|
||||
fileName = 'photo.jpg';
|
||||
@@ -341,339 +227,185 @@ async function getBot() {
|
||||
fileSize = ctx.message.video.file_size;
|
||||
}
|
||||
|
||||
// Асинхронная загрузка файлов
|
||||
if (fileId) {
|
||||
try {
|
||||
const fileLink = await ctx.telegram.getFileLink(fileId);
|
||||
const res = await fetch(fileLink.href);
|
||||
attachmentBuffer = await res.buffer();
|
||||
attachmentMeta = {
|
||||
attachment_filename: fileName,
|
||||
attachment_mimetype: mimeType,
|
||||
attachment_size: fileSize,
|
||||
attachment_data: attachmentBuffer
|
||||
};
|
||||
} catch (fileError) {
|
||||
logger.error('[TelegramBot] Error downloading file:', fileError);
|
||||
// Продолжаем без файла
|
||||
}
|
||||
}
|
||||
|
||||
// Сохраняем сообщение в БД
|
||||
if (!conversation || !conversation.id) {
|
||||
logger.error(`[TelegramBot] Conversation is undefined or has no id for user ${userId}`);
|
||||
await ctx.reply('Произошла ошибка при создании диалога. Попробуйте позже.');
|
||||
return;
|
||||
}
|
||||
|
||||
const userMessage = await encryptedDb.saveData('messages', {
|
||||
user_id: userId,
|
||||
conversation_id: conversation.id,
|
||||
sender_type: 'user',
|
||||
content: content,
|
||||
channel: 'telegram',
|
||||
role: role,
|
||||
direction: 'in',
|
||||
created_at: new Date(),
|
||||
attachment_filename: attachmentMeta.attachment_filename || null,
|
||||
attachment_mimetype: attachmentMeta.attachment_mimetype || null,
|
||||
attachment_size: attachmentMeta.attachment_size || null,
|
||||
attachment_data: attachmentMeta.attachment_data || null
|
||||
attachments.push({
|
||||
type: 'telegram_file',
|
||||
fileId: fileId,
|
||||
filename: fileName,
|
||||
mimetype: mimeType,
|
||||
size: fileSize,
|
||||
ctx: ctx // Сохраняем контекст для последующей загрузки
|
||||
});
|
||||
|
||||
// Отправляем WebSocket уведомление о пользовательском сообщении
|
||||
try {
|
||||
const decryptedUserMessage = await encryptedDb.getData('messages', { id: userMessage.id }, 1);
|
||||
if (decryptedUserMessage && decryptedUserMessage[0]) {
|
||||
broadcastChatMessage(decryptedUserMessage[0], userId);
|
||||
}
|
||||
} catch (wsError) {
|
||||
logger.error('[TelegramBot] WebSocket notification error for user message:', wsError);
|
||||
}
|
||||
|
||||
if (await isUserBlocked(userId)) {
|
||||
logger.info(`[TelegramBot] Пользователь ${userId} заблокирован — ответ ИИ не отправляется.`);
|
||||
return;
|
||||
}
|
||||
|
||||
// 3. Получить ответ от ИИ (RAG + LLM) - асинхронно
|
||||
const aiResponsePromise = (async () => {
|
||||
const aiSettings = await aiAssistantSettingsService.getSettings();
|
||||
let ragTableId = null;
|
||||
if (aiSettings && aiSettings.selected_rag_tables) {
|
||||
ragTableId = Array.isArray(aiSettings.selected_rag_tables)
|
||||
? aiSettings.selected_rag_tables[0]
|
||||
: aiSettings.selected_rag_tables;
|
||||
}
|
||||
|
||||
// Загружаем историю сообщений для контекста (ограничиваем до 5 сообщений)
|
||||
let history = null;
|
||||
try {
|
||||
const recentMessages = await encryptedDb.getData('messages', {
|
||||
conversation_id: conversation.id
|
||||
}, 5, 'created_at DESC');
|
||||
|
||||
if (recentMessages && recentMessages.length > 0) {
|
||||
// Преобразуем сообщения в формат для AI
|
||||
history = recentMessages.reverse().map(msg => ({
|
||||
// Любые человеческие роли трактуем как 'user', только ответы ассистента — 'assistant'
|
||||
role: msg.sender_type === 'assistant' ? 'assistant' : 'user',
|
||||
content: msg.content || ''
|
||||
}));
|
||||
}
|
||||
} catch (historyError) {
|
||||
logger.error('[TelegramBot] Error loading message history:', historyError);
|
||||
}
|
||||
|
||||
let aiResponse;
|
||||
if (ragTableId) {
|
||||
// Сначала ищем ответ через RAG
|
||||
const ragResult = await ragAnswer({ tableId: ragTableId, userQuestion: content });
|
||||
if (ragResult && ragResult.answer && typeof ragResult.score === 'number' && Math.abs(ragResult.score) <= 0.1) {
|
||||
aiResponse = ragResult.answer;
|
||||
} else {
|
||||
// Используем очередь AIQueue для LLM генерации
|
||||
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('requestCompleted', onCompleted);
|
||||
aiAssistant.aiQueue.off('requestFailed', onFailed);
|
||||
reject(new Error(item.error));
|
||||
}
|
||||
};
|
||||
|
||||
aiAssistant.aiQueue.on('requestCompleted', onCompleted);
|
||||
aiAssistant.aiQueue.on('requestFailed', onFailed);
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// Используем очередь 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;
|
||||
})();
|
||||
|
||||
// Ждем ответ от ИИ с таймаутом
|
||||
const aiResponse = await Promise.race([
|
||||
aiResponsePromise,
|
||||
new Promise((_, reject) =>
|
||||
setTimeout(() => reject(new Error('AI response timeout')), 120000)
|
||||
)
|
||||
]);
|
||||
|
||||
// 4. Сохранить ответ в БД с conversation_id
|
||||
const aiMessage = await encryptedDb.saveData('messages', {
|
||||
user_id: userId,
|
||||
conversation_id: conversation.id,
|
||||
sender_type: 'assistant',
|
||||
content: aiResponse,
|
||||
channel: 'telegram',
|
||||
role: 'assistant',
|
||||
direction: 'out',
|
||||
created_at: new Date()
|
||||
});
|
||||
|
||||
// 5. Отправить ответ пользователю
|
||||
await ctx.reply(aiResponse);
|
||||
|
||||
// 6. Отправить WebSocket уведомление
|
||||
try {
|
||||
const decryptedAiMessage = await encryptedDb.getData('messages', { id: aiMessage.id }, 1);
|
||||
if (decryptedAiMessage && decryptedAiMessage[0]) {
|
||||
broadcastChatMessage(decryptedAiMessage[0], userId);
|
||||
}
|
||||
} catch (wsError) {
|
||||
logger.error('[TelegramBot] WebSocket notification error:', wsError);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('[TelegramBot] Ошибка при обработке сообщения:', error);
|
||||
await ctx.reply('Произошла ошибка при обработке вашего сообщения. Попробуйте позже.');
|
||||
}
|
||||
});
|
||||
|
||||
// Запуск бота с таймаутом
|
||||
// console.log('[TelegramBot] Before botInstance.launch()');
|
||||
try {
|
||||
// Запускаем бота с таймаутом
|
||||
const launchPromise = botInstance.launch();
|
||||
const timeoutPromise = new Promise((_, reject) => {
|
||||
setTimeout(() => reject(new Error('Telegram bot launch timeout')), 30000); // 30 секунд таймаут
|
||||
});
|
||||
|
||||
await Promise.race([launchPromise, timeoutPromise]);
|
||||
// console.log('[TelegramBot] After botInstance.launch()');
|
||||
logger.info('[TelegramBot] Бот запущен');
|
||||
return {
|
||||
channel: 'telegram',
|
||||
identifier: telegramId,
|
||||
content: content,
|
||||
attachments: attachments,
|
||||
metadata: {
|
||||
telegramUsername: ctx.from.username,
|
||||
telegramFirstName: ctx.from.first_name,
|
||||
telegramLastName: ctx.from.last_name,
|
||||
messageId: ctx.message.message_id,
|
||||
chatId: ctx.chat.id
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
// console.error('[TelegramBot] Error launching bot:', error);
|
||||
// Не выбрасываем ошибку, чтобы не блокировать запуск сервера
|
||||
// console.log('[TelegramBot] Bot launch failed, but continuing...');
|
||||
}
|
||||
}
|
||||
|
||||
return botInstance;
|
||||
}
|
||||
|
||||
// Остановка бота
|
||||
async function stopBot() {
|
||||
if (botInstance) {
|
||||
try {
|
||||
await botInstance.stop();
|
||||
botInstance = null;
|
||||
logger.info('Telegram bot stopped successfully');
|
||||
} catch (error) {
|
||||
logger.error('Error stopping Telegram bot:', error);
|
||||
logger.error('[TelegramBot] Ошибка извлечения данных из сообщения:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Инициализация процесса аутентификации
|
||||
async function initTelegramAuth(session) {
|
||||
try {
|
||||
// Используем временный идентификатор для создания кода верификации
|
||||
// Реальный пользователь будет создан или найден при проверке кода через бота
|
||||
const tempId = crypto.randomBytes(16).toString('hex');
|
||||
/**
|
||||
* Загрузка файла из Telegram
|
||||
* @param {Object} attachment - Данные вложения
|
||||
* @returns {Promise<Buffer>} - Буфер с данными файла
|
||||
*/
|
||||
async downloadAttachment(attachment) {
|
||||
try {
|
||||
const fileLink = await attachment.ctx.telegram.getFileLink(attachment.fileId);
|
||||
const res = await fetch(fileLink.href);
|
||||
return await res.buffer();
|
||||
} catch (error) {
|
||||
logger.error('[TelegramBot] Ошибка загрузки файла:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Если пользователь уже авторизован, сохраняем его userId в guest_user_mapping
|
||||
// чтобы потом при авторизации через бота этот пользователь был найден
|
||||
if (session && session.authenticated && session.userId) {
|
||||
const guestId = session.guestId || tempId;
|
||||
/**
|
||||
* Обработка сообщения через процессор
|
||||
* @param {Object} ctx - Telegraf context
|
||||
* @param {Function} processor - Функция обработки сообщения
|
||||
*/
|
||||
async handleMessage(ctx, processor = null) {
|
||||
try {
|
||||
await ctx.replyWithChatAction('typing');
|
||||
|
||||
// Извлекаем данные из сообщения
|
||||
const messageData = this.extractMessageData(ctx);
|
||||
|
||||
logger.info(`[TelegramBot] Обработка сообщения от пользователя: ${messageData.identifier}`);
|
||||
|
||||
// Связываем гостевой ID с текущим пользователем
|
||||
await encryptedDb.saveData('guest_user_mapping', {
|
||||
user_id: session.userId,
|
||||
guest_id: guestId
|
||||
}, {
|
||||
user_id: session.userId
|
||||
// Загружаем вложения если есть
|
||||
for (const attachment of messageData.attachments) {
|
||||
const buffer = await this.downloadAttachment(attachment);
|
||||
if (buffer) {
|
||||
attachment.data = buffer;
|
||||
// Удаляем ctx из вложения
|
||||
delete attachment.ctx;
|
||||
}
|
||||
}
|
||||
|
||||
// Используем установленный процессор или переданный
|
||||
const messageProcessor = processor || this.messageProcessor;
|
||||
|
||||
if (!messageProcessor) {
|
||||
await ctx.reply('Сообщение получено и будет обработано.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Обрабатываем сообщение через унифицированный процессор
|
||||
const result = await messageProcessor(messageData);
|
||||
|
||||
// Отправляем ответ пользователю
|
||||
if (result.success && result.aiResponse) {
|
||||
await ctx.reply(result.aiResponse.response);
|
||||
} else if (result.success) {
|
||||
await ctx.reply('Сообщение получено');
|
||||
} else {
|
||||
await ctx.reply('Произошла ошибка при обработке сообщения');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
logger.error('[TelegramBot] Ошибка обработки сообщения:', error);
|
||||
try {
|
||||
await ctx.reply('Произошла ошибка при обработке вашего сообщения. Попробуйте позже.');
|
||||
} catch (replyError) {
|
||||
logger.error('[TelegramBot] Не удалось отправить сообщение об ошибке:', replyError);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Запуск бота (без timeout и retry - Telegraf сам управляет подключением)
|
||||
*/
|
||||
async launch() {
|
||||
try {
|
||||
logger.info('[TelegramBot] Запуск polling...');
|
||||
|
||||
// Запускаем бота без таймаута - пусть Telegraf сам управляет подключением
|
||||
await this.bot.launch({
|
||||
dropPendingUpdates: true,
|
||||
allowedUpdates: ['message', 'callback_query']
|
||||
});
|
||||
|
||||
logger.info(
|
||||
`[initTelegramAuth] Linked guestId ${guestId} to authenticated user ${session.userId}`
|
||||
);
|
||||
|
||||
logger.info('[TelegramBot] ✅ Бот запущен успешно');
|
||||
} catch (error) {
|
||||
logger.error('[TelegramBot] ❌ Ошибка запуска:', {
|
||||
message: error.message,
|
||||
code: error.code,
|
||||
response: error.response?.data,
|
||||
stack: error.stack
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Создаем код через сервис верификации с идентификатором
|
||||
const code = await verificationService.createVerificationCode(
|
||||
'telegram',
|
||||
session.guestId || tempId,
|
||||
session.authenticated ? session.userId : null
|
||||
);
|
||||
|
||||
logger.info(
|
||||
`[initTelegramAuth] Created verification code for guestId: ${session.guestId || tempId}${session.authenticated ? `, userId: ${session.userId}` : ''}`
|
||||
);
|
||||
/**
|
||||
* Установка процессора сообщений
|
||||
* @param {Function} processor - Функция обработки сообщений
|
||||
*/
|
||||
setMessageProcessor(processor) {
|
||||
this.messageProcessor = processor;
|
||||
logger.info('[TelegramBot] ✅ Процессор сообщений установлен');
|
||||
}
|
||||
|
||||
const settings = await getTelegramSettings();
|
||||
/**
|
||||
* Проверка статуса бота
|
||||
* @returns {Object} - Статус бота
|
||||
*/
|
||||
getStatus() {
|
||||
return {
|
||||
verificationCode: code,
|
||||
botLink: `https://t.me/${settings.bot_username}`,
|
||||
name: this.name,
|
||||
channel: this.channel,
|
||||
isInitialized: this.isInitialized,
|
||||
status: this.status,
|
||||
hasSettings: !!this.settings
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Получение экземпляра бота (для совместимости)
|
||||
* @returns {Object} - Экземпляр Telegraf бота
|
||||
*/
|
||||
getBot() {
|
||||
return this.bot;
|
||||
}
|
||||
|
||||
/**
|
||||
* Остановка бота
|
||||
*/
|
||||
async stop() {
|
||||
try {
|
||||
logger.info('[TelegramBot] 🛑 Остановка Telegram Bot...');
|
||||
|
||||
if (this.bot) {
|
||||
await this.bot.stop();
|
||||
this.bot = null;
|
||||
}
|
||||
|
||||
this.isInitialized = false;
|
||||
this.status = 'inactive';
|
||||
|
||||
logger.info('[TelegramBot] ✅ Telegram Bot остановлен');
|
||||
} catch (error) {
|
||||
logger.error('Error initializing Telegram auth:', error);
|
||||
logger.error('[TelegramBot] ❌ Ошибка остановки:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
module.exports = TelegramBot;
|
||||
|
||||
async function getAllBots() {
|
||||
const settings = await encryptedDb.getData('telegram_settings', {}, 1, 'id');
|
||||
return settings;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getTelegramSettings,
|
||||
getBot,
|
||||
stopBot,
|
||||
initTelegramAuth,
|
||||
clearSettingsCache,
|
||||
saveTelegramSettings,
|
||||
getAllBots,
|
||||
};
|
||||
|
||||
213
backend/services/testNewBots.js
Normal file
213
backend/services/testNewBots.js
Normal file
@@ -0,0 +1,213 @@
|
||||
/**
|
||||
* Copyright (c) 2024-2025 Тарабанов Александр Викторович
|
||||
* All rights reserved.
|
||||
*
|
||||
* This software is proprietary and confidential.
|
||||
* Unauthorized copying, modification, or distribution is prohibited.
|
||||
*
|
||||
* For licensing inquiries: info@hb3-accelerator.com
|
||||
* Website: https://hb3-accelerator.com
|
||||
* GitHub: https://github.com/HB3-ACCELERATOR
|
||||
*/
|
||||
|
||||
const logger = require('../utils/logger');
|
||||
const botManager = require('./botManager');
|
||||
const WebBot = require('./webBot');
|
||||
const TelegramBot = require('./telegramBot');
|
||||
const EmailBot = require('./emailBot');
|
||||
|
||||
/**
|
||||
* Тестовый скрипт для проверки новой архитектуры ботов
|
||||
* Используется для отладки и тестирования ботов
|
||||
*/
|
||||
|
||||
/**
|
||||
* Тест Web Bot
|
||||
*/
|
||||
async function testWebBot() {
|
||||
console.log('\n=== ТЕСТ WEB BOT ===');
|
||||
|
||||
try {
|
||||
const webBot = new WebBot();
|
||||
|
||||
// Инициализация
|
||||
const initResult = await webBot.initialize();
|
||||
console.log('✓ Инициализация:', initResult);
|
||||
|
||||
// Статус
|
||||
const status = webBot.getStatus();
|
||||
console.log('✓ Статус:', status);
|
||||
|
||||
// Тестовое сообщение
|
||||
const testMessage = {
|
||||
userId: 1,
|
||||
content: 'Тестовое сообщение',
|
||||
channel: 'web'
|
||||
};
|
||||
|
||||
console.log('✓ Web Bot тест пройден');
|
||||
return true;
|
||||
|
||||
} catch (error) {
|
||||
console.error('✗ Ошибка Web Bot:', error.message);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Тест Telegram Bot
|
||||
*/
|
||||
async function testTelegramBot() {
|
||||
console.log('\n=== ТЕСТ TELEGRAM BOT ===');
|
||||
|
||||
try {
|
||||
const telegramBot = new TelegramBot();
|
||||
|
||||
// Инициализация
|
||||
const initResult = await telegramBot.initialize();
|
||||
console.log('✓ Инициализация:', initResult);
|
||||
|
||||
// Статус
|
||||
const status = telegramBot.getStatus ? telegramBot.getStatus() : {
|
||||
isInitialized: telegramBot.isInitialized,
|
||||
status: telegramBot.status
|
||||
};
|
||||
console.log('✓ Статус:', status);
|
||||
|
||||
console.log('✓ Telegram Bot тест пройден');
|
||||
return true;
|
||||
|
||||
} catch (error) {
|
||||
console.error('✗ Ошибка Telegram Bot:', error.message);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Тест Email Bot
|
||||
*/
|
||||
async function testEmailBot() {
|
||||
console.log('\n=== ТЕСТ EMAIL BOT ===');
|
||||
|
||||
try {
|
||||
const emailBot = new EmailBot();
|
||||
|
||||
// Инициализация
|
||||
const initResult = await emailBot.initialize();
|
||||
console.log('✓ Инициализация:', initResult);
|
||||
|
||||
// Статус
|
||||
const status = emailBot.getStatus ? emailBot.getStatus() : {
|
||||
isInitialized: emailBot.isInitialized,
|
||||
status: emailBot.status
|
||||
};
|
||||
console.log('✓ Статус:', status);
|
||||
|
||||
console.log('✓ Email Bot тест пройден');
|
||||
return true;
|
||||
|
||||
} catch (error) {
|
||||
console.error('✗ Ошибка Email Bot:', error.message);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Тест Bot Manager
|
||||
*/
|
||||
async function testBotManager() {
|
||||
console.log('\n=== ТЕСТ BOT MANAGER ===');
|
||||
|
||||
try {
|
||||
// Инициализация
|
||||
await botManager.initialize();
|
||||
console.log('✓ BotManager инициализирован');
|
||||
|
||||
// Проверка готовности
|
||||
const isReady = botManager.isReady();
|
||||
console.log('✓ isReady:', isReady);
|
||||
|
||||
// Получение статуса
|
||||
const status = botManager.getStatus();
|
||||
console.log('✓ Статус всех ботов:', status);
|
||||
|
||||
// Получение конкретных ботов
|
||||
const webBot = botManager.getBot('web');
|
||||
const telegramBot = botManager.getBot('telegram');
|
||||
const emailBot = botManager.getBot('email');
|
||||
|
||||
console.log('✓ Web Bot:', webBot ? 'OK' : 'NOT FOUND');
|
||||
console.log('✓ Telegram Bot:', telegramBot ? 'OK' : 'NOT FOUND');
|
||||
console.log('✓ Email Bot:', emailBot ? 'OK' : 'NOT FOUND');
|
||||
|
||||
console.log('✓ Bot Manager тест пройден');
|
||||
return true;
|
||||
|
||||
} catch (error) {
|
||||
console.error('✗ Ошибка Bot Manager:', error.message);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Запустить все тесты
|
||||
*/
|
||||
async function runAllTests() {
|
||||
console.log('╔═══════════════════════════════════════╗');
|
||||
console.log('║ ТЕСТИРОВАНИЕ НОВОЙ АРХИТЕКТУРЫ БОТОВ ║');
|
||||
console.log('╚═══════════════════════════════════════╝');
|
||||
|
||||
const results = {
|
||||
webBot: false,
|
||||
telegramBot: false,
|
||||
emailBot: false,
|
||||
botManager: false
|
||||
};
|
||||
|
||||
try {
|
||||
// Тестируем каждый компонент
|
||||
results.webBot = await testWebBot();
|
||||
results.telegramBot = await testTelegramBot();
|
||||
results.emailBot = await testEmailBot();
|
||||
results.botManager = await testBotManager();
|
||||
|
||||
// Итоги
|
||||
console.log('\n╔═══════════════════════════════════════╗');
|
||||
console.log('║ ИТОГИ ТЕСТИРОВАНИЯ ║');
|
||||
console.log('╚═══════════════════════════════════════╝');
|
||||
console.log('Web Bot: ', results.webBot ? '✓ PASS' : '✗ FAIL');
|
||||
console.log('Telegram Bot: ', results.telegramBot ? '✓ PASS' : '✗ FAIL');
|
||||
console.log('Email Bot: ', results.emailBot ? '✓ PASS' : '✗ FAIL');
|
||||
console.log('Bot Manager: ', results.botManager ? '✓ PASS' : '✗ FAIL');
|
||||
|
||||
const allPassed = Object.values(results).every(r => r === true);
|
||||
console.log('\nОБЩИЙ РЕЗУЛЬТАТ:', allPassed ? '✓ ВСЕ ТЕСТЫ ПРОЙДЕНЫ' : '✗ ЕСТЬ ОШИБКИ');
|
||||
|
||||
return allPassed;
|
||||
|
||||
} catch (error) {
|
||||
logger.error('[TestNewBots] Критическая ошибка:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Если запущен напрямую как скрипт
|
||||
if (require.main === module) {
|
||||
runAllTests()
|
||||
.then(success => {
|
||||
process.exit(success ? 0 : 1);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('КРИТИЧЕСКАЯ ОШИБКА:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
testWebBot,
|
||||
testTelegramBot,
|
||||
testEmailBot,
|
||||
testBotManager,
|
||||
runAllTests
|
||||
};
|
||||
|
||||
@@ -22,18 +22,8 @@ async function getUserTokenBalances(address) {
|
||||
if (!address) return [];
|
||||
|
||||
// Получаем ключ шифрования
|
||||
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();
|
||||
}
|
||||
} catch (keyError) {
|
||||
console.error('Error reading encryption key:', keyError);
|
||||
}
|
||||
const encryptionUtils = require('../utils/encryptionUtils');
|
||||
const encryptionKey = encryptionUtils.getEncryptionKey();
|
||||
|
||||
// Получаем токены и RPC с расшифровкой
|
||||
const tokensResult = await db.getQuery()(
|
||||
|
||||
293
backend/services/unifiedMessageProcessor.js
Normal file
293
backend/services/unifiedMessageProcessor.js
Normal file
@@ -0,0 +1,293 @@
|
||||
/**
|
||||
* Copyright (c) 2024-2025 Тарабанов Александр Викторович
|
||||
* All rights reserved.
|
||||
*
|
||||
* This software is proprietary and confidential.
|
||||
* Unauthorized copying, modification, or distribution is prohibited.
|
||||
*
|
||||
* For licensing inquiries: info@hb3-accelerator.com
|
||||
* Website: https://hb3-accelerator.com
|
||||
* GitHub: https://github.com/HB3-ACCELERATOR
|
||||
*/
|
||||
|
||||
const db = require('../db');
|
||||
const logger = require('../utils/logger');
|
||||
const encryptionUtils = require('../utils/encryptionUtils');
|
||||
const aiAssistant = require('./ai-assistant');
|
||||
const conversationService = require('./conversationService');
|
||||
const { broadcastMessagesUpdate } = require('../wsHub');
|
||||
|
||||
/**
|
||||
* Унифицированный процессор сообщений для всех каналов
|
||||
* Обрабатывает сообщения из web, telegram, email
|
||||
*/
|
||||
|
||||
/**
|
||||
* Обработать сообщение от пользователя
|
||||
* @param {Object} messageData - Данные сообщения
|
||||
* @param {number} messageData.userId - ID пользователя
|
||||
* @param {string} messageData.content - Текст сообщения
|
||||
* @param {string} messageData.channel - Канал (web/telegram/email)
|
||||
* @param {Array} messageData.attachments - Вложения
|
||||
* @param {number} messageData.conversationId - ID беседы (опционально)
|
||||
* @returns {Promise<Object>}
|
||||
*/
|
||||
async function processMessage(messageData) {
|
||||
try {
|
||||
const {
|
||||
userId,
|
||||
content,
|
||||
channel = 'web',
|
||||
attachments = [],
|
||||
conversationId: inputConversationId,
|
||||
guestId
|
||||
} = messageData;
|
||||
|
||||
logger.info('[UnifiedMessageProcessor] Обработка сообщения:', {
|
||||
userId,
|
||||
channel,
|
||||
contentLength: content?.length,
|
||||
hasAttachments: attachments.length > 0
|
||||
});
|
||||
|
||||
const encryptionKey = encryptionUtils.getEncryptionKey();
|
||||
|
||||
// 1. Получаем или создаем беседу
|
||||
let conversation;
|
||||
if (inputConversationId) {
|
||||
conversation = await conversationService.getConversationById(inputConversationId);
|
||||
}
|
||||
|
||||
if (!conversation && userId) {
|
||||
conversation = await conversationService.getOrCreateConversation(userId, 'Беседа');
|
||||
}
|
||||
|
||||
const conversationId = conversation?.id || null;
|
||||
|
||||
// 2. Сохраняем входящее сообщение пользователя
|
||||
let userMessage;
|
||||
|
||||
// Обработка вложений
|
||||
let attachment_filename = null;
|
||||
let attachment_mimetype = null;
|
||||
let attachment_size = null;
|
||||
let attachment_data = null;
|
||||
|
||||
if (attachments && attachments.length > 0) {
|
||||
const firstAttachment = attachments[0];
|
||||
attachment_filename = firstAttachment.filename;
|
||||
attachment_mimetype = firstAttachment.mimetype;
|
||||
attachment_size = firstAttachment.size;
|
||||
attachment_data = firstAttachment.data;
|
||||
}
|
||||
|
||||
if (userId) {
|
||||
const { rows } = await db.getQuery()(
|
||||
`INSERT INTO messages (
|
||||
user_id,
|
||||
conversation_id,
|
||||
sender_type_encrypted,
|
||||
content_encrypted,
|
||||
channel_encrypted,
|
||||
role_encrypted,
|
||||
direction_encrypted,
|
||||
attachment_filename_encrypted,
|
||||
attachment_mimetype_encrypted,
|
||||
attachment_size,
|
||||
attachment_data,
|
||||
created_at
|
||||
) VALUES (
|
||||
$1, $2,
|
||||
encrypt_text($3, $12),
|
||||
encrypt_text($4, $12),
|
||||
encrypt_text($5, $12),
|
||||
encrypt_text($6, $12),
|
||||
encrypt_text($7, $12),
|
||||
encrypt_text($8, $12),
|
||||
encrypt_text($9, $12),
|
||||
$10, $11,
|
||||
NOW()
|
||||
) RETURNING id`,
|
||||
[
|
||||
userId,
|
||||
conversationId,
|
||||
'user',
|
||||
content,
|
||||
channel,
|
||||
'user',
|
||||
'incoming',
|
||||
attachment_filename,
|
||||
attachment_mimetype,
|
||||
attachment_size,
|
||||
attachment_data,
|
||||
encryptionKey
|
||||
]
|
||||
);
|
||||
|
||||
userMessage = rows[0];
|
||||
logger.info('[UnifiedMessageProcessor] Сообщение пользователя сохранено:', userMessage.id);
|
||||
}
|
||||
|
||||
// 3. Получаем историю беседы для контекста
|
||||
let conversationHistory = [];
|
||||
if (conversationId && userId) {
|
||||
const { rows } = await db.getQuery()(
|
||||
`SELECT
|
||||
decrypt_text(role_encrypted, $2) as role,
|
||||
decrypt_text(content_encrypted, $2) as content,
|
||||
created_at
|
||||
FROM messages
|
||||
WHERE conversation_id = $1 AND user_id = $3
|
||||
ORDER BY created_at ASC
|
||||
LIMIT 20`,
|
||||
[conversationId, encryptionKey, userId]
|
||||
);
|
||||
|
||||
conversationHistory = rows.map(row => ({
|
||||
role: row.role,
|
||||
content: row.content
|
||||
}));
|
||||
}
|
||||
|
||||
// 4. Генерируем AI ответ
|
||||
logger.info('[UnifiedMessageProcessor] Генерация AI ответа...');
|
||||
|
||||
const aiResponse = await aiAssistant.generateResponse({
|
||||
channel,
|
||||
messageId: userMessage?.id || `guest_${Date.now()}`,
|
||||
userId: userId || guestId,
|
||||
userQuestion: content,
|
||||
conversationHistory,
|
||||
conversationId,
|
||||
metadata: {
|
||||
hasAttachments: attachments.length > 0,
|
||||
channel
|
||||
}
|
||||
});
|
||||
|
||||
if (!aiResponse || !aiResponse.success) {
|
||||
logger.warn('[UnifiedMessageProcessor] AI не вернул ответ или ошибка:', aiResponse?.reason);
|
||||
|
||||
// Возвращаем результат без AI ответа
|
||||
return {
|
||||
success: true,
|
||||
userMessageId: userMessage?.id,
|
||||
conversationId,
|
||||
noAiResponse: true,
|
||||
reason: aiResponse?.reason
|
||||
};
|
||||
}
|
||||
|
||||
// 5. Сохраняем ответ AI
|
||||
if (userId && aiResponse.response) {
|
||||
const { rows: aiMessageRows } = await db.getQuery()(
|
||||
`INSERT INTO messages (
|
||||
user_id,
|
||||
conversation_id,
|
||||
sender_type_encrypted,
|
||||
content_encrypted,
|
||||
channel_encrypted,
|
||||
role_encrypted,
|
||||
direction_encrypted,
|
||||
created_at
|
||||
) VALUES (
|
||||
$1, $2,
|
||||
encrypt_text($3, $8),
|
||||
encrypt_text($4, $8),
|
||||
encrypt_text($5, $8),
|
||||
encrypt_text($6, $8),
|
||||
encrypt_text($7, $8),
|
||||
NOW()
|
||||
) RETURNING id`,
|
||||
[
|
||||
userId,
|
||||
conversationId,
|
||||
'assistant',
|
||||
aiResponse.response,
|
||||
channel,
|
||||
'assistant',
|
||||
'outgoing',
|
||||
encryptionKey
|
||||
]
|
||||
);
|
||||
|
||||
logger.info('[UnifiedMessageProcessor] Ответ AI сохранен:', aiMessageRows[0].id);
|
||||
|
||||
// 6. Обновляем время беседы
|
||||
if (conversationId) {
|
||||
await conversationService.touchConversation(conversationId);
|
||||
}
|
||||
|
||||
// 7. Отправляем уведомление через WebSocket
|
||||
try {
|
||||
broadcastMessagesUpdate(userId);
|
||||
} catch (wsError) {
|
||||
logger.warn('[UnifiedMessageProcessor] Ошибка отправки WebSocket:', wsError.message);
|
||||
}
|
||||
}
|
||||
|
||||
// 8. Возвращаем результат
|
||||
return {
|
||||
success: true,
|
||||
userMessageId: userMessage?.id,
|
||||
conversationId,
|
||||
aiResponse: {
|
||||
response: aiResponse.response,
|
||||
ragData: aiResponse.ragData
|
||||
}
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
logger.error('[UnifiedMessageProcessor] Ошибка обработки сообщения:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Обработать сообщение от гостя
|
||||
* @param {Object} messageData - Данные сообщения
|
||||
* @returns {Promise<Object>}
|
||||
*/
|
||||
async function processGuestMessage(messageData) {
|
||||
try {
|
||||
const guestService = require('./guestService');
|
||||
|
||||
// Создаем guest ID если нет
|
||||
const guestId = messageData.guestId || guestService.createGuestId();
|
||||
|
||||
// Сохраняем гостевое сообщение
|
||||
await guestService.saveGuestMessage({
|
||||
guestId,
|
||||
content: messageData.content,
|
||||
channel: messageData.channel || 'web'
|
||||
});
|
||||
|
||||
// Генерируем AI ответ для гостя (без сохранения в messages)
|
||||
const aiResponse = await aiAssistant.generateResponse({
|
||||
channel: messageData.channel || 'web',
|
||||
messageId: `guest_${guestId}_${Date.now()}`,
|
||||
userId: guestId,
|
||||
userQuestion: messageData.content,
|
||||
conversationHistory: [],
|
||||
metadata: { isGuest: true }
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
guestId,
|
||||
aiResponse: aiResponse?.success ? {
|
||||
response: aiResponse.response
|
||||
} : null
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
logger.error('[UnifiedMessageProcessor] Ошибка обработки гостевого сообщения:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
processMessage,
|
||||
processGuestMessage
|
||||
};
|
||||
|
||||
@@ -33,6 +33,14 @@ async function deleteUserById(userId) {
|
||||
);
|
||||
console.log('[DELETE] Удалено messages:', resMessages.rows.length);
|
||||
|
||||
// 2.1. Удаляем хеши дедупликации
|
||||
console.log('[DELETE] Начинаем удаление message_deduplication для userId:', userId);
|
||||
const resDeduplication = await db.getQuery()(
|
||||
'DELETE FROM message_deduplication WHERE user_id = $1 RETURNING id',
|
||||
[userId]
|
||||
);
|
||||
console.log('[DELETE] Удалено deduplication hashes:', resDeduplication.rows.length);
|
||||
|
||||
// 3. Удаляем conversations
|
||||
console.log('[DELETE] Начинаем удаление conversations для userId:', userId);
|
||||
const resConversations = await db.getQuery()(
|
||||
|
||||
128
backend/services/webBot.js
Normal file
128
backend/services/webBot.js
Normal file
@@ -0,0 +1,128 @@
|
||||
/**
|
||||
* Copyright (c) 2024-2025 Тарабанов Александр Викторович
|
||||
* All rights reserved.
|
||||
*
|
||||
* This software is proprietary and confidential.
|
||||
* Unauthorized copying, modification, or distribution is prohibited.
|
||||
*
|
||||
* For licensing inquiries: info@hb3-accelerator.com
|
||||
* Website: https://hb3-accelerator.com
|
||||
* GitHub: https://github.com/HB3-ACCELERATOR
|
||||
*/
|
||||
|
||||
const logger = require('../utils/logger');
|
||||
const unifiedMessageProcessor = require('./unifiedMessageProcessor');
|
||||
|
||||
/**
|
||||
* WebBot - обработчик веб-чата
|
||||
* Простой бот для веб-интерфейса, всегда активен
|
||||
*/
|
||||
class WebBot {
|
||||
constructor() {
|
||||
this.name = 'WebBot';
|
||||
this.channel = 'web';
|
||||
this.isInitialized = false;
|
||||
this.status = 'inactive';
|
||||
}
|
||||
|
||||
/**
|
||||
* Инициализация Web Bot
|
||||
*/
|
||||
async initialize() {
|
||||
try {
|
||||
logger.info('[WebBot] 🚀 Инициализация Web Bot...');
|
||||
|
||||
// Web bot всегда готов к работе
|
||||
this.isInitialized = true;
|
||||
this.status = 'active';
|
||||
|
||||
logger.info('[WebBot] ✅ Web Bot успешно инициализирован');
|
||||
return { success: true };
|
||||
|
||||
} catch (error) {
|
||||
logger.error('[WebBot] ❌ Ошибка инициализации:', error);
|
||||
this.status = 'error';
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Обработка сообщения из веб-чата
|
||||
* @param {Object} messageData - Данные сообщения
|
||||
* @returns {Promise<Object>}
|
||||
*/
|
||||
async processMessage(messageData) {
|
||||
try {
|
||||
if (!this.isInitialized) {
|
||||
throw new Error('WebBot is not initialized');
|
||||
}
|
||||
|
||||
// Устанавливаем канал
|
||||
messageData.channel = 'web';
|
||||
|
||||
// Обрабатываем через unified processor
|
||||
return await unifiedMessageProcessor.processMessage(messageData);
|
||||
|
||||
} catch (error) {
|
||||
logger.error('[WebBot] Ошибка обработки сообщения:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Отправка сообщения в веб-чат
|
||||
* @param {number} userId - ID пользователя
|
||||
* @param {string} message - Текст сообщения
|
||||
* @returns {Promise<Object>}
|
||||
*/
|
||||
async sendMessage(userId, message) {
|
||||
try {
|
||||
logger.info('[WebBot] Отправка сообщения пользователю:', userId);
|
||||
|
||||
// Для веб-чата отправка происходит через WebSocket
|
||||
// Здесь просто возвращаем успех, реальная отправка через wsHub
|
||||
|
||||
return {
|
||||
success: true,
|
||||
userId,
|
||||
message,
|
||||
channel: 'web'
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
logger.error('[WebBot] Ошибка отправки сообщения:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Получить статус бота
|
||||
* @returns {Object}
|
||||
*/
|
||||
getStatus() {
|
||||
return {
|
||||
name: this.name,
|
||||
channel: this.channel,
|
||||
isInitialized: this.isInitialized,
|
||||
status: this.status
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Остановка бота
|
||||
*/
|
||||
async stop() {
|
||||
try {
|
||||
logger.info('[WebBot] Остановка Web Bot...');
|
||||
this.isInitialized = false;
|
||||
this.status = 'inactive';
|
||||
logger.info('[WebBot] ✅ Web Bot остановлен');
|
||||
} catch (error) {
|
||||
logger.error('[WebBot] Ошибка остановки:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = WebBot;
|
||||
|
||||
Reference in New Issue
Block a user