769 lines
35 KiB
JavaScript
769 lines
35 KiB
JavaScript
const express = require('express');
|
||
const router = express.Router();
|
||
const multer = require('multer');
|
||
const aiAssistant = require('../services/ai-assistant');
|
||
const db = require('../db');
|
||
const logger = require('../utils/logger');
|
||
const { requireAuth } = require('../middleware/auth');
|
||
const crypto = require('crypto');
|
||
const aiAssistantSettingsService = require('../services/aiAssistantSettingsService');
|
||
const aiAssistantRulesService = require('../services/aiAssistantRulesService');
|
||
|
||
// Настройка multer для обработки файлов в памяти
|
||
const storage = multer.memoryStorage();
|
||
const upload = multer({ storage: storage });
|
||
|
||
// Функция для обработки гостевых сообщений после аутентификации
|
||
async function processGuestMessages(userId, guestId) {
|
||
try {
|
||
logger.info(`Processing guest messages for user ${userId} with guest ID ${guestId}`);
|
||
|
||
// Проверяем, обрабатывались ли уже эти сообщения
|
||
const mappingCheck = await db.getQuery()(
|
||
'SELECT processed FROM guest_user_mapping WHERE guest_id = $1',
|
||
[guestId]
|
||
);
|
||
|
||
// Если сообщения уже обработаны, пропускаем
|
||
if (mappingCheck.rows.length > 0 && mappingCheck.rows[0].processed) {
|
||
logger.info(`Guest messages for guest ID ${guestId} were already processed.`);
|
||
return { success: true, message: 'Guest messages already processed' };
|
||
}
|
||
|
||
// Проверяем наличие mapping записи и создаем если нет
|
||
if (mappingCheck.rows.length === 0) {
|
||
await db.getQuery()(
|
||
'INSERT INTO guest_user_mapping (user_id, guest_id) VALUES ($1, $2) ON CONFLICT (guest_id) DO UPDATE SET user_id = $1',
|
||
[userId, guestId]
|
||
);
|
||
logger.info(`Created mapping for guest ID ${guestId} to user ${userId}`);
|
||
}
|
||
|
||
// Получаем все гостевые сообщения со всеми новыми полями
|
||
const guestMessagesResult = await db.getQuery()(
|
||
`SELECT
|
||
id, guest_id, content, language, is_ai, created_at,
|
||
attachment_filename, attachment_mimetype, attachment_size, attachment_data
|
||
FROM guest_messages WHERE guest_id = $1 ORDER BY created_at ASC`,
|
||
[guestId]
|
||
);
|
||
|
||
if (guestMessagesResult.rows.length === 0) {
|
||
logger.info(`No guest messages found for guest ID ${guestId}`);
|
||
const checkResult = await db.getQuery()('SELECT 1 FROM guest_user_mapping WHERE guest_id = $1', [guestId]);
|
||
if (checkResult.rows.length > 0) {
|
||
await db.getQuery()('UPDATE guest_user_mapping SET processed = true WHERE guest_id = $1', [guestId]);
|
||
logger.info(`Marked guest mapping as processed (no messages found) for guest ID ${guestId}`);
|
||
} else {
|
||
logger.warn(`Attempted to mark non-existent guest mapping as processed for guest ID ${guestId}`);
|
||
}
|
||
return { success: true, message: 'No guest messages found' };
|
||
}
|
||
|
||
const guestMessages = guestMessagesResult.rows;
|
||
logger.info(`Found ${guestMessages.length} guest messages for guest ID ${guestId}`);
|
||
|
||
// --- Новый порядок: ищем последний диалог пользователя ---
|
||
let conversation = null;
|
||
const lastConvResult = await db.getQuery()(
|
||
'SELECT * FROM conversations WHERE user_id = $1 ORDER BY updated_at DESC, created_at DESC LIMIT 1',
|
||
[userId]
|
||
);
|
||
if (lastConvResult.rows.length > 0) {
|
||
conversation = lastConvResult.rows[0];
|
||
} else {
|
||
// Если нет ни одного диалога, создаём новый
|
||
const firstMessage = guestMessages[0];
|
||
const title = firstMessage.content
|
||
? (firstMessage.content.length > 30 ? `${firstMessage.content.substring(0, 30)}...` : firstMessage.content)
|
||
: (firstMessage.attachment_filename ? `Файл: ${firstMessage.attachment_filename}` : 'Новый диалог');
|
||
const newConversationResult = await db.getQuery()(
|
||
'INSERT INTO conversations (user_id, title) VALUES ($1, $2) RETURNING *',
|
||
[userId, title]
|
||
);
|
||
conversation = newConversationResult.rows[0];
|
||
logger.info(`Created new conversation ${conversation.id} for guest messages`);
|
||
}
|
||
// --- КОНЕЦ блока поиска/создания диалога ---
|
||
|
||
// Отслеживаем успешные сохранения сообщений
|
||
const savedMessageIds = [];
|
||
|
||
// Обрабатываем каждое гостевое сообщение
|
||
for (const guestMessage of guestMessages) {
|
||
logger.info(`Processing guest message ID ${guestMessage.id}: ${guestMessage.content || guestMessage.attachment_filename || '(empty)'}`);
|
||
try {
|
||
// Сохраняем сообщение пользователя в таблицу messages, включая данные файла
|
||
const userMessageResult = await db.getQuery()(
|
||
`INSERT INTO messages
|
||
(conversation_id, content, sender_type, role, channel, created_at, user_id,
|
||
attachment_filename, attachment_mimetype, attachment_size, attachment_data)
|
||
VALUES
|
||
($1, $2, 'user', 'user', 'web', $3, $4,
|
||
$5, $6, $7, $8)
|
||
RETURNING *`,
|
||
[
|
||
conversation.id,
|
||
guestMessage.content, // Текст (может быть NULL)
|
||
guestMessage.created_at,
|
||
userId,
|
||
guestMessage.attachment_filename, // Метаданные и данные файла
|
||
guestMessage.attachment_mimetype,
|
||
guestMessage.attachment_size,
|
||
guestMessage.attachment_data // BYTEA
|
||
]
|
||
);
|
||
const savedUserMessage = userMessageResult.rows[0];
|
||
logger.info(`Saved user message with ID ${savedUserMessage.id}`);
|
||
savedMessageIds.push(guestMessage.id);
|
||
// --- Генерируем ответ ИИ на гостевое сообщение, если это текст ---
|
||
if (guestMessage.content) {
|
||
// Проверяем, что на это сообщение ещё нет ответа ассистента
|
||
const aiReplyExists = await db.getQuery()(
|
||
`SELECT 1 FROM messages WHERE conversation_id = $1 AND sender_type = 'assistant' AND created_at > $2 LIMIT 1`,
|
||
[conversation.id, guestMessage.created_at]
|
||
);
|
||
if (!aiReplyExists.rows.length) {
|
||
try {
|
||
// Получаем настройки ассистента
|
||
const aiSettings = await aiAssistantSettingsService.getSettings();
|
||
let rules = null;
|
||
if (aiSettings && aiSettings.rules_id) {
|
||
rules = await aiAssistantRulesService.getRuleById(aiSettings.rules_id);
|
||
}
|
||
// Получаем историю сообщений до этого guestMessage (до created_at)
|
||
const historyResult = await db.getQuery()(
|
||
'SELECT sender_type, content FROM messages WHERE conversation_id = $1 AND created_at < $2 ORDER BY created_at DESC LIMIT 10',
|
||
[conversation.id, guestMessage.created_at]
|
||
);
|
||
const history = historyResult.rows.reverse().map(msg => ({
|
||
role: msg.sender_type === 'user' ? 'user' : 'assistant',
|
||
content: msg.content
|
||
}));
|
||
// Язык guestMessage.language или auto
|
||
const detectedLanguage = guestMessage.language === 'auto' ? aiAssistant.detectLanguage(guestMessage.content) : guestMessage.language;
|
||
logger.info('Getting AI response for guest message:', guestMessage.content);
|
||
const aiResponseContent = await aiAssistant.getResponse(
|
||
guestMessage.content,
|
||
detectedLanguage,
|
||
history,
|
||
aiSettings ? aiSettings.system_prompt : '',
|
||
rules ? rules.rules : null
|
||
);
|
||
logger.info('AI response for guest message received' + (aiResponseContent ? '' : ' (empty)'), { conversationId: conversation.id });
|
||
if (aiResponseContent) {
|
||
await db.getQuery()(
|
||
`INSERT INTO messages
|
||
(conversation_id, user_id, content, sender_type, role, channel)
|
||
VALUES ($1, $2, $3, 'assistant', 'assistant', 'web')`,
|
||
[conversation.id, userId, aiResponseContent]
|
||
);
|
||
logger.info('AI response for guest message saved', { conversationId: conversation.id });
|
||
}
|
||
} catch (aiError) {
|
||
logger.error('Error getting or saving AI response for guest message:', aiError);
|
||
}
|
||
}
|
||
}
|
||
// --- конец блока генерации ответа ИИ ---
|
||
} catch (error) {
|
||
logger.error(`Error processing guest message ${guestMessage.id}: ${error.message}`, { stack: error.stack });
|
||
// Продолжаем с другими сообщениями в случае ошибки
|
||
}
|
||
}
|
||
|
||
// Удаляем только успешно обработанные гостевые сообщения
|
||
if (savedMessageIds.length > 0) {
|
||
await db.getQuery()('DELETE FROM guest_messages WHERE id = ANY($1::int[])', [savedMessageIds]);
|
||
logger.info(
|
||
`Deleted ${savedMessageIds.length} processed guest messages for guest ID ${guestId}`
|
||
);
|
||
|
||
// Помечаем гостевой ID как обработанный
|
||
await db.getQuery()('UPDATE guest_user_mapping SET processed = true WHERE guest_id = $1', [
|
||
guestId,
|
||
]);
|
||
logger.info(`Marked guest mapping as processed for guest ID ${guestId}`);
|
||
} else {
|
||
logger.warn(`No guest messages were successfully processed, skipping deletion for guest ID ${guestId}`);
|
||
// Если не было успешных, все равно пометим как обработанные, чтобы не пытаться снова
|
||
await db.getQuery()('UPDATE guest_user_mapping SET processed = true WHERE guest_id = $1', [guestId]);
|
||
logger.info(`Marked guest mapping as processed (no successful messages) for guest ID ${guestId}`);
|
||
}
|
||
|
||
return {
|
||
success: true,
|
||
message: `Processed ${savedMessageIds.length} of ${guestMessages.length} guest messages`,
|
||
conversationId: conversation.id,
|
||
};
|
||
} catch (error) {
|
||
logger.error(`Error in processGuestMessages for guest ID ${guestId}: ${error.message}`, { stack: error.stack });
|
||
// Не пробрасываем ошибку дальше, чтобы не прерывать основной поток, но логируем ее
|
||
return { success: false, error: 'Internal error during guest message processing' };
|
||
}
|
||
}
|
||
|
||
// Обработчик для гостевых сообщений
|
||
router.post('/guest-message', upload.array('attachments'), async (req, res) => {
|
||
// Логируем полученные данные
|
||
logger.info('Received /guest-message request');
|
||
logger.debug('Request Body:', req.body);
|
||
logger.debug('Request Files:', req.files); // Файлы будут здесь
|
||
|
||
try {
|
||
// Извлекаем данные из req.body (текстовые поля)
|
||
const { message, language, guestId: requestGuestId } = req.body;
|
||
const files = req.files; // Файлы извлекаем из req.files
|
||
const file = files && files.length > 0 ? files[0] : null; // Берем только первый файл
|
||
|
||
// Валидация: должно быть либо сообщение, либо файл
|
||
if (!message && !file) {
|
||
logger.warn('Guest message attempt without content or file.', { guestId: requestGuestId });
|
||
return res.status(400).json({ success: false, error: 'Требуется текст сообщения или файл.' });
|
||
}
|
||
// Запрещаем и текст, и файл одновременно (согласно новым требованиям)
|
||
if (message && file) {
|
||
logger.warn('Guest message attempt with both text and file.', { guestId: requestGuestId });
|
||
return res.status(400).json({ success: false, error: 'Нельзя отправить текст и файл одновременно.' });
|
||
}
|
||
|
||
// Используем гостевой ID из запроса или из сессии, или генерируем новый
|
||
const guestId = requestGuestId || req.session.guestId || crypto.randomBytes(16).toString('hex');
|
||
|
||
// Сохраняем/обновляем ID гостя в сессии
|
||
if (req.session.guestId !== guestId) {
|
||
req.session.guestId = guestId;
|
||
}
|
||
|
||
// Подготавливаем данные для вставки
|
||
const messageContent = message || ''; // Текст или ПУСТАЯ СТРОКА, если есть файл
|
||
const attachmentFilename = file ? file.originalname : null;
|
||
const attachmentMimetype = file ? file.mimetype : null;
|
||
const attachmentSize = file ? file.size : null;
|
||
const attachmentData = file ? file.buffer : null; // Сам буфер файла
|
||
|
||
logger.info('Saving guest message:', {
|
||
guestId,
|
||
message: messageContent,
|
||
file: attachmentFilename,
|
||
mimetype: attachmentMimetype,
|
||
size: attachmentSize
|
||
});
|
||
|
||
// Сохраняем сообщение пользователя с текстом или файлом
|
||
const result = await db.getQuery()(
|
||
`INSERT INTO guest_messages
|
||
(guest_id, content, language, is_ai,
|
||
attachment_filename, attachment_mimetype, attachment_size, attachment_data)
|
||
VALUES ($1, $2, $3, false, $4, $5, $6, $7) RETURNING id`,
|
||
[
|
||
guestId,
|
||
messageContent, // Текст сообщения или NULL
|
||
language || 'auto',
|
||
attachmentFilename,
|
||
attachmentMimetype,
|
||
attachmentSize,
|
||
attachmentData // BYTEA данные файла или NULL
|
||
]
|
||
);
|
||
|
||
const savedMessageId = result.rows[0].id;
|
||
logger.info('Guest message saved with ID:', savedMessageId);
|
||
|
||
// Сохраняем сессию после успешной операции с БД
|
||
try {
|
||
await new Promise((resolve, reject) => {
|
||
req.session.save((err) => {
|
||
if (err) return reject(err);
|
||
resolve();
|
||
});
|
||
});
|
||
logger.info('Session saved after guest message');
|
||
} catch (sessionError) {
|
||
logger.error('Error saving session after guest message:', sessionError);
|
||
// Не прерываем ответ пользователю из-за ошибки сессии
|
||
}
|
||
|
||
// Получаем настройки ассистента для systemMessage
|
||
let telegramBotUrl = null;
|
||
let supportEmailAddr = null;
|
||
try {
|
||
const aiSettings = await aiAssistantSettingsService.getSettings();
|
||
if (aiSettings && aiSettings.telegramBot && aiSettings.telegramBot.bot_username) {
|
||
telegramBotUrl = `https://t.me/${aiSettings.telegramBot.bot_username}`;
|
||
}
|
||
if (aiSettings && aiSettings.supportEmail && aiSettings.supportEmail.from_email) {
|
||
supportEmailAddr = aiSettings.supportEmail.from_email;
|
||
}
|
||
} catch (e) {
|
||
logger.error('Ошибка получения настроек ассистента для systemMessage:', e);
|
||
}
|
||
|
||
res.json({
|
||
success: true,
|
||
messageId: savedMessageId, // Возвращаем ID сохраненного сообщения
|
||
guestId: guestId, // Возвращаем использованный guestId
|
||
systemMessage: 'Для продолжения диалога авторизуйтесь: подключите кошелек, перейдите в чат-бот Telegram или отправьте письмо на email.',
|
||
telegramBotUrl,
|
||
supportEmail: supportEmailAddr
|
||
});
|
||
} catch (error) {
|
||
logger.error('Error saving guest message:', error);
|
||
res.status(500).json({ success: false, error: 'Ошибка сохранения гостевого сообщения' });
|
||
}
|
||
});
|
||
|
||
// Обработчик для сообщений аутентифицированных пользователей
|
||
router.post('/message', requireAuth, upload.array('attachments'), async (req, res) => {
|
||
logger.info('Received /message request');
|
||
logger.debug('Request Body:', req.body);
|
||
logger.debug('Request Files:', req.files);
|
||
|
||
const userId = req.session.userId;
|
||
const { message, language, conversationId: convIdFromRequest } = req.body;
|
||
const files = req.files;
|
||
const file = files && files.length > 0 ? files[0] : null;
|
||
|
||
// Валидация: должно быть либо сообщение, либо файл
|
||
if (!message && !file) {
|
||
logger.warn('Authenticated message attempt without content or file.', { userId });
|
||
return res.status(400).json({ success: false, error: 'Требуется текст сообщения или файл.' });
|
||
}
|
||
// Запрещаем и текст, и файл одновременно
|
||
if (message && file) {
|
||
logger.warn('Authenticated message attempt with both text and file.', { userId });
|
||
return res.status(400).json({ success: false, error: 'Нельзя отправить текст и файл одновременно.' });
|
||
}
|
||
|
||
let conversationId = convIdFromRequest;
|
||
let conversation = null;
|
||
|
||
try {
|
||
// Найти или создать диалог
|
||
if (conversationId) {
|
||
let convResult;
|
||
if (req.session.isAdmin) {
|
||
// Админ может писать в любой диалог
|
||
convResult = await db.getQuery()(
|
||
'SELECT * FROM conversations WHERE id = $1',
|
||
[conversationId]
|
||
);
|
||
} else {
|
||
// Обычный пользователь — только в свой диалог
|
||
convResult = await db.getQuery()(
|
||
'SELECT * FROM conversations WHERE id = $1 AND user_id = $2',
|
||
[conversationId, userId]
|
||
);
|
||
}
|
||
if (convResult.rows.length === 0) {
|
||
logger.warn('Conversation not found or access denied', { conversationId, userId });
|
||
return res.status(404).json({ success: false, error: 'Диалог не найден или доступ запрещен' });
|
||
}
|
||
conversation = convResult.rows[0];
|
||
} else {
|
||
// Ищем последний диалог пользователя
|
||
const lastConvResult = await db.getQuery()(
|
||
'SELECT * FROM conversations WHERE user_id = $1 ORDER BY updated_at DESC, created_at DESC LIMIT 1',
|
||
[userId]
|
||
);
|
||
if (lastConvResult.rows.length > 0) {
|
||
conversation = lastConvResult.rows[0];
|
||
conversationId = conversation.id;
|
||
} else {
|
||
// Создаем новый диалог, если нет ни одного
|
||
const title = message
|
||
? (message.length > 50 ? `${message.substring(0, 50)}...` : message)
|
||
: (file ? `Файл: ${file.originalname}` : 'Новый диалог');
|
||
const newConvResult = await db.getQuery()(
|
||
'INSERT INTO conversations (user_id, title) VALUES ($1, $2) RETURNING *',
|
||
[userId, title]
|
||
);
|
||
conversation = newConvResult.rows[0];
|
||
conversationId = conversation.id;
|
||
logger.info('Created new conversation', { conversationId, userId });
|
||
}
|
||
}
|
||
|
||
// Подготавливаем данные для вставки сообщения пользователя
|
||
const messageContent = message || ''; // Текст или ПУСТАЯ СТРОКА, если есть файл
|
||
const attachmentFilename = file ? file.originalname : null;
|
||
const attachmentMimetype = file ? file.mimetype : null;
|
||
const attachmentSize = file ? file.size : null;
|
||
const attachmentData = file ? file.buffer : null;
|
||
|
||
// Определяем user_id для сообщения: всегда user_id диалога (контакта)
|
||
const recipientId = conversation.user_id;
|
||
// Определяем sender_type
|
||
let senderType = 'user';
|
||
let role = 'user';
|
||
if (req.session.isAdmin) {
|
||
senderType = 'admin';
|
||
role = 'admin';
|
||
}
|
||
|
||
// Сохраняем сообщение
|
||
const userMessageResult = await db.getQuery()(
|
||
`INSERT INTO messages
|
||
(conversation_id, user_id, content, sender_type, role, channel,
|
||
attachment_filename, attachment_mimetype, attachment_size, attachment_data)
|
||
VALUES ($1, $2, $3, $4, $5, 'web', $6, $7, $8, $9)
|
||
RETURNING *`,
|
||
[
|
||
conversationId,
|
||
recipientId, // user_id контакта
|
||
messageContent,
|
||
senderType,
|
||
role,
|
||
attachmentFilename,
|
||
attachmentMimetype,
|
||
attachmentSize,
|
||
attachmentData
|
||
]
|
||
);
|
||
const userMessage = userMessageResult.rows[0];
|
||
logger.info('User message saved', { messageId: userMessage.id, conversationId });
|
||
|
||
// --- Новая логика автоответа ИИ по RAG ---
|
||
let aiMessage = null;
|
||
let shouldGenerateAiReply = true;
|
||
if (senderType === 'admin') {
|
||
// Если админ пишет не себе, не отвечаем
|
||
if (userId !== recipientId) {
|
||
shouldGenerateAiReply = false;
|
||
}
|
||
}
|
||
if (messageContent && shouldGenerateAiReply) { // Только для текстовых сообщений и если разрешено
|
||
try {
|
||
// Получаем настройки ассистента
|
||
const aiSettings = await aiAssistantSettingsService.getSettings();
|
||
let rules = null;
|
||
if (aiSettings && aiSettings.rules_id) {
|
||
rules = await aiAssistantRulesService.getRuleById(aiSettings.rules_id);
|
||
}
|
||
// --- RAG автоответ ---
|
||
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;
|
||
}
|
||
let ragResult = null;
|
||
if (ragTableId) {
|
||
const { ragAnswer, generateLLMResponse } = require('../services/ragService');
|
||
const threshold = 0.3;
|
||
logger.info(`[RAG] Запуск поиска по RAG: tableId=${ragTableId}, вопрос="${messageContent}", threshold=${threshold}`);
|
||
const ragResult = await ragAnswer({ tableId: ragTableId, userQuestion: messageContent, threshold });
|
||
logger.info(`[RAG] Результат поиска по RAG:`, ragResult);
|
||
if (ragResult && ragResult.answer && ragResult.score && ragResult.score > threshold) {
|
||
logger.info(`[RAG] Найден confident-ответ (score=${ragResult.score}), отправляем ответ из базы.`);
|
||
// Прямой ответ из RAG
|
||
const aiMessageResult = await db.getQuery()(
|
||
`INSERT INTO messages
|
||
(conversation_id, user_id, content, sender_type, role, channel)
|
||
VALUES ($1, $2, $3, 'assistant', 'assistant', 'web')
|
||
RETURNING *`,
|
||
[conversationId, userId, ragResult.answer]
|
||
);
|
||
aiMessage = aiMessageResult.rows[0];
|
||
} else if (ragResult) {
|
||
logger.info(`[RAG] Нет confident-ответа (score=${ragResult.score}), переходим к генерации через LLM.`);
|
||
// Генерация через LLM с подстановкой значений из RAG
|
||
const historyResult = await db.getQuery()(
|
||
'SELECT sender_type, content FROM messages WHERE conversation_id = $1 AND id < $2 ORDER BY created_at DESC LIMIT 10',
|
||
[conversationId, userMessage.id]
|
||
);
|
||
const history = historyResult.rows.reverse().map(msg => ({
|
||
role: msg.sender_type === 'user' ? 'user' : 'assistant',
|
||
content: msg.content
|
||
}));
|
||
const llmResponse = await generateLLMResponse({
|
||
userQuestion: messageContent,
|
||
context: ragResult.context,
|
||
answer: ragResult.answer,
|
||
clarifyingAnswer: ragResult.clarifyingAnswer,
|
||
objectionAnswer: ragResult.objectionAnswer,
|
||
systemPrompt: aiSettings ? aiSettings.system_prompt : '',
|
||
history,
|
||
model: aiSettings ? aiSettings.model : undefined,
|
||
language: aiSettings && aiSettings.languages && aiSettings.languages.length > 0 ? aiSettings.languages[0] : 'ru'
|
||
});
|
||
if (llmResponse) {
|
||
const aiMessageResult = await db.getQuery()(
|
||
`INSERT INTO messages
|
||
(conversation_id, user_id, content, sender_type, role, channel)
|
||
VALUES ($1, $2, $3, 'assistant', 'assistant', 'web')
|
||
RETURNING *`,
|
||
[conversationId, userId, llmResponse]
|
||
);
|
||
aiMessage = aiMessageResult.rows[0];
|
||
} else {
|
||
logger.info(`[RAG] Нет ни одного результата, прошедшего порог (${threshold}).`);
|
||
}
|
||
}
|
||
}
|
||
// --- конец RAG автоответа ---
|
||
} catch (aiError) {
|
||
logger.error('Error getting or saving AI response (RAG):', aiError);
|
||
// Не прерываем основной ответ, но логируем ошибку
|
||
}
|
||
}
|
||
|
||
// Форматируем ответ для фронтенда
|
||
const formatMessageForFrontend = (msg) => {
|
||
if (!msg) return null;
|
||
const formatted = {
|
||
id: msg.id,
|
||
conversation_id: msg.conversation_id,
|
||
user_id: msg.user_id,
|
||
content: msg.content,
|
||
sender_type: msg.sender_type,
|
||
role: msg.role,
|
||
channel: msg.channel,
|
||
created_at: msg.created_at,
|
||
attachments: null // Инициализируем как null
|
||
};
|
||
// Добавляем информацию о файле, если она есть
|
||
if (msg.attachment_filename) {
|
||
formatted.attachments = [{
|
||
originalname: msg.attachment_filename,
|
||
mimetype: msg.attachment_mimetype,
|
||
size: msg.attachment_size,
|
||
// НЕ передаем attachment_data обратно в ответе на POST
|
||
}];
|
||
}
|
||
return formatted;
|
||
};
|
||
|
||
// Обновляем updated_at у диалога
|
||
await db.getQuery()(
|
||
'UPDATE conversations SET updated_at = NOW() WHERE id = $1',
|
||
[conversationId]
|
||
);
|
||
|
||
res.json({
|
||
success: true,
|
||
conversationId: conversationId,
|
||
userMessage: formatMessageForFrontend(userMessage),
|
||
aiMessage: formatMessageForFrontend(aiMessage),
|
||
});
|
||
} catch (error) {
|
||
logger.error('Error processing authenticated message:', error);
|
||
res.status(500).json({ success: false, error: 'Ошибка обработки сообщения' });
|
||
}
|
||
});
|
||
|
||
// Добавьте этот маршрут для проверки доступных моделей
|
||
router.get('/models', async (req, res) => {
|
||
try {
|
||
const models = await aiAssistant.getAvailableModels();
|
||
|
||
res.json({
|
||
success: true,
|
||
models: models,
|
||
});
|
||
} catch (error) {
|
||
console.error('Ошибка при получении списка моделей:', error);
|
||
res.status(500).json({ success: false, message: 'Ошибка сервера' });
|
||
}
|
||
});
|
||
|
||
// Получение истории сообщений
|
||
router.get('/history', requireAuth, async (req, res) => {
|
||
const userId = req.session.userId;
|
||
// Параметры пагинации
|
||
const limit = parseInt(req.query.limit, 10) || 30;
|
||
const offset = parseInt(req.query.offset, 10) || 0;
|
||
// Флаг для запроса только количества
|
||
const countOnly = req.query.count_only === 'true';
|
||
// Опциональный ID диалога
|
||
const conversationId = req.query.conversation_id;
|
||
|
||
try {
|
||
// Если нужен только подсчет
|
||
if (countOnly) {
|
||
let countQuery = 'SELECT COUNT(*) FROM messages WHERE user_id = $1';
|
||
let countParams = [userId];
|
||
if (conversationId) {
|
||
countQuery += ' AND conversation_id = $2';
|
||
countParams.push(conversationId);
|
||
}
|
||
const countResult = await db.getQuery()(countQuery, countParams);
|
||
const totalCount = parseInt(countResult.rows[0].count, 10);
|
||
return res.json({ success: true, count: totalCount });
|
||
}
|
||
|
||
// Формируем основной запрос
|
||
let query = `
|
||
SELECT
|
||
id,
|
||
conversation_id,
|
||
user_id,
|
||
content,
|
||
sender_type,
|
||
role,
|
||
channel,
|
||
created_at,
|
||
attachment_filename,
|
||
attachment_mimetype,
|
||
attachment_size,
|
||
attachment_data -- Выбираем и данные файла
|
||
FROM messages
|
||
WHERE user_id = $1
|
||
`;
|
||
const params = [userId];
|
||
|
||
// Добавляем фильтр по диалогу, если нужно
|
||
if (conversationId) {
|
||
query += ' AND conversation_id = $2';
|
||
params.push(conversationId);
|
||
}
|
||
|
||
// Добавляем сортировку и пагинацию
|
||
query += ' ORDER BY created_at ASC LIMIT $' + (params.length + 1) + ' OFFSET $' + (params.length + 2);
|
||
params.push(limit);
|
||
params.push(offset);
|
||
|
||
logger.debug('Executing history query:', { query, params });
|
||
|
||
const result = await db.getQuery()(query, params);
|
||
|
||
// Обрабатываем результаты для фронтенда
|
||
const messages = result.rows.map(msg => {
|
||
const formatted = {
|
||
id: msg.id,
|
||
conversation_id: msg.conversation_id,
|
||
user_id: msg.user_id,
|
||
content: msg.content,
|
||
sender_type: msg.sender_type,
|
||
role: msg.role,
|
||
channel: msg.channel,
|
||
created_at: msg.created_at,
|
||
attachments: null // Инициализируем
|
||
};
|
||
|
||
// Если есть данные файла, добавляем их в attachments
|
||
if (msg.attachment_data) {
|
||
formatted.attachments = [{
|
||
originalname: msg.attachment_filename,
|
||
mimetype: msg.attachment_mimetype,
|
||
size: msg.attachment_size,
|
||
// Кодируем Buffer в Base64 для передачи на фронтенд
|
||
data_base64: msg.attachment_data.toString('base64')
|
||
}];
|
||
}
|
||
// Не забываем удалить поле attachment_data из итогового объекта,
|
||
// так как оно уже обработано и не нужно в сыром виде на фронте
|
||
// (хотя map и так создает новый объект, это для ясности)
|
||
delete formatted.attachment_data;
|
||
|
||
return formatted;
|
||
});
|
||
|
||
// Получаем общее количество сообщений для пагинации (если не запрашивали только количество)
|
||
let totalCountQuery = 'SELECT COUNT(*) FROM messages WHERE user_id = $1';
|
||
let totalCountParams = [userId];
|
||
if (conversationId) {
|
||
totalCountQuery += ' AND conversation_id = $2';
|
||
totalCountParams.push(conversationId);
|
||
}
|
||
const totalCountResult = await db.getQuery()(totalCountQuery, totalCountParams);
|
||
const totalMessages = parseInt(totalCountResult.rows[0].count, 10);
|
||
|
||
logger.info(`Returning message history for user ${userId}`, { count: messages.length, offset, limit, total: totalMessages });
|
||
|
||
res.json({
|
||
success: true,
|
||
messages: messages,
|
||
offset: offset,
|
||
limit: limit,
|
||
total: totalMessages
|
||
});
|
||
|
||
} catch (error) {
|
||
logger.error(`Error fetching message history for user ${userId}: ${error.message}`, { stack: error.stack });
|
||
res.status(500).json({ success: false, error: 'Ошибка получения истории сообщений' });
|
||
}
|
||
});
|
||
|
||
// --- Новый роут для связывания гостя после аутентификации ---
|
||
router.post('/process-guest', requireAuth, async (req, res) => {
|
||
const userId = req.session.userId;
|
||
const { guestId } = req.body;
|
||
if (!guestId) {
|
||
return res.status(400).json({ success: false, error: 'guestId is required' });
|
||
}
|
||
try {
|
||
const result = await module.exports.processGuestMessages(userId, guestId);
|
||
if (result && result.conversationId) {
|
||
return res.json({ success: true, conversationId: result.conversationId });
|
||
} else {
|
||
return res.json({ success: false, error: result.error || 'No conversation created' });
|
||
}
|
||
} catch (error) {
|
||
logger.error('Error in /process-guest:', error);
|
||
return res.status(500).json({ success: false, error: 'Internal error' });
|
||
}
|
||
});
|
||
|
||
// POST /api/chat/ai-draft — генерация черновика ответа ИИ
|
||
router.post('/ai-draft', requireAuth, async (req, res) => {
|
||
const userId = req.session.userId;
|
||
const { conversationId, messages, language } = req.body;
|
||
if (!conversationId || !Array.isArray(messages) || messages.length === 0) {
|
||
return res.status(400).json({ success: false, error: 'conversationId и messages обязательны' });
|
||
}
|
||
try {
|
||
// Получаем настройки ассистента
|
||
const aiSettings = await aiAssistantSettingsService.getSettings();
|
||
let rules = null;
|
||
if (aiSettings && aiSettings.rules_id) {
|
||
rules = await aiAssistantRulesService.getRuleById(aiSettings.rules_id);
|
||
}
|
||
// Формируем prompt из выбранных сообщений
|
||
const promptText = messages.map(m => m.content).join('\n\n');
|
||
// Получаем последние 10 сообщений из диалога для истории
|
||
const historyResult = await db.getQuery()(
|
||
'SELECT sender_type, content FROM messages WHERE conversation_id = $1 ORDER BY created_at DESC LIMIT 10',
|
||
[conversationId]
|
||
);
|
||
const history = historyResult.rows.reverse().map(msg => ({
|
||
role: msg.sender_type === 'user' ? 'user' : 'assistant',
|
||
content: msg.content
|
||
}));
|
||
// --- RAG draft ---
|
||
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;
|
||
}
|
||
let ragResult = null;
|
||
if (ragTableId) {
|
||
const { ragAnswer } = require('../services/ragService');
|
||
logger.info(`[RAG] [DRAFT] Запуск поиска по RAG: tableId=${ragTableId}, draft prompt="${promptText}"`);
|
||
ragResult = await ragAnswer({ tableId: ragTableId, userQuestion: promptText });
|
||
logger.info(`[RAG] [DRAFT] Результат поиска по RAG:`, ragResult);
|
||
}
|
||
const { generateLLMResponse } = require('../services/ragService');
|
||
const detectedLanguage = language === 'auto' ? aiAssistant.detectLanguage(promptText) : language;
|
||
const aiResponseContent = await generateLLMResponse({
|
||
userQuestion: promptText,
|
||
context: ragResult && ragResult.context ? ragResult.context : '',
|
||
answer: ragResult && ragResult.answer ? ragResult.answer : '',
|
||
systemPrompt: aiSettings ? aiSettings.system_prompt : '',
|
||
history,
|
||
model: aiSettings ? aiSettings.model : undefined,
|
||
language: aiSettings && aiSettings.languages && aiSettings.languages.length > 0 ? aiSettings.languages[0] : 'ru',
|
||
rules: rules ? rules.rules : null
|
||
});
|
||
res.json({ success: true, aiMessage: aiResponseContent });
|
||
} catch (error) {
|
||
logger.error('Error generating AI draft:', error);
|
||
res.status(500).json({ success: false, error: 'Ошибка генерации черновика' });
|
||
}
|
||
});
|
||
|
||
// Экспортируем маршрутизатор и функцию processGuestMessages отдельно
|
||
module.exports = router;
|
||
module.exports.processGuestMessages = processGuestMessages;
|