feat: новая функция

This commit is contained in:
2025-10-08 18:01:14 +03:00
parent 2c53bce32a
commit 725e7fd5a2
60 changed files with 5427 additions and 3921 deletions

View File

@@ -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()(

View 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
};

View File

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

View File

@@ -87,6 +87,7 @@ class AICache {
calculateHitRate() {
// Простая реализация - в реальности нужно отслеживать hits/misses
if (this.maxSize === 0) return 0;
return this.cache.size / this.maxSize;
}
}

View File

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

View File

@@ -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

View File

@@ -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()(

View 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;

View 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
};

View 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
};

View File

@@ -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

View File

@@ -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';
}
}
/**
* Получить данные из таблицы с автоматической расшифровкой

View 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
};

View 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
};

View File

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

View 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
};

View 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
};

View 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
};

View File

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

View File

@@ -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

View File

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

View 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
};

View File

@@ -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()(

View 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
};

View File

@@ -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
View 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;