358 lines
12 KiB
JavaScript
358 lines
12 KiB
JavaScript
/**
|
||
* 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 adminLogicService = require('./adminLogicService');
|
||
const universalGuestService = require('./UniversalGuestService');
|
||
const identityService = require('./identity-service');
|
||
const { broadcastMessagesUpdate } = require('../wsHub');
|
||
|
||
/**
|
||
* Унифицированный процессор сообщений для всех каналов
|
||
* Обрабатывает сообщения из 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<Object>}
|
||
*/
|
||
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 isGuestIdentifier = await checkIfGuest(identifier);
|
||
|
||
if (isGuestIdentifier) {
|
||
// ГОСТЬ: обработка через UniversalGuestService
|
||
logger.info('[UnifiedMessageProcessor] Обработка гостевого сообщения');
|
||
return await universalGuestService.processMessage({
|
||
identifier,
|
||
content,
|
||
channel,
|
||
metadata,
|
||
...messageData
|
||
});
|
||
}
|
||
|
||
// 2. ПОЛЬЗОВАТЕЛЬ: ищем user_id
|
||
const [provider, providerId] = identifier.split(':');
|
||
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
|
||
});
|
||
|
||
// 3. Проверяем: админ или обычный пользователь?
|
||
const isAdmin = userRole === 'editor' || userRole === 'readonly';
|
||
|
||
// 4. Определяем нужно ли генерировать AI ответ
|
||
const shouldGenerateAi = adminLogicService.shouldGenerateAiReply({
|
||
senderType: isAdmin ? 'admin' : 'user',
|
||
userId: userId,
|
||
recipientId: recipientId || userId,
|
||
channel: channel
|
||
});
|
||
|
||
logger.info('[UnifiedMessageProcessor] Генерация AI:', { shouldGenerateAi, isAdmin });
|
||
|
||
// 5. Получаем или создаем беседу
|
||
let conversation;
|
||
if (inputConversationId) {
|
||
conversation = await conversationService.getConversationById(inputConversationId);
|
||
}
|
||
|
||
if (!conversation) {
|
||
conversation = await conversationService.getOrCreateConversation(userId, 'Беседа');
|
||
}
|
||
|
||
const conversationId = conversation.id;
|
||
|
||
// 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. Сохраняем входящее сообщение пользователя
|
||
const encryptionKey = encryptionUtils.getEncryptionKey();
|
||
|
||
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,
|
||
message_type,
|
||
created_at
|
||
) VALUES (
|
||
$1, $2,
|
||
encrypt_text($3, $13),
|
||
encrypt_text($4, $13),
|
||
encrypt_text($5, $13),
|
||
encrypt_text($6, $13),
|
||
encrypt_text($7, $13),
|
||
encrypt_text($8, $13),
|
||
encrypt_text($9, $13),
|
||
$10, $11, $12,
|
||
NOW()
|
||
) RETURNING id`,
|
||
[
|
||
userId,
|
||
conversationId,
|
||
isAdmin ? 'admin' : 'user',
|
||
content,
|
||
channel,
|
||
'user',
|
||
'incoming',
|
||
attachment_filename,
|
||
attachment_mimetype,
|
||
attachment_size,
|
||
attachment_data,
|
||
'user_chat', // message_type
|
||
encryptionKey
|
||
]
|
||
);
|
||
|
||
const userMessageId = rows[0].id;
|
||
logger.info('[UnifiedMessageProcessor] Сообщение пользователя сохранено:', userMessageId);
|
||
|
||
// 8. Генерируем AI ответ (если нужно)
|
||
let aiResponse = null;
|
||
|
||
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) {
|
||
// Сохраняем ответ AI
|
||
const { rows: aiMessageRows } = await db.getQuery()(
|
||
`INSERT INTO messages (
|
||
user_id,
|
||
conversation_id,
|
||
sender_type_encrypted,
|
||
content_encrypted,
|
||
channel_encrypted,
|
||
role_encrypted,
|
||
direction_encrypted,
|
||
message_type,
|
||
created_at
|
||
) VALUES (
|
||
$1, $2,
|
||
encrypt_text($3, $9),
|
||
encrypt_text($4, $9),
|
||
encrypt_text($5, $9),
|
||
encrypt_text($6, $9),
|
||
encrypt_text($7, $9),
|
||
$8,
|
||
NOW()
|
||
) RETURNING id`,
|
||
[
|
||
userId,
|
||
conversationId,
|
||
'assistant',
|
||
aiResponse.response,
|
||
channel,
|
||
'assistant',
|
||
'outgoing',
|
||
'user_chat',
|
||
encryptionKey
|
||
]
|
||
);
|
||
|
||
logger.info('[UnifiedMessageProcessor] Ответ AI сохранен:', aiMessageRows[0].id);
|
||
} 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. Возвращаем результат
|
||
return {
|
||
success: true,
|
||
userMessageId,
|
||
conversationId,
|
||
aiResponse: aiResponse && aiResponse.success ? {
|
||
response: aiResponse.response,
|
||
ragData: aiResponse.ragData
|
||
} : null,
|
||
noAiResponse: !shouldGenerateAi
|
||
};
|
||
|
||
} catch (error) {
|
||
logger.error('[UnifiedMessageProcessor] Ошибка обработки сообщения:', error);
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Проверить, является ли идентификатор гостевым
|
||
* @param {string} identifier
|
||
* @returns {Promise<boolean>}
|
||
*/
|
||
async function checkIfGuest(identifier) {
|
||
try {
|
||
if (!identifier || typeof identifier !== 'string') {
|
||
return true; // По умолчанию гость
|
||
}
|
||
|
||
// Разбираем идентификатор
|
||
const [provider, providerId] = identifier.split(':');
|
||
|
||
// Проверяем что это не web:guest_*
|
||
if (provider === 'web' && providerId.startsWith('guest_')) {
|
||
return true; // Это web гость
|
||
}
|
||
|
||
// Проверяем есть ли пользователь с wallet
|
||
const user = await identityService.findUserByIdentity(provider, providerId);
|
||
|
||
if (!user) {
|
||
return true; // Пользователь не найден - это гость
|
||
}
|
||
|
||
// Проверяем есть ли у пользователя wallet
|
||
const walletIdentity = await identityService.findIdentity(user.id, 'wallet');
|
||
|
||
if (!walletIdentity) {
|
||
// Нет кошелька - это временный пользователь, считаем гостем
|
||
return true;
|
||
}
|
||
|
||
// Есть кошелек - полноценный пользователь
|
||
return false;
|
||
|
||
} catch (error) {
|
||
logger.error('[UnifiedMessageProcessor] Ошибка проверки гостя:', error);
|
||
return true; // В случае ошибки считаем гостем для безопасности
|
||
}
|
||
}
|
||
|
||
/**
|
||
* DEPRECATED: Используйте processMessage()
|
||
* Обработать сообщение от гостя
|
||
* @param {Object} messageData - Данные сообщения
|
||
* @returns {Promise<Object>}
|
||
*/
|
||
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
|
||
};
|