417 lines
16 KiB
JavaScript
417 lines
16 KiB
JavaScript
const express = require('express');
|
||
const router = express.Router();
|
||
const { ChatOllama } = require('@langchain/ollama');
|
||
const { getVectorStore } = require('../services/vectorStore');
|
||
const db = require('../db');
|
||
const { requireAuth, requireAdmin } = require('../middleware/auth');
|
||
const logger = require('../utils/logger');
|
||
const crypto = require('crypto');
|
||
|
||
// Добавьте эту функцию в начало файла chat.js
|
||
async function getAIResponse(message, language = 'ru') {
|
||
// Определяем язык сообщения, если не указан явно
|
||
let detectedLanguage = language;
|
||
if (!language || language === 'auto') {
|
||
// Простая эвристика для определения языка
|
||
const cyrillicPattern = /[а-яА-ЯёЁ]/;
|
||
detectedLanguage = cyrillicPattern.test(message) ? 'ru' : 'en';
|
||
}
|
||
|
||
// Формируем системный промпт в зависимости от языка
|
||
let systemPrompt = '';
|
||
if (detectedLanguage === 'ru') {
|
||
systemPrompt = 'Вы - полезный ассистент. Отвечайте на русском языке.';
|
||
} else {
|
||
systemPrompt = 'You are a helpful assistant. Respond in English.';
|
||
}
|
||
|
||
// Создаем экземпляр ChatOllama
|
||
const chat = new ChatOllama({
|
||
baseUrl: process.env.OLLAMA_BASE_URL || 'http://localhost:11434',
|
||
model: process.env.OLLAMA_MODEL || 'mistral',
|
||
system: systemPrompt
|
||
});
|
||
|
||
console.log('Отправка запроса к Ollama...');
|
||
|
||
// Получаем ответ от модели
|
||
try {
|
||
const response = await chat.invoke(message);
|
||
return response.content;
|
||
} catch (error) {
|
||
console.error('Ошибка при вызове ChatOllama:', error);
|
||
|
||
// Альтернативный метод запроса через прямой API
|
||
try {
|
||
console.log('Пробуем альтернативный метод запроса...');
|
||
const response = await fetch(`${process.env.OLLAMA_BASE_URL || 'http://localhost:11434'}/api/generate`, {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
},
|
||
body: JSON.stringify({
|
||
model: process.env.OLLAMA_MODEL || 'mistral',
|
||
prompt: message,
|
||
system: systemPrompt,
|
||
stream: false
|
||
}),
|
||
});
|
||
|
||
const data = await response.json();
|
||
return data.response;
|
||
} catch (fallbackError) {
|
||
console.error('Ошибка при использовании альтернативного метода:', fallbackError);
|
||
return "Извините, я не смог обработать ваш запрос. Пожалуйста, попробуйте позже.";
|
||
}
|
||
}
|
||
}
|
||
|
||
// Обработчик сообщений чата
|
||
router.post('/message', requireAuth, async (req, res) => {
|
||
console.log('Сессия в /api/chat/message:', req.session);
|
||
console.log('Аутентифицирован:', req.session.authenticated);
|
||
|
||
try {
|
||
const { message, language = 'ru' } = req.body;
|
||
const userId = typeof req.session.userId === 'object'
|
||
? req.session.userId.userId
|
||
: req.session.userId;
|
||
|
||
console.log(`Получено сообщение: ${message}, язык: ${language}, userId: ${userId}`);
|
||
|
||
// Проверяем, что userId существует
|
||
if (!userId) {
|
||
return res.status(400).json({ error: 'User ID is required' });
|
||
}
|
||
|
||
// Определяем язык сообщения, если не указан явно
|
||
let detectedLanguage = language;
|
||
if (!language || language === 'auto') {
|
||
// Простая эвристика для определения языка
|
||
const cyrillicPattern = /[а-яА-ЯёЁ]/;
|
||
detectedLanguage = cyrillicPattern.test(message) ? 'ru' : 'en';
|
||
}
|
||
|
||
// Формируем системный промпт в зависимости от языка
|
||
let systemPrompt = '';
|
||
if (detectedLanguage === 'ru') {
|
||
systemPrompt = 'Вы - полезный ассистент. Отвечайте на русском языке.';
|
||
} else {
|
||
systemPrompt = 'You are a helpful assistant. Respond in English.';
|
||
}
|
||
|
||
// Отправляем запрос к Ollama с указанием языка
|
||
console.log(`Отправка запроса к Ollama (модель: ${process.env.OLLAMA_MODEL || 'mistral'}, язык: ${detectedLanguage}): ${message}`);
|
||
|
||
// Проверяем доступность Ollama
|
||
console.log('Проверка доступности Ollama...');
|
||
try {
|
||
const response = await fetch(`${process.env.OLLAMA_BASE_URL || 'http://localhost:11434'}/api/tags`);
|
||
const data = await response.json();
|
||
console.log('Ollama доступен. Доступные модели:');
|
||
data.models.forEach(model => {
|
||
console.log(`- ${model.name}`);
|
||
});
|
||
} catch (error) {
|
||
console.error('Ошибка при проверке доступности Ollama:', error);
|
||
return res.status(500).json({ error: 'Сервис Ollama недоступен' });
|
||
}
|
||
|
||
// Создаем экземпляр ChatOllama
|
||
const chat = new ChatOllama({
|
||
baseUrl: process.env.OLLAMA_BASE_URL || 'http://localhost:11434',
|
||
model: process.env.OLLAMA_MODEL || 'mistral',
|
||
system: systemPrompt
|
||
});
|
||
|
||
console.log('Отправка запроса к Ollama...');
|
||
|
||
// Получаем ответ от модели
|
||
let aiResponse;
|
||
try {
|
||
const response = await chat.invoke(message);
|
||
aiResponse = response.content;
|
||
console.log('Ответ AI:', aiResponse);
|
||
} catch (error) {
|
||
console.error('Ошибка при вызове ChatOllama:', error);
|
||
|
||
// Альтернативный метод запроса через прямой API
|
||
try {
|
||
console.log('Пробуем альтернативный метод запроса...');
|
||
const response = await fetch(`${process.env.OLLAMA_BASE_URL || 'http://localhost:11434'}/api/generate`, {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
},
|
||
body: JSON.stringify({
|
||
model: process.env.OLLAMA_MODEL || 'mistral',
|
||
prompt: message,
|
||
system: systemPrompt,
|
||
stream: false
|
||
}),
|
||
});
|
||
|
||
const data = await response.json();
|
||
aiResponse = data.response;
|
||
console.log('Ответ AI (альтернативный метод):', aiResponse);
|
||
} catch (fallbackError) {
|
||
console.error('Ошибка при использовании альтернативного метода:', fallbackError);
|
||
throw error; // Выбрасываем исходную ошибку
|
||
}
|
||
}
|
||
|
||
// Получаем или создаем диалог
|
||
let conversationId;
|
||
const conversationResult = await db.query(`
|
||
SELECT id FROM conversations
|
||
WHERE user_id = $1
|
||
ORDER BY updated_at DESC
|
||
LIMIT 1
|
||
`, [userId]);
|
||
|
||
if (conversationResult.rows.length === 0) {
|
||
// Создаем новый диалог
|
||
const newConversationResult = await db.query(`
|
||
INSERT INTO conversations (user_id, created_at, updated_at)
|
||
VALUES ($1, NOW(), NOW())
|
||
RETURNING id
|
||
`, [userId]);
|
||
conversationId = newConversationResult.rows[0].id;
|
||
console.log('Created new conversation:', conversationId);
|
||
} else {
|
||
conversationId = conversationResult.rows[0].id;
|
||
console.log('Using existing conversation:', conversationId);
|
||
}
|
||
|
||
// Сохраняем сообщение пользователя
|
||
const userMessageResult = await db.query(`
|
||
INSERT INTO messages (conversation_id, sender_type, sender_id, content, channel, created_at)
|
||
VALUES ($1, 'user', $2, $3, 'web', NOW())
|
||
RETURNING id
|
||
`, [conversationId, userId, message]);
|
||
console.log('Saved user message:', userMessageResult.rows[0].id);
|
||
|
||
// Сохраняем ответ ИИ
|
||
const aiMessageResult = await db.query(`
|
||
INSERT INTO messages (conversation_id, sender_type, content, channel, created_at)
|
||
VALUES ($1, 'ai', $2, 'web', NOW())
|
||
RETURNING id
|
||
`, [conversationId, aiResponse]);
|
||
console.log('Saved AI message:', aiMessageResult.rows[0].id);
|
||
|
||
// Обновляем время последнего сообщения в диалоге
|
||
await db.query(`
|
||
UPDATE conversations
|
||
SET updated_at = NOW()
|
||
WHERE id = $1
|
||
`, [conversationId]);
|
||
|
||
res.json({
|
||
reply: aiResponse,
|
||
language: detectedLanguage
|
||
});
|
||
} catch (error) {
|
||
console.error('Error processing message:', error);
|
||
res.status(500).json({ error: 'Internal server error' });
|
||
}
|
||
});
|
||
|
||
// Добавьте этот маршрут для проверки доступных моделей
|
||
router.get('/models', async (req, res) => {
|
||
try {
|
||
const ollama = new Ollama();
|
||
const models = await ollama.list();
|
||
|
||
res.json({
|
||
success: true,
|
||
models: models.models.map((model) => model.name),
|
||
});
|
||
} catch (error) {
|
||
console.error('Ошибка при получении списка моделей:', error);
|
||
res.status(500).json({ success: false, message: 'Ошибка сервера' });
|
||
}
|
||
});
|
||
|
||
// Получение истории сообщений
|
||
router.get('/history', requireAuth, async (req, res) => {
|
||
try {
|
||
// Получаем ID пользователя из сессии или из объекта пользователя
|
||
const userId = req.session?.userId || req.user?.userId;
|
||
|
||
console.log('Запрос истории чата для пользователя:', userId);
|
||
console.log('User object from request:', req.user);
|
||
|
||
// Проверяем, что userId существует
|
||
if (!userId) {
|
||
console.error('Пользователь не аутентифицирован');
|
||
return res.status(401).json({ error: 'Unauthorized' });
|
||
}
|
||
|
||
// Получаем историю сообщений из базы данных
|
||
console.log('Querying chat history for user:', userId);
|
||
|
||
// Проверяем, существует ли таблица messages
|
||
try {
|
||
const tableCheck = await db.query(`
|
||
SELECT EXISTS (
|
||
SELECT FROM information_schema.tables
|
||
WHERE table_name = 'messages'
|
||
);
|
||
`);
|
||
|
||
console.log('Table messages exists:', tableCheck.rows[0].exists);
|
||
|
||
if (tableCheck.rows[0].exists) {
|
||
// Используем таблицу messages
|
||
const result = await db.query(`
|
||
SELECT m.*, c.user_id
|
||
FROM messages m
|
||
JOIN conversations c ON m.conversation_id = c.id
|
||
WHERE c.user_id = $1
|
||
ORDER BY m.created_at ASC
|
||
`, [userId]);
|
||
|
||
console.log(`Найдено ${result.rows.length} сообщений для пользователя ${userId}`);
|
||
|
||
return res.json({ messages: result.rows });
|
||
} else {
|
||
// Проверяем, существует ли таблица chat_history
|
||
const chatHistoryCheck = await db.query(`
|
||
SELECT EXISTS (
|
||
SELECT FROM information_schema.tables
|
||
WHERE table_name = 'chat_history'
|
||
);
|
||
`);
|
||
|
||
console.log('Table chat_history exists:', chatHistoryCheck.rows[0].exists);
|
||
|
||
if (chatHistoryCheck.rows[0].exists) {
|
||
// Используем таблицу chat_history
|
||
const result = await db.query(`
|
||
SELECT * FROM chat_history
|
||
WHERE user_id = $1
|
||
ORDER BY created_at ASC
|
||
`, [userId]);
|
||
|
||
console.log(`Найдено ${result.rows.length} сообщений для пользователя ${userId}`);
|
||
|
||
return res.json({ messages: result.rows });
|
||
} else {
|
||
// Ни одна из таблиц не существует
|
||
console.log('No message tables found in database');
|
||
return res.json({ messages: [] });
|
||
}
|
||
}
|
||
} catch (error) {
|
||
console.error('Error checking tables:', error);
|
||
return res.json({ messages: [] });
|
||
}
|
||
} catch (error) {
|
||
console.error('Error fetching chat history:', error);
|
||
res.status(500).json({ error: 'Internal server error' });
|
||
}
|
||
});
|
||
|
||
// Маршрут для получения всех диалогов (только для админов)
|
||
router.get('/admin/history', requireAdmin, async (req, res) => {
|
||
try {
|
||
const { limit = 50, offset = 0, userId } = req.query;
|
||
|
||
let query = `
|
||
SELECT ch.id, ch.user_id, u.username, ch.channel,
|
||
ch.sender_type, ch.content, ch.metadata, ch.created_at
|
||
FROM chat_history ch
|
||
LEFT JOIN users u ON ch.user_id = u.id
|
||
`;
|
||
|
||
const params = [];
|
||
let paramIndex = 1;
|
||
|
||
if (userId) {
|
||
query += ` WHERE ch.user_id = $${paramIndex}`;
|
||
params.push(userId);
|
||
paramIndex++;
|
||
}
|
||
|
||
query += ` ORDER BY ch.created_at DESC LIMIT $${paramIndex} OFFSET $${paramIndex + 1}`;
|
||
params.push(limit, offset);
|
||
|
||
const result = await db.query(query, params);
|
||
|
||
res.json(result.rows);
|
||
} catch (error) {
|
||
logger.error('Error fetching admin chat history:', error);
|
||
res.status(500).json({ error: 'Внутренняя ошибка сервера' });
|
||
}
|
||
});
|
||
|
||
// Обработчик для гостевых сообщений
|
||
router.post('/guest-message', async (req, res) => {
|
||
try {
|
||
const { message, language } = req.body;
|
||
console.log(`Получено гостевое сообщение: ${message} язык: ${language}`);
|
||
|
||
// Генерируем временный ID сессии, если его нет
|
||
if (!req.session.guestId) {
|
||
req.session.guestId = crypto.randomBytes(16).toString('hex');
|
||
}
|
||
|
||
// Сохраняем сообщение в базе данных с временным ID
|
||
await db.query(`
|
||
INSERT INTO guest_messages (guest_id, content, language, created_at)
|
||
VALUES ($1, $2, $3, NOW())
|
||
`, [req.session.guestId, message, language]);
|
||
|
||
// Отправляем запрос к AI
|
||
const aiResponse = await getAIResponse(message, language);
|
||
|
||
// Сохраняем ответ AI в базе данных
|
||
await db.query(`
|
||
INSERT INTO guest_messages (guest_id, content, language, created_at, is_ai)
|
||
VALUES ($1, $2, $3, NOW(), true)
|
||
`, [req.session.guestId, aiResponse, language]);
|
||
|
||
return res.json({ message: aiResponse, reply: aiResponse });
|
||
} catch (error) {
|
||
console.error('Error processing guest message:', error);
|
||
return res.status(500).json({ error: 'Internal server error' });
|
||
}
|
||
});
|
||
|
||
// Обработчик для связывания гостевых сообщений с пользователем
|
||
router.post('/link-guest-messages', requireAuth, async (req, res) => {
|
||
try {
|
||
const userId = req.session.userId;
|
||
const guestId = req.session.guestId;
|
||
|
||
if (!guestId) {
|
||
return res.json({ success: true, message: 'No guest messages to link' });
|
||
}
|
||
|
||
// Связываем гостевые сообщения с пользователем
|
||
await db.query(`
|
||
INSERT INTO messages (user_id, content, role, created_at)
|
||
SELECT $1, content, CASE WHEN is_ai THEN 'assistant' ELSE 'user' END, created_at
|
||
FROM guest_messages
|
||
WHERE guest_id = $2
|
||
ORDER BY created_at
|
||
`, [userId, guestId]);
|
||
|
||
// Удаляем гостевые сообщения
|
||
await db.query(`
|
||
DELETE FROM guest_messages
|
||
WHERE guest_id = $1
|
||
`, [guestId]);
|
||
|
||
// Удаляем временный ID из сессии
|
||
delete req.session.guestId;
|
||
|
||
return res.json({ success: true });
|
||
} catch (error) {
|
||
console.error('Error linking guest messages:', error);
|
||
return res.status(500).json({ error: 'Internal server error' });
|
||
}
|
||
});
|
||
|
||
module.exports = router;
|