ваше сообщение коммита
This commit is contained in:
50
backend/db/migrations/017_add_file_storage_columns.sql
Normal file
50
backend/db/migrations/017_add_file_storage_columns.sql
Normal file
@@ -0,0 +1,50 @@
|
||||
-- Миграция для добавления колонок хранения файлов и удаления старой колонки attachments
|
||||
|
||||
-- UP Migration
|
||||
BEGIN;
|
||||
|
||||
-- Добавляем колонки для хранения файла и его метаданных в таблицу messages
|
||||
ALTER TABLE messages
|
||||
ADD COLUMN attachment_filename TEXT NULL,
|
||||
ADD COLUMN attachment_mimetype TEXT NULL,
|
||||
ADD COLUMN attachment_size BIGINT NULL,
|
||||
ADD COLUMN attachment_data BYTEA NULL;
|
||||
|
||||
-- Добавляем колонки для хранения файла и его метаданных в таблицу guest_messages
|
||||
ALTER TABLE guest_messages
|
||||
ADD COLUMN attachment_filename TEXT NULL,
|
||||
ADD COLUMN attachment_mimetype TEXT NULL,
|
||||
ADD COLUMN attachment_size BIGINT NULL,
|
||||
ADD COLUMN attachment_data BYTEA NULL;
|
||||
|
||||
-- Удаляем старую колонку attachments из таблицы messages, если она существует
|
||||
ALTER TABLE messages DROP COLUMN IF EXISTS attachments;
|
||||
|
||||
-- Удаляем старую колонку attachments из таблицы guest_messages, если она существует
|
||||
ALTER TABLE guest_messages DROP COLUMN IF EXISTS attachments;
|
||||
|
||||
COMMIT;
|
||||
|
||||
-- DOWN Migration
|
||||
-- Откат изменений: удаляем новые колонки и пытаемся вернуть старую колонку (тип TEXT или JSONB? Используем TEXT как предположение)
|
||||
BEGIN;
|
||||
|
||||
ALTER TABLE messages
|
||||
DROP COLUMN IF EXISTS attachment_filename,
|
||||
DROP COLUMN IF EXISTS attachment_mimetype,
|
||||
DROP COLUMN IF EXISTS attachment_size,
|
||||
DROP COLUMN IF EXISTS attachment_data;
|
||||
-- Пытаемся вернуть старую колонку (данные будут потеряны при откате)
|
||||
-- Возможно, потребуется указать правильный тип (TEXT или JSONB), который был раньше
|
||||
ALTER TABLE messages ADD COLUMN attachments TEXT NULL;
|
||||
|
||||
ALTER TABLE guest_messages
|
||||
DROP COLUMN IF EXISTS attachment_filename,
|
||||
DROP COLUMN IF EXISTS attachment_mimetype,
|
||||
DROP COLUMN IF EXISTS attachment_size,
|
||||
DROP COLUMN IF EXISTS attachment_data;
|
||||
-- Пытаемся вернуть старую колонку (данные будут потеряны при откате)
|
||||
-- Возможно, потребуется указать правильный тип (TEXT или JSONB), который был раньше
|
||||
ALTER TABLE guest_messages ADD COLUMN attachments TEXT NULL;
|
||||
|
||||
COMMIT;
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1 +1,7 @@
|
||||
{"code":"42703","file":"parse_relation.c","hint":"Perhaps you meant to reference the column \"user_identities.created_at\".","length":183,"level":"error","line":"3729","message":"[IdentityService] Error finding wallet identity for user 1: column \"updated_at\" does not exist","name":"error","position":"43","routine":"errorMissingColumn","severity":"ERROR","stack":"error: column \"updated_at\" does not exist\n at /app/node_modules/pg-pool/index.js:45:11\n at process.processTicksAndRejections (node:internal/process/task_queues:95:5)\n at async IdentityService.findIdentity (/app/services/identity-service.js:258:22)\n at async /app/routes/auth.js:368:32","timestamp":"2025-04-22T14:39:48.332Z"}
|
||||
{"body":{"guestId":"1745825541972-nwmi3pcpe","language":"ru","message":""},"level":"error","message":"Error in /guest-message: null value in column \"content\" of relation \"guest_messages\" violates not-null constraint","stack":"error: null value in column \"content\" of relation \"guest_messages\" violates not-null constraint\n at /app/node_modules/pg-pool/index.js:45:11\n at process.processTicksAndRejections (node:internal/process/task_queues:95:5)\n at async /app/routes/chat.js:211:20","timestamp":"2025-04-28T07:37:57.316Z"}
|
||||
{"body":{"guestId":"1745826229509-a9cc49arg","language":"ru","message":""},"level":"error","message":"Error in /guest-message: null value in column \"content\" of relation \"guest_messages\" violates not-null constraint","stack":"error: null value in column \"content\" of relation \"guest_messages\" violates not-null constraint\n at /app/node_modules/pg-pool/index.js:45:11\n at process.processTicksAndRejections (node:internal/process/task_queues:95:5)\n at async /app/routes/chat.js:211:20","timestamp":"2025-04-28T07:43:49.319Z"}
|
||||
{"body":{"guestId":"1745827727896-sqo5zp8c5","language":"ru","message":""},"level":"error","message":"Error in /guest-message: invalid input syntax for type json","stack":"error: invalid input syntax for type json\n at /app/node_modules/pg-pool/index.js:45:11\n at process.processTicksAndRejections (node:internal/process/task_queues:95:5)\n at async /app/routes/chat.js:227:20","timestamp":"2025-04-28T08:08:48.293Z"}
|
||||
{"code":"42703","file":"parse_target.c","length":146,"level":"error","line":"1066","message":"Error saving guest message: column \"attachment_filename\" of relation \"guest_messages\" does not exist","name":"error","position":"82","routine":"checkInsertTargets","severity":"ERROR","stack":"error: column \"attachment_filename\" of relation \"guest_messages\" does not exist\n at /app/node_modules/pg-pool/index.js:45:11\n at process.processTicksAndRejections (node:internal/process/task_queues:95:5)\n at async /app/routes/chat.js:224:20","timestamp":"2025-04-28T14:28:29.099Z"}
|
||||
{"code":"23502","column":"content","detail":"Failing row contains (2, 1745850508983-zb9xi0g4t, null, ru, f, 2025-04-28 14:53:07.021316+00, image (15).png, image/png, 18375, \\x89504e470d0a1a0a0000000d4948445200000213000000ee08060000008330...).","file":"execMain.c","length":383,"level":"error","line":"2006","message":"Error saving guest message: null value in column \"content\" of relation \"guest_messages\" violates not-null constraint","name":"error","routine":"ExecConstraints","schema":"public","severity":"ERROR","stack":"error: null value in column \"content\" of relation \"guest_messages\" violates not-null constraint\n at /app/node_modules/pg-pool/index.js:45:11\n at process.processTicksAndRejections (node:internal/process/task_queues:95:5)\n at async /app/routes/chat.js:224:20","table":"guest_messages","timestamp":"2025-04-28T14:53:07.027Z"}
|
||||
{"level":"error","message":"Error fetching message history for user 1: syntax error at or near \"$\"","stack":"error: syntax error at or near \"$\"\n at /app/node_modules/pg-pool/index.js:45:11\n at process.processTicksAndRejections (node:internal/process/task_queues:95:5)\n at async /app/routes/chat.js:484:20","timestamp":"2025-04-28T14:57:24.546Z"}
|
||||
{"level":"error","message":"Error fetching message history for user 1: syntax error at or near \"$\"","stack":"error: syntax error at or near \"$\"\n at /app/node_modules/pg-pool/index.js:45:11\n at process.processTicksAndRejections (node:internal/process/task_queues:95:5)\n at async /app/routes/chat.js:484:20","timestamp":"2025-04-28T14:58:35.485Z"}
|
||||
|
||||
@@ -43,6 +43,7 @@
|
||||
"imap": "^0.8.19",
|
||||
"langchain": "^0.3.19",
|
||||
"mailparser": "^3.7.2",
|
||||
"multer": "^1.4.5-lts.2",
|
||||
"node-cron": "^3.0.3",
|
||||
"node-telegram-bot-api": "^0.66.0",
|
||||
"nodemailer": "^6.10.0",
|
||||
@@ -87,5 +88,6 @@
|
||||
"cookie": "^1.0.2",
|
||||
"semver": "^7.7.1",
|
||||
"**/utf7/semver": "^7.7.1"
|
||||
}
|
||||
},
|
||||
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
|
||||
}
|
||||
|
||||
@@ -1,14 +1,20 @@
|
||||
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');
|
||||
|
||||
// Настройка multer для обработки файлов в памяти
|
||||
const storage = multer.memoryStorage();
|
||||
const upload = multer({ storage: storage });
|
||||
|
||||
// Функция для обработки гостевых сообщений после аутентификации
|
||||
async function processGuestMessages(userId, guestId) {
|
||||
try {
|
||||
console.log(`Processing guest messages for user ${userId} with guest ID ${guestId}`);
|
||||
logger.info(`Processing guest messages for user ${userId} with guest ID ${guestId}`);
|
||||
|
||||
// Проверяем, обрабатывались ли уже эти сообщения
|
||||
const mappingCheck = await db.query(
|
||||
@@ -18,7 +24,7 @@ async function processGuestMessages(userId, guestId) {
|
||||
|
||||
// Если сообщения уже обработаны, пропускаем
|
||||
if (mappingCheck.rows.length > 0 && mappingCheck.rows[0].processed) {
|
||||
console.log(`Guest messages for guest ID ${guestId} were already processed.`);
|
||||
logger.info(`Guest messages for guest ID ${guestId} were already processed.`);
|
||||
return { success: true, message: 'Guest messages already processed' };
|
||||
}
|
||||
|
||||
@@ -28,35 +34,38 @@ async function processGuestMessages(userId, guestId) {
|
||||
'INSERT INTO guest_user_mapping (user_id, guest_id) VALUES ($1, $2) ON CONFLICT (guest_id) DO UPDATE SET user_id = $1',
|
||||
[userId, guestId]
|
||||
);
|
||||
console.log(`Created mapping for guest ID ${guestId} to user ${userId}`);
|
||||
logger.info(`Created mapping for guest ID ${guestId} to user ${userId}`);
|
||||
}
|
||||
|
||||
// Получаем все гостевые сообщения
|
||||
// Получаем все гостевые сообщения со всеми новыми полями
|
||||
const guestMessagesResult = await db.query(
|
||||
'SELECT * FROM guest_messages WHERE guest_id = $1 ORDER BY created_at ASC',
|
||||
`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) {
|
||||
console.log('No guest messages found');
|
||||
|
||||
// Помечаем как обработанные, даже если сообщений нет
|
||||
await db.query('UPDATE guest_user_mapping SET processed = true WHERE guest_id = $1', [
|
||||
guestId,
|
||||
]);
|
||||
|
||||
logger.info(`No guest messages found for guest ID ${guestId}`);
|
||||
const checkResult = await db.query('SELECT 1 FROM guest_user_mapping WHERE guest_id = $1', [guestId]);
|
||||
if (checkResult.rows.length > 0) {
|
||||
await db.query('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;
|
||||
console.log(`Found ${guestMessages.length} guest messages`);
|
||||
logger.info(`Found ${guestMessages.length} guest messages for guest ID ${guestId}`);
|
||||
|
||||
// Создаем новый диалог для этих сообщений
|
||||
const firstMessage = guestMessages[0];
|
||||
const title =
|
||||
firstMessage.content.length > 30
|
||||
? `${firstMessage.content.substring(0, 30)}...`
|
||||
: firstMessage.content;
|
||||
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.query(
|
||||
'INSERT INTO conversations (user_id, title) VALUES ($1, $2) RETURNING *',
|
||||
@@ -64,74 +73,79 @@ async function processGuestMessages(userId, guestId) {
|
||||
);
|
||||
|
||||
const conversation = newConversationResult.rows[0];
|
||||
console.log('Created new conversation for guest messages:', conversation);
|
||||
logger.info(`Created new conversation ${conversation.id} for guest messages`);
|
||||
|
||||
// Отслеживаем успешные сохранения сообщений
|
||||
const savedMessageIds = [];
|
||||
|
||||
// Обрабатываем каждое гостевое сообщение
|
||||
for (const guestMessage of guestMessages) {
|
||||
console.log(`Processing guest message ID ${guestMessage.id}: ${guestMessage.content}`);
|
||||
logger.info(`Processing guest message ID ${guestMessage.id}: ${guestMessage.content || guestMessage.attachment_filename || '(empty)'}`);
|
||||
|
||||
try {
|
||||
// Сохраняем сообщение пользователя
|
||||
// Сохраняем сообщение пользователя в таблицу messages, включая данные файла
|
||||
const userMessageResult = await db.query(
|
||||
`INSERT INTO messages
|
||||
(conversation_id, content, sender_type, role, channel, created_at, user_id)
|
||||
VALUES
|
||||
($1, $2, $3, $4, $5, $6, $7)
|
||||
`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,
|
||||
'user',
|
||||
'user',
|
||||
'web',
|
||||
guestMessage.content, // Текст (может быть NULL)
|
||||
guestMessage.created_at,
|
||||
userId, // Добавляем userId в сообщение для прямой связи
|
||||
userId,
|
||||
guestMessage.attachment_filename, // Метаданные и данные файла
|
||||
guestMessage.attachment_mimetype,
|
||||
guestMessage.attachment_size,
|
||||
guestMessage.attachment_data // BYTEA
|
||||
]
|
||||
);
|
||||
|
||||
console.log(`Saved user message with ID ${userMessageResult.rows[0].id}`);
|
||||
const savedUserMessage = userMessageResult.rows[0];
|
||||
logger.info(`Saved user message with ID ${savedUserMessage.id}`);
|
||||
savedMessageIds.push(guestMessage.id);
|
||||
|
||||
// Получаем ответ от ИИ только для сообщений пользователя (не AI)
|
||||
if (!guestMessage.is_ai) {
|
||||
console.log('Getting AI response for:', guestMessage.content);
|
||||
// Получаем ответ от ИИ только для текстовых сообщений
|
||||
if (!guestMessage.is_ai && guestMessage.content) {
|
||||
logger.info('Getting AI response for:', guestMessage.content);
|
||||
const language = guestMessage.language || 'auto';
|
||||
const aiResponse = await aiAssistant.getResponse(guestMessage.content, language);
|
||||
console.log('AI response received:', aiResponse);
|
||||
// Предполагаем, что aiAssistant.getResponse принимает только текст
|
||||
const aiResponseContent = await aiAssistant.getResponse(guestMessage.content, language);
|
||||
logger.info('AI response received' + (aiResponseContent ? '' : ' (empty)'), 'for conversation', conversation.id);
|
||||
|
||||
// Сохраняем ответ от ИИ
|
||||
const aiMessageResult = await db.query(
|
||||
`INSERT INTO messages
|
||||
(conversation_id, content, sender_type, role, channel, created_at, user_id)
|
||||
VALUES
|
||||
($1, $2, $3, $4, $5, $6, $7)
|
||||
RETURNING *`,
|
||||
[
|
||||
conversation.id,
|
||||
aiResponse,
|
||||
'assistant',
|
||||
'assistant',
|
||||
'web',
|
||||
new Date(),
|
||||
userId, // Добавляем userId в сообщение для прямой связи
|
||||
]
|
||||
);
|
||||
|
||||
console.log(`Saved AI response with ID ${aiMessageResult.rows[0].id}`);
|
||||
if (aiResponseContent) {
|
||||
// Сохраняем ответ от ИИ (у него нет вложений)
|
||||
const aiMessageResult = await db.query(
|
||||
`INSERT INTO messages
|
||||
(conversation_id, content, sender_type, role, channel, created_at, user_id)
|
||||
VALUES
|
||||
($1, $2, 'assistant', 'assistant', 'web', $3, $4)
|
||||
RETURNING *`,
|
||||
[
|
||||
conversation.id,
|
||||
aiResponseContent,
|
||||
new Date(),
|
||||
userId
|
||||
]
|
||||
);
|
||||
logger.info(`Saved AI response with ID ${aiMessageResult.rows[0].id}`);
|
||||
}
|
||||
} else {
|
||||
logger.info(`Skipping AI response for guest message ID ${guestMessage.id} (is_ai: ${guestMessage.is_ai}, hasContent: ${!!guestMessage.content})`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error processing guest message ${guestMessage.id}:`, error);
|
||||
logger.error(`Error processing guest message ${guestMessage.id}: ${error.message}`, { stack: error.stack });
|
||||
// Продолжаем с другими сообщениями в случае ошибки
|
||||
}
|
||||
}
|
||||
|
||||
// Удаляем только успешно обработанные гостевые сообщения
|
||||
if (savedMessageIds.length > 0) {
|
||||
await db.query('DELETE FROM guest_messages WHERE id = ANY($1)', [savedMessageIds]);
|
||||
console.log(
|
||||
await db.query('DELETE FROM guest_messages WHERE id = ANY($1::int[])', [savedMessageIds]);
|
||||
logger.info(
|
||||
`Deleted ${savedMessageIds.length} processed guest messages for guest ID ${guestId}`
|
||||
);
|
||||
|
||||
@@ -139,8 +153,12 @@ async function processGuestMessages(userId, guestId) {
|
||||
await db.query('UPDATE guest_user_mapping SET processed = true WHERE guest_id = $1', [
|
||||
guestId,
|
||||
]);
|
||||
logger.info(`Marked guest mapping as processed for guest ID ${guestId}`);
|
||||
} else {
|
||||
console.log('No guest messages were successfully processed, skipping deletion');
|
||||
logger.warn(`No guest messages were successfully processed, skipping deletion for guest ID ${guestId}`);
|
||||
// Если не было успешных, все равно пометим как обработанные, чтобы не пытаться снова
|
||||
await db.query('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 {
|
||||
@@ -149,130 +167,244 @@ async function processGuestMessages(userId, guestId) {
|
||||
conversationId: conversation.id,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error processing guest messages:', error);
|
||||
throw 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', async (req, res) => {
|
||||
try {
|
||||
const { content, language, guestId: requestGuestId } = req.body;
|
||||
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); // Файлы будут здесь
|
||||
|
||||
if (!content) {
|
||||
return res.status(400).json({ success: false, error: 'Content is required' });
|
||||
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 гостя в сессии
|
||||
req.session.guestId = guestId;
|
||||
await req.session.save();
|
||||
// Сохраняем/обновляем ID гостя в сессии
|
||||
if (req.session.guestId !== guestId) {
|
||||
req.session.guestId = guestId;
|
||||
}
|
||||
|
||||
console.log('Saving guest message:', { guestId, content });
|
||||
// Подготавливаем данные для вставки
|
||||
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.query(
|
||||
'INSERT INTO guest_messages (guest_id, content, language, is_ai) VALUES ($1, $2, $3, false) RETURNING id',
|
||||
[guestId, content, language || 'auto']
|
||||
`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
|
||||
]
|
||||
);
|
||||
|
||||
console.log('Guest message saved:', result.rows[0]);
|
||||
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);
|
||||
// Не прерываем ответ пользователю из-за ошибки сессии
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
messageId: result.rows[0].id,
|
||||
messageId: savedMessageId, // Возвращаем ID сохраненного сообщения
|
||||
guestId: guestId // Возвращаем использованный guestId
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error saving guest message:', error);
|
||||
res.status(500).json({ success: false, error: 'Internal server error' });
|
||||
logger.error('Error saving guest message:', error);
|
||||
res.status(500).json({ success: false, error: 'Ошибка сохранения гостевого сообщения' });
|
||||
}
|
||||
});
|
||||
|
||||
// Маршрут для обычных сообщений (для аутентифицированных пользователей)
|
||||
router.post('/message', requireAuth, async (req, res) => {
|
||||
const { message, conversationId, language = 'auto' } = req.body;
|
||||
// Обработчик для сообщений аутентифицированных пользователей
|
||||
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);
|
||||
|
||||
if (!message) {
|
||||
return res.status(400).json({ error: 'Message is required' });
|
||||
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 {
|
||||
console.log('Processing message:', {
|
||||
message,
|
||||
conversationId,
|
||||
language,
|
||||
userId: req.session.userId,
|
||||
});
|
||||
const userId = req.session.userId;
|
||||
|
||||
let conversation;
|
||||
|
||||
// Если указан ID диалога, проверяем его существование и принадлежность пользователю
|
||||
// Найти или создать диалог
|
||||
if (conversationId) {
|
||||
const conversationResult = await db.query(
|
||||
const convResult = await db.query(
|
||||
'SELECT * FROM conversations WHERE id = $1 AND user_id = $2',
|
||||
[conversationId, userId]
|
||||
);
|
||||
|
||||
if (conversationResult.rows.length === 0) {
|
||||
return res.status(404).json({ error: 'Conversation not found or access denied' });
|
||||
if (convResult.rows.length === 0) {
|
||||
logger.warn('Conversation not found or access denied', { conversationId, userId });
|
||||
return res.status(404).json({ success: false, error: 'Диалог не найден или доступ запрещен' });
|
||||
}
|
||||
|
||||
conversation = conversationResult.rows[0];
|
||||
console.log('Using existing conversation:', conversation);
|
||||
conversation = convResult.rows[0];
|
||||
} else {
|
||||
// Создаем новый диалог
|
||||
const title = message.length > 30 ? `${message.substring(0, 30)}...` : message;
|
||||
// Создаем новый диалог, если ID не предоставлен
|
||||
const title = message
|
||||
? (message.length > 50 ? `${message.substring(0, 50)}...` : message)
|
||||
: (file ? `Файл: ${file.originalname}` : 'Новый диалог');
|
||||
|
||||
const newConversationResult = await db.query(
|
||||
const newConvResult = await db.query(
|
||||
'INSERT INTO conversations (user_id, title) VALUES ($1, $2) RETURNING *',
|
||||
[userId, title]
|
||||
);
|
||||
|
||||
conversation = newConversationResult.rows[0];
|
||||
console.log('Created new conversation:', conversation);
|
||||
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;
|
||||
|
||||
// Сохраняем сообщение пользователя
|
||||
console.log('Saving user message');
|
||||
const userMessageResult = await db.query(
|
||||
`INSERT INTO messages
|
||||
(conversation_id, content, sender_type, role, tokens_used, channel, created_at)
|
||||
VALUES
|
||||
($1, $2, $3, $4, $5, $6, $7)
|
||||
`INSERT INTO messages
|
||||
(conversation_id, user_id, content, sender_type, role, channel,
|
||||
attachment_filename, attachment_mimetype, attachment_size, attachment_data)
|
||||
VALUES ($1, $2, $3, 'user', 'user', 'web', $4, $5, $6, $7)
|
||||
RETURNING *`,
|
||||
[conversation.id, message, 'user', 'user', 0, 'web', new Date()]
|
||||
[
|
||||
conversationId,
|
||||
userId,
|
||||
messageContent,
|
||||
attachmentFilename,
|
||||
attachmentMimetype,
|
||||
attachmentSize,
|
||||
attachmentData
|
||||
]
|
||||
);
|
||||
const userMessage = userMessageResult.rows[0];
|
||||
logger.info('User message saved', { messageId: userMessage.id, conversationId });
|
||||
|
||||
// Получаем ответ от ИИ
|
||||
console.log('Getting AI response');
|
||||
const aiResponse = await aiAssistant.getResponse(message, language);
|
||||
console.log('AI response received:', aiResponse);
|
||||
// Получаем ответ от ИИ, только если это было текстовое сообщение
|
||||
let aiMessage = null;
|
||||
if (messageContent) { // Только для текстовых сообщений
|
||||
try {
|
||||
const detectedLanguage = language === 'auto' ? aiAssistant.detectLanguage(messageContent) : language;
|
||||
logger.info('Getting AI response for:', messageContent);
|
||||
const aiResponseContent = await aiAssistant.getResponse(messageContent, detectedLanguage);
|
||||
logger.info('AI response received' + (aiResponseContent ? '' : ' (empty)'), { conversationId });
|
||||
|
||||
// Сохраняем ответ от ИИ
|
||||
console.log('Saving AI response');
|
||||
const aiMessageResult = await db.query(
|
||||
`INSERT INTO messages
|
||||
(conversation_id, content, sender_type, role, tokens_used, channel, created_at)
|
||||
VALUES
|
||||
($1, $2, $3, $4, $5, $6, $7)
|
||||
RETURNING *`,
|
||||
[conversation.id, aiResponse, 'assistant', 'assistant', 0, 'web', new Date()]
|
||||
);
|
||||
if (aiResponseContent) {
|
||||
const aiMessageResult = await db.query(
|
||||
`INSERT INTO messages
|
||||
(conversation_id, user_id, content, sender_type, role, channel)
|
||||
VALUES ($1, $2, $3, 'assistant', 'assistant', 'web')
|
||||
RETURNING *`,
|
||||
[conversationId, userId, aiResponseContent]
|
||||
);
|
||||
aiMessage = aiMessageResult.rows[0];
|
||||
logger.info('AI response saved', { messageId: aiMessage.id, conversationId });
|
||||
}
|
||||
} catch (aiError) {
|
||||
logger.error('Error getting or saving AI response:', aiError);
|
||||
// Не прерываем основной ответ, но логируем ошибку
|
||||
}
|
||||
}
|
||||
|
||||
const response = {
|
||||
success: true,
|
||||
userMessage: userMessageResult.rows[0],
|
||||
aiMessage: aiMessageResult.rows[0],
|
||||
conversation,
|
||||
// Форматируем ответ для фронтенда
|
||||
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;
|
||||
};
|
||||
|
||||
res.json(response);
|
||||
res.json({
|
||||
success: true,
|
||||
conversationId: conversationId,
|
||||
userMessage: formatMessageForFrontend(userMessage),
|
||||
aiMessage: formatMessageForFrontend(aiMessage),
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error processing message:', error);
|
||||
res.status(500).json({ error: 'Error processing message' });
|
||||
logger.error('Error processing authenticated message:', error);
|
||||
res.status(500).json({ success: false, error: 'Ошибка обработки сообщения' });
|
||||
}
|
||||
});
|
||||
|
||||
@@ -292,79 +424,120 @@ router.get('/models', async (req, res) => {
|
||||
});
|
||||
|
||||
// Получение истории сообщений
|
||||
router.get('/history', async (req, res) => {
|
||||
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 {
|
||||
console.log('Session in history route:', {
|
||||
id: req.sessionID,
|
||||
userId: req.session.userId,
|
||||
address: req.session.address,
|
||||
authenticated: req.session.authenticated,
|
||||
guestId: req.session.guestId,
|
||||
// Если нужен только подсчет
|
||||
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.query(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.query(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;
|
||||
});
|
||||
|
||||
const limit = parseInt(req.query.limit) || 50;
|
||||
const offset = parseInt(req.query.offset) || 0;
|
||||
|
||||
// Если пользователь аутентифицирован и у него есть гостевые сообщения,
|
||||
// автоматически связываем их перед получением истории
|
||||
if (req.session.authenticated && req.session.userId && req.session.guestId) {
|
||||
try {
|
||||
console.log('Automatically linking guest messages before fetching history');
|
||||
await processGuestMessages(req.session.userId, req.session.guestId);
|
||||
|
||||
// Очищаем guestId из сессии после связывания
|
||||
req.session.guestId = null;
|
||||
await req.session.save();
|
||||
|
||||
console.log('Guest messages automatically linked');
|
||||
} catch (linkError) {
|
||||
console.error('Error auto-linking guest messages:', linkError);
|
||||
// Продолжаем выполнение, даже если связывание не удалось
|
||||
}
|
||||
// Получаем общее количество сообщений для пагинации (если не запрашивали только количество)
|
||||
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.query(totalCountQuery, totalCountParams);
|
||||
const totalMessages = parseInt(totalCountResult.rows[0].count, 10);
|
||||
|
||||
let messages = [];
|
||||
let total = 0;
|
||||
logger.info(`Returning message history for user ${userId}`, { count: messages.length, offset, limit, total: totalMessages });
|
||||
|
||||
// Если пользователь аутентифицирован, получаем его сообщения
|
||||
if (req.session.authenticated && req.session.userId) {
|
||||
const countResult = await db.query(
|
||||
`SELECT COUNT(*) as total FROM messages m
|
||||
JOIN conversations c ON m.conversation_id = c.id
|
||||
WHERE c.user_id = $1`,
|
||||
[req.session.userId]
|
||||
);
|
||||
total = parseInt(countResult.rows[0].total) || 0;
|
||||
|
||||
const result = await db.query(
|
||||
`SELECT
|
||||
m.id,
|
||||
m.content,
|
||||
m.sender_type,
|
||||
m.role,
|
||||
m.created_at,
|
||||
c.user_id,
|
||||
c.id as conversation_id
|
||||
FROM messages m
|
||||
JOIN conversations c ON m.conversation_id = c.id
|
||||
WHERE c.user_id = $1
|
||||
ORDER BY m.created_at ASC
|
||||
LIMIT $2 OFFSET $3`,
|
||||
[req.session.userId, limit, offset]
|
||||
);
|
||||
|
||||
messages = result.rows;
|
||||
console.log(`Found ${messages.length} messages for authenticated user`);
|
||||
}
|
||||
|
||||
return res.json({
|
||||
res.json({
|
||||
success: true,
|
||||
messages: messages,
|
||||
total: total,
|
||||
offset: offset,
|
||||
limit: limit,
|
||||
total: totalMessages
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
logger.error('Error getting chat history:', error);
|
||||
return res.status(500).json({ error: 'Internal server error' });
|
||||
logger.error(`Error fetching message history for user ${userId}: ${error.message}`, { stack: error.stack });
|
||||
res.status(500).json({ success: false, error: 'Ошибка получения истории сообщений' });
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -38,16 +38,48 @@ async function runMigrations() {
|
||||
for (const file of migrationFiles) {
|
||||
if (!executedMigrations.has(file)) {
|
||||
const filePath = path.join(migrationsDir, file);
|
||||
const sql = await fs.readFile(filePath, 'utf-8');
|
||||
const fileContent = await fs.readFile(filePath, 'utf-8');
|
||||
|
||||
// Ищем начало UP секции (или начало файла)
|
||||
const upMarker = '-- UP Migration';
|
||||
const downMarker = '-- DOWN Migration';
|
||||
let upSqlStartIndex = fileContent.indexOf(upMarker);
|
||||
if (upSqlStartIndex !== -1) {
|
||||
// Ищем перевод строки после маркера
|
||||
let newlineIndex = fileContent.indexOf('\n', upSqlStartIndex);
|
||||
if (newlineIndex === -1) { // Если маркер в последней строке
|
||||
newlineIndex = fileContent.length;
|
||||
}
|
||||
upSqlStartIndex = newlineIndex + 1; // Начинаем со следующей строки
|
||||
} else {
|
||||
upSqlStartIndex = 0; // Если маркера нет, берем все с начала
|
||||
}
|
||||
|
||||
// Ищем конец UP секции (начало DOWN секции)
|
||||
let upSqlEndIndex = fileContent.indexOf(downMarker);
|
||||
if (upSqlEndIndex === -1) {
|
||||
upSqlEndIndex = fileContent.length; // Если маркера DOWN нет, берем все до конца
|
||||
}
|
||||
|
||||
// Извлекаем только UP SQL
|
||||
const sqlToExecute = fileContent.substring(upSqlStartIndex, upSqlEndIndex).trim();
|
||||
|
||||
if (!sqlToExecute) {
|
||||
logger.warn(`Migration file ${file} has no executable UP SQL content. Skipping.`);
|
||||
continue; // Пропускаем пустые миграции
|
||||
}
|
||||
|
||||
logger.info(`Executing UP migration from ${file}...`);
|
||||
await pool.query('BEGIN');
|
||||
try {
|
||||
await pool.query(sql);
|
||||
// Выполняем только извлеченный UP SQL
|
||||
await pool.query(sqlToExecute);
|
||||
await pool.query('INSERT INTO migrations (name) VALUES ($1)', [file]);
|
||||
await pool.query('COMMIT');
|
||||
logger.info(`Migration ${file} executed successfully`);
|
||||
} catch (error) {
|
||||
await pool.query('ROLLBACK');
|
||||
logger.error(`Error executing migration ${file}:`, error); // Логируем ошибку перед пробросом
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
1793
backend/yarn.lock
1793
backend/yarn.lock
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user