Files
DLE/backend/routes/chat.js
2025-10-30 22:41:04 +03:00

551 lines
22 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 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/VC-HB3-Accelerator
*/
const express = require('express');
const router = express.Router();
const multer = require('multer');
const aiAssistant = require('../services/ai-assistant');
const db = require('../db');
const encryptedDb = require('../services/encryptedDatabaseService');
const logger = require('../utils/logger');
const { requireAuth, requireAdmin } = require('../middleware/auth');
const { requirePermission } = require('../middleware/permissions');
const { PERMISSIONS } = require('../shared/permissions');
const aiAssistantSettingsService = require('../services/aiAssistantSettingsService');
const aiAssistantRulesService = require('../services/aiAssistantRulesService');
const botManager = require('../services/botManager');
const universalMediaProcessor = require('../services/UniversalMediaProcessor');
// Настройка multer для обработки файлов в памяти
const storage = multer.memoryStorage();
const upload = multer({ storage: storage });
// Функция processGuestMessages заменена на UniversalGuestService.migrateToUser()
// Обработчик для гостевых сообщений (НОВАЯ ВЕРСИЯ)
router.post('/guest-message', upload.array('attachments'), async (req, res) => {
try {
// Frontend отправляет FormData, поэтому читаем из req.body
const content = req.body.message;
const guestId = req.body.guestId;
const files = req.files || [];
logger.info('[Chat] Получен guest-message запрос:', {
content: content?.substring(0, 50),
guestId,
hasFiles: files.length > 0,
bodyKeys: Object.keys(req.body)
});
// Проверяем, что есть либо текст, либо файлы
if (!content && (!files || files.length === 0)) {
logger.warn('[Chat] Гостевое сообщение без content и файлов:', req.body);
return res.status(400).json({
success: false,
error: 'Текст сообщения или файлы обязательны'
});
}
// Проверяем готовность системы
if (!botManager.isReady()) {
return res.status(503).json({
success: false,
error: 'Система ботов не готова. Попробуйте позже.'
});
}
const universalGuestService = require('../services/UniversalGuestService');
const unifiedMessageProcessor = require('../services/unifiedMessageProcessor');
// Создаем или используем существующий гостевой ID
const webGuestId = guestId || universalGuestService.generateWebGuestId();
const identifier = universalGuestService.createIdentifier('web', webGuestId);
// Обработка вложений через медиа-процессор
let contentData = null;
if (files && files.length > 0) {
const mediaFiles = [];
for (const file of files) {
try {
const processedFile = await universalMediaProcessor.processFile(
file.buffer,
file.originalname,
{
webUpload: true,
originalSize: file.size,
mimeType: file.mimetype
}
);
mediaFiles.push(processedFile);
} catch (fileError) {
logger.error('[Chat] Ошибка обработки файла:', fileError);
// Fallback: сохраняем как есть
mediaFiles.push({
type: 'document',
content: `[Файл: ${file.originalname}]`,
processed: false,
error: fileError.message,
file: {
filename: file.originalname,
mimetype: file.mimetype,
size: file.size,
data: file.buffer
}
});
}
}
// Создаем contentData только если есть обработанные файлы
if (mediaFiles.length > 0) {
contentData = {
text: content,
files: mediaFiles.map(file => ({
data: file.file?.data || file.file?.buffer,
filename: file.file?.originalName || file.file?.filename,
metadata: {
type: file.type,
processed: file.processed,
webUpload: true,
mimeType: file.file?.mimetype,
originalSize: file.file?.size,
size: file.file?.size
}
}))
};
}
}
// Обратная совместимость - старый формат attachments
const attachments = (files || []).map(file => ({
filename: file.originalname,
mimetype: file.mimetype,
size: file.size,
data: file.buffer
}));
const messageData = {
identifier: identifier,
content: content,
channel: 'web',
attachments: attachments,
contentData: contentData
};
// Обработка через unified processor
const result = await unifiedMessageProcessor.processMessage(messageData);
logger.info('[Chat] Результат обработки:', {
success: result.success,
hasAiResponse: !!result.aiResponse,
aiResponseType: typeof result.aiResponse?.response
});
res.json({
success: true,
guestId: webGuestId,
aiResponse: result.aiResponse ? {
response: result.aiResponse.response
} : null
});
} catch (error) {
logger.error('[Chat] Ошибка обработки гостевого сообщения:', error);
res.status(500).json({
success: false,
error: 'Внутренняя ошибка сервера'
});
}
});
// Старая логика удалена - используется guestService.js);
// Обработчик для сообщений аутентифицированных пользователей (НОВАЯ ВЕРСИЯ)
router.post('/message', requireAuth, upload.array('attachments'), async (req, res) => {
try {
// Frontend отправляет FormData, поэтому читаем из req.body
const content = req.body.message;
const { conversationId, recipientId } = req.body;
const userId = req.session.userId;
const files = req.files || [];
if (!content) {
return res.status(400).json({
success: false,
error: 'Текст сообщения обязателен'
});
}
if (!userId) {
return res.status(401).json({
success: false,
error: 'Пользователь не авторизован'
});
}
// Проверяем готовность системы
if (!botManager.isReady()) {
return res.status(503).json({
success: false,
error: 'Система ботов не готова. Попробуйте позже.'
});
}
const encryptedDb = require('../services/encryptedDatabaseService');
const unifiedMessageProcessor = require('../services/unifiedMessageProcessor');
const identityService = require('../services/identity-service');
// Получаем информацию о пользователе
const users = await encryptedDb.getData('users', { id: userId }, 1);
// ✨ Используем централизованную проверку прав
const { canSendMessage } = require('/app/shared/permissions');
const sessionUserId = req.session.userId;
const targetUserId = userId;
const userRole = req.session.userAccessLevel?.level || 'user';
// Получаем роль получателя
const recipientUser = users[0];
const recipientRole = recipientUser.role || 'user';
const permissionCheck = canSendMessage(userRole, recipientRole, sessionUserId, targetUserId);
if (!permissionCheck.canSend) {
logger.warn(`[Chat] Пользователь ${sessionUserId} (${userRole}) пытался писать в беседу ${targetUserId} (${recipientRole}) без прав: ${permissionCheck.errorMessage}`);
return res.status(403).json({
success: false,
error: permissionCheck.errorMessage || 'Недостаточно прав для отправки сообщений'
});
}
if (!users || users.length === 0) {
return res.status(404).json({
success: false,
error: 'Пользователь не найден'
});
}
const user = users[0];
// Находим wallet идентификатор пользователя
const walletIdentity = await identityService.findIdentity(userId, 'wallet');
if (!walletIdentity) {
return res.status(403).json({
success: false,
error: 'Требуется подключение кошелька'
});
}
// Создаем identifier для пользователя
const identifier = `wallet:${walletIdentity.provider_id}`;
// Обработка вложений
const attachments = files.map(file => ({
filename: file.originalname,
mimetype: file.mimetype,
size: file.size,
data: file.buffer
}));
const messageData = {
identifier: identifier,
content: content,
channel: 'web',
attachments: attachments,
conversationId: conversationId || null,
recipientId: recipientId || null,
userId: userId
};
// Обработка через unified processor
const result = await unifiedMessageProcessor.processMessage(messageData);
res.json({
success: true,
userMessageId: result.userMessageId,
conversationId: result.conversationId,
aiResponse: result.aiResponse ? {
response: result.aiResponse.response
} : null,
noAiResponse: result.noAiResponse
});
} catch (error) {
logger.error('[Chat] Ошибка обработки сообщения:', error);
res.status(500).json({
success: false,
error: 'Внутренняя ошибка сервера'
});
}
});
// Старая логика полностью удалена - используется только BotManager
// Маршрут /message-queued удален - дублировал логику и не использовал централизованные сервисы
// Добавьте этот маршрут для проверки доступных моделей
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;
// Получаем ключ шифрования
const fs = require('fs');
const path = require('path');
// Получаем ключ шифрования через унифицированную утилиту
const encryptionUtils = require('../utils/encryptionUtils');
const encryptionKey = encryptionUtils.getEncryptionKey();
try {
// Если нужен только подсчет
if (countOnly) {
let countQuery = 'SELECT COUNT(*) FROM messages WHERE user_id = $1 AND (message_type = $2 OR message_type = $3)';
let countParams = [userId, 'user_chat', 'public'];
if (conversationId) {
countQuery += ' AND conversation_id = $4';
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 });
}
// Загружаем сообщения: ИИ сообщения + публичные сообщения от других пользователей
// Используем SQL запрос для правильной фильтрации
const encryptionUtils = require('../utils/encryptionUtils');
const encryptionKey = encryptionUtils.getEncryptionKey();
const result = await db.getQuery()(
`SELECT m.id, m.user_id, m.sender_id, m.conversation_id,
decrypt_text(m.sender_type_encrypted, $2) as sender_type,
decrypt_text(m.content_encrypted, $2) as content,
decrypt_text(m.channel_encrypted, $2) as channel,
decrypt_text(m.role_encrypted, $2) as role,
decrypt_text(m.direction_encrypted, $2) as direction,
m.message_type, m.created_at
FROM messages m
WHERE m.user_id = $1
AND (m.message_type = 'user_chat' OR m.message_type = 'public')
ORDER BY m.created_at DESC
LIMIT $3`,
[userId, encryptionKey, limit]
);
const messages = result.rows;
// Переворачиваем массив для правильного порядка
messages.reverse();
// Обрабатываем результаты для фронтенда
const formattedMessages = messages.map(msg => {
const formatted = {
id: msg.id,
conversation_id: msg.conversation_id,
user_id: msg.user_id,
content: msg.content, // content уже расшифрован encryptedDb
sender_type: msg.sender_type, // sender_type уже расшифрован encryptedDb
role: msg.role, // role уже расшифрован encryptedDb
channel: msg.channel, // channel уже расшифрован encryptedDb
created_at: msg.created_at,
attachments: null // Инициализируем
};
// Если есть данные файла, добавляем их в attachments
if (msg.attachment_data) {
formatted.attachments = [{
originalname: msg.attachment_filename, // attachment_filename уже расшифрован encryptedDb
mimetype: msg.attachment_mimetype, // attachment_mimetype уже расшифрован encryptedDb
size: msg.attachment_size,
// Кодируем Buffer в Base64 для передачи на фронтенд
data_base64: msg.attachment_data.toString('base64')
}];
}
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: formattedMessages.length, offset, limit, total: totalMessages });
res.json({
success: true,
messages: formattedMessages,
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 universalGuestService = require('../services/UniversalGuestService');
const identifier = `web:${guestId}`; // Старые гости всегда из web
const result = await universalGuestService.migrateToUser(identifier, userId);
if (result && result.success) {
return res.json({
success: true,
conversationId: result.conversationId,
migratedMessages: result.migratedCount
});
} else {
return res.json({ success: false, error: result.error || 'Migration failed' });
}
} catch (error) {
logger.error('Error in /migrate-guest-messages:', error);
return res.status(500).json({ success: false, error: 'Internal error' });
}
});
// POST /api/chat/ai-draft — генерация черновика ответа ИИ
// Генерация AI-черновика ответа (только для админов-редакторов)
router.post('/ai-draft', requireAuth, requirePermission(PERMISSIONS.GENERATE_AI_REPLIES), async (req, res) => {
const userId = req.session.userId;
const { conversationId, messages, language } = req.body;
// Получаем ключ шифрования
const fs = require('fs');
const path = require('path');
// Получаем ключ шифрования через унифицированную утилиту
const encryptionUtils = require('../utils/encryptionUtils');
const encryptionKey = encryptionUtils.getEncryptionKey();
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 decrypt_text(sender_type_encrypted, $2) as sender_type, decrypt_text(content_encrypted, $2) as content FROM messages WHERE conversation_id = $1 ORDER BY created_at DESC LIMIT 10',
[conversationId, encryptionKey]
);
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 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,
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: 'Ошибка генерации черновика' });
}
});
// Перезапуск конкретного бота (только для админов)
router.post('/restart-bot', requireAdmin, async (req, res) => {
try {
const { botName } = req.body;
if (!botName || !['web', 'telegram', 'email'].includes(botName)) {
return res.status(400).json({
success: false,
error: 'Некорректное имя бота. Допустимые значения: web, telegram, email'
});
}
logger.info(`[Chat] Запрос на перезапуск ${botName} бота`);
const result = await botManager.restartBot(botName);
if (result.success) {
res.json({
success: true,
message: `${botName} бот успешно перезапущен`
});
} else {
res.status(500).json({
success: false,
error: result.error
});
}
} catch (error) {
logger.error('[Chat] Ошибка перезапуска бота:', error);
res.status(500).json({
success: false,
error: 'Ошибка перезапуска бота'
});
}
});
// Экспортируем маршрутизатор
module.exports = router;