/** * Copyright (c) 2024-2026 Тарабанов Александр Викторович * 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/VC-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 adminLogicService = require('./adminLogicService'); const universalGuestService = require('./UniversalGuestService'); const identityService = require('./identity-service'); /** * Определить тип сообщения по контексту * @param {number|null} recipientId - ID получателя * @param {number} userId - ID отправителя * @param {boolean} isAdminSender - Является ли отправитель админом * @returns {string} - Тип сообщения: 'user_chat', 'admin_chat', 'public' */ function determineMessageType(recipientId, userId, isAdminSender) { // 1. Личный чат с ИИ (recipientId не указан или равен userId) if (!recipientId || recipientId === userId) { return 'user_chat'; } // 2. Приватное сообщение к редактору (recipientId = 1) if (recipientId === 1) { return 'admin_chat'; } // 3. Публичное сообщение между пользователями return 'public'; } /** * Определить тип беседы * @param {string} messageType - Тип сообщения * @param {number|null} recipientId - ID получателя * @param {number} userId - ID отправителя * @returns {string} - Тип беседы: 'user_chat', 'private', 'public' */ function determineConversationType(messageType, recipientId, userId) { switch (messageType) { case 'user_chat': return 'user_chat'; // Личная беседа с ИИ case 'admin_chat': return 'private'; // Приватная беседа с редактором case 'public': return 'public_chat'; // Публичная беседа между пользователями default: return 'user_chat'; } } /** * Определить, нужно ли генерировать AI ответ * @param {string} messageType - Тип сообщения * @param {number|null} recipientId - ID получателя * @param {number} userId - ID отправителя * @returns {boolean} */ function shouldGenerateAiReply(messageType, recipientId, userId) { // ИИ отвечает только в личных чатах return messageType === 'user_chat'; } const { broadcastMessagesUpdate } = require('../wsHub'); // НОВАЯ СИСТЕМА РОЛЕЙ: используем shared/permissions.js const { hasPermission, ROLES, PERMISSIONS } = require('/app/shared/permissions'); /** * Унифицированный процессор сообщений для всех каналов * Обрабатывает сообщения из web, telegram, email * НОВАЯ ВЕРСИЯ с поддержкой универсальной гостевой системы */ /** * Обработать сообщение (гость или пользователь) * @param {Object} messageData - Данные сообщения * @param {string} messageData.identifier - Универсальный идентификатор * @param {string} messageData.content - Текст сообщения * @param {string} messageData.channel - Канал (web/telegram/email) * @param {Array} messageData.attachments - Вложения * @param {number} messageData.conversationId - ID беседы (опционально) * @param {number} messageData.recipientId - ID получателя (для админов) * @returns {Promise} */ async function processMessage(messageData) { try { const { identifier, content, channel = 'web', attachments = [], conversationId: inputConversationId, recipientId, metadata = {} } = messageData; logger.info('[UnifiedMessageProcessor] Обработка сообщения:', { identifier, channel, contentLength: content?.length, hasAttachments: attachments.length > 0 }); // 1. Разбираем идентификатор const [provider, providerId] = identifier.split(':'); // Проверяем формат identifier (должен быть provider:providerId) if (!providerId) { logger.warn(`[UnifiedMessageProcessor] Неверный формат identifier (нет префикса): ${identifier}, обрабатываем как гостя`); return await universalGuestService.processMessage({ identifier, content, channel, metadata, ...messageData }); } // 2. Для telegram/email: автоматически создаем пользователя, если его нет if ((provider === 'telegram' || provider === 'email') && providerId) { let user = await identityService.findUserByIdentity(provider, providerId); if (!user) { // Автоматически создаем пользователя для telegram/email при первом сообщении logger.info(`[UnifiedMessageProcessor] Автоматическое создание пользователя для ${provider}:${providerId}`); const { ROLES } = require('/app/shared/permissions'); const db = require('../db'); // Создаем нового пользователя с ролью user const newUserResult = await db.getQuery()('INSERT INTO users (role) VALUES ($1) RETURNING id', [ ROLES.USER, ]); const userId = newUserResult.rows[0].id; // Добавляем идентификатор await identityService.saveIdentity(userId, provider, providerId, true); logger.info(`[UnifiedMessageProcessor] Создан пользователь ${userId} для ${provider}:${providerId} с ролью ${ROLES.USER}`); // Обновляем список контактов const { broadcastContactsUpdate } = require('../wsHub'); broadcastContactsUpdate(); } } // 3. Определяем: гость или пользователь? const isGuestIdentifier = await checkIfGuest(identifier); if (isGuestIdentifier) { // ГОСТЬ: обработка через UniversalGuestService logger.info('[UnifiedMessageProcessor] Обработка гостевого сообщения'); return await universalGuestService.processMessage({ identifier, content, channel, metadata, ...messageData }); } // 4. ПОЛЬЗОВАТЕЛЬ: ищем user_id (теперь он точно должен быть, так как для telegram/email мы его создали) const user = await identityService.findUserByIdentity(provider, providerId); if (!user) { throw new Error(`User not found for identifier: ${identifier}`); } const userId = user.id; const userRole = user.role || 'user'; logger.info('[UnifiedMessageProcessor] Обработка сообщения пользователя:', { userId, role: userRole }); // НОВАЯ СИСТЕМА РОЛЕЙ: определяем права через новую систему const isAdmin = userRole === ROLES.EDITOR || userRole === ROLES.READONLY; // 4. Определяем тип сообщения по контексту const messageType = determineMessageType(recipientId, userId, isAdmin); // 5. Определяем нужно ли генерировать AI ответ let shouldGenerateAi = shouldGenerateAiReply(messageType, recipientId, userId); logger.info('[UnifiedMessageProcessor] Генерация AI:', { shouldGenerateAi, userRole, isAdmin }); // 6. Получаем или создаем беседу с правильным типом let conversation; const conversationType = determineConversationType(messageType, recipientId, userId); if (inputConversationId) { conversation = await conversationService.getConversationById(inputConversationId); } if (!conversation) { // Для публичных сообщений создаем беседу между пользователями if (messageType === 'public') { conversation = await conversationService.getOrCreatePublicConversation(userId, recipientId); } else { // Для личных и админских чатов используем стандартную логику conversation = await conversationService.getOrCreateConversation(userId, 'Беседа'); } // Обновляем тип беседы в БД, если он не соответствует if (conversation.conversation_type !== conversationType) { await db.getQuery()( 'UPDATE conversations SET conversation_type = $1 WHERE id = $2', [conversationType, conversation.id] ); conversation.conversation_type = conversationType; } } const conversationId = conversation.id; // Получаем ключ шифрования (будет использоваться далее) const encryptionKey = encryptionUtils.getEncryptionKey(); // 5.5. Проверяем, нужно ли автоматически подписать согласие при ответе // Ищем последнее сообщение от ассистента или системное сообщение с согласием const consentService = require('./consentService'); const { rows: lastMessages } = await db.getQuery()( `SELECT decrypt_text(role_encrypted, $2) as role, decrypt_text(content_encrypted, $2) as content, message_type FROM messages WHERE conversation_id = $1 AND user_id = $3 AND ( decrypt_text(role_encrypted, $2) = 'assistant' OR message_type = 'system_consent' ) ORDER BY created_at DESC LIMIT 1`, [conversationId, encryptionKey, userId] ); // Если последнее сообщение было от ассистента, проверяем наличие системного сообщения о согласиях if (lastMessages.length > 0) { // Проверяем согласия пользователя const walletIdentity = await identityService.findIdentity(userId, 'wallet'); const consentCheck = await consentService.checkConsents({ userId, walletAddress: walletIdentity?.provider_id || null }); // Если согласия нужны, но пользователь отвечает на сообщение, автоматически подписываем if (consentCheck.needsConsent) { logger.info(`[UnifiedMessageProcessor] Автоматическое подписание согласий при ответе пользователя ${userId}`); // Получаем документы для подписания const consentDocuments = await consentService.getConsentDocuments(consentCheck.missingConsents); const documentIds = consentDocuments.map(doc => doc.id); const consentTypes = consentDocuments.map(doc => doc.consentType).filter(type => type); // Автоматически подписываем согласие if (documentIds.length > 0 && consentTypes.length > 0) { try { // Используем проверку существования вместо ON CONFLICT (т.к. может не быть уникального ограничения) for (let i = 0; i < documentIds.length; i++) { const docId = documentIds[i]; const docTitle = consentDocuments.find(d => d.id === docId)?.title || ''; const consentType = consentTypes[i]; // Проверяем, есть ли уже согласие const existing = await db.getQuery()( `SELECT id FROM consent_logs WHERE user_id = $1 AND consent_type = $2 AND document_id = $3 AND status = 'granted'`, [userId, consentType, docId] ); if (existing.rows.length > 0) { // Обновляем существующее await db.getQuery()( `UPDATE consent_logs SET signed_at = NOW(), revoked_at = NULL, updated_at = NOW() WHERE id = $1`, [existing.rows[0].id] ); } else { // Создаем новое await db.getQuery()( `INSERT INTO consent_logs (user_id, wallet_address, document_id, document_title, consent_type, status, signed_at, channel, created_at, updated_at) VALUES ($1, $2, $3, $4, $5, 'granted', NOW(), 'web', NOW(), NOW())`, [userId, walletIdentity?.provider_id || null, docId, docTitle, consentType] ); } } logger.info(`[UnifiedMessageProcessor] Согласия автоматически подписаны для пользователя ${userId}`); } catch (consentError) { logger.error(`[UnifiedMessageProcessor] Ошибка автоматического подписания согласий:`, consentError); // Не блокируем обработку сообщения при ошибке подписания } } } } // 6. Обработка вложений 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; } // 7. Сохраняем входящее сообщение пользователя // encryptionKey уже объявлен выше const { rows } = await db.getQuery()( `INSERT INTO messages ( conversation_id, sender_id, sender_type_encrypted, content_encrypted, channel_encrypted, role_encrypted, direction_encrypted, attachment_filename, attachment_mimetype, attachment_size, attachment_data, message_type, user_id, role, direction, created_at ) VALUES ( $1, $2, encrypt_text($3, $16), encrypt_text($4, $16), encrypt_text($5, $16), encrypt_text($6, $16), encrypt_text($7, $16), $8, $9, $10, $11, $12, $13, $14, $15, NOW() ) RETURNING id`, [ conversationId, userId, // sender_id isAdmin ? 'editor' : 'user', content, channel, 'user', 'incoming', attachment_filename, attachment_mimetype, attachment_size, attachment_data, messageType, // message_type recipientId || userId, // user_id (получатель для публичных сообщений) 'user', // role (незашифрованное) 'incoming', // direction (незашифрованное) encryptionKey ] ); const userMessageId = rows[0].id; logger.info('[UnifiedMessageProcessor] Сообщение пользователя сохранено:', userMessageId); // 8. Генерируем AI ответ (если нужно) let aiResponse = null; // Инициализируем finalAiResponse для использования в результатах (должен быть доступен везде) let finalAiResponse = null; let aiResponseDisabled = false; if (shouldGenerateAi) { // Загружаем историю беседы const { rows: historyRows } = 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] ); const conversationHistory = historyRows.map(row => ({ role: row.role, content: row.content })); logger.info('[UnifiedMessageProcessor] Генерация AI ответа...'); aiResponse = await aiAssistant.generateResponse({ channel, messageId: userMessageId, userId: userId, userQuestion: content, conversationHistory, conversationId, metadata: { hasAttachments: attachments.length > 0, channel, isAdmin } }); if (aiResponse && aiResponse.success && aiResponse.response) { // Формируем финальный ответ ИИ finalAiResponse = aiResponse.response; // Сохраняем ответ AI const { rows: aiMessageRows } = await db.getQuery()( `INSERT INTO messages ( conversation_id, sender_id, sender_type_encrypted, content_encrypted, channel_encrypted, role_encrypted, direction_encrypted, message_type, user_id, role, direction, 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), $8, $9, $10, $11, NOW() ) RETURNING id`, [ conversationId, userId, // sender_id 'assistant', finalAiResponse, channel, 'assistant', 'outgoing', messageType, userId, // user_id 'assistant', // role (незашифрованное) 'outgoing', // direction (незашифрованное) encryptionKey ] ); logger.info('[UnifiedMessageProcessor] Ответ AI сохранен:', aiMessageRows[0].id); } else if (aiResponse && aiResponse.disabled) { aiResponseDisabled = true; logger.info('[UnifiedMessageProcessor] AI ассистент отключен для текущего канала — ответ не генерируется.'); } else { logger.warn('[UnifiedMessageProcessor] AI не вернул ответ:', aiResponse?.reason); } } else { logger.info('[UnifiedMessageProcessor] AI ответ не требуется (админ → пользователь)'); } // 9. Обновляем время беседы await conversationService.touchConversation(conversationId); // 10. Отправляем уведомление через WebSocket try { broadcastMessagesUpdate(userId); } catch (wsError) { logger.warn('[UnifiedMessageProcessor] Ошибка отправки WebSocket:', wsError.message); } // 11. Возвращаем результат const result = { success: true, userMessageId, conversationId, aiResponse: aiResponse && aiResponse.success ? { response: finalAiResponse || (aiResponse?.response || ''), ragData: aiResponse.ragData } : null, noAiResponse: !shouldGenerateAi || aiResponseDisabled, assistantDisabled: aiResponseDisabled }; return result; } catch (error) { logger.error('[UnifiedMessageProcessor] Ошибка обработки сообщения:', error); throw error; } } /** * Проверить, является ли идентификатор гостевым * @param {string} identifier * @returns {Promise} */ async function checkIfGuest(identifier) { try { if (!identifier || typeof identifier !== 'string') { return true; // По умолчанию гость } // Разбираем идентификатор const [provider, providerId] = identifier.split(':'); // Если нет providerId (старый формат без префикса) - считаем гостем if (!providerId) { return true; } // Проверяем что это не web:guest_* if (provider === 'web' && providerId.startsWith('guest_')) { return true; // Это web гость } // Проверяем есть ли пользователь с этим идентификатором (telegram/email/wallet) const user = await identityService.findUserByIdentity(provider, providerId); if (!user) { // Пользователь не найден - это гость // Исключение: для telegram/email если пользователь не найден, это может быть гость // Но если он авторизован, он должен быть создан через verifyTelegramAuth/checkEmailVerification return true; } // Если пользователь найден в БД - это пользователь (независимо от наличия кошелька) // Telegram/Email пользователи получают роль user даже без кошелька // Только web-гости без авторизации остаются гостами return false; } catch (error) { logger.error('[UnifiedMessageProcessor] Ошибка проверки гостя:', error); return true; // В случае ошибки считаем гостем для безопасности } } /** * DEPRECATED: Используйте processMessage() * Обработать сообщение от гостя * @param {Object} messageData - Данные сообщения * @returns {Promise} */ async function processGuestMessage(messageData) { logger.warn('[UnifiedMessageProcessor] processGuestMessage() устарел, используйте processMessage()'); // Для обратной совместимости const { guestId, content, channel } = messageData; const identifier = universalGuestService.createIdentifier(channel || 'web', guestId); return processMessage({ identifier, content, channel: channel || 'web', ...messageData }); } module.exports = { processMessage, processGuestMessage, // deprecated checkIfGuest };