ваше сообщение коммита

This commit is contained in:
2025-06-04 14:39:36 +03:00
parent 77f09a03a5
commit 59557e4413
15 changed files with 912 additions and 84 deletions

View File

@@ -0,0 +1,21 @@
CREATE TABLE IF NOT EXISTS ai_assistant_settings (
id SERIAL PRIMARY KEY,
system_prompt TEXT,
selected_rag_tables INTEGER[],
languages TEXT[],
model TEXT,
rules JSONB,
updated_at TIMESTAMP DEFAULT NOW(),
updated_by INTEGER,
);
-- Вставить дефолтную строку (глобальные настройки)
INSERT INTO ai_assistant_settings (system_prompt, selected_rag_tables, languages, model, rules)
VALUES (
'Вы — полезный ассистент. Отвечайте на русском языке.',
ARRAY[]::INTEGER[],
ARRAY['ru'],
'qwen2.5',
'{"checkUserTags": true, "searchRagFirst": true, "generateIfNoRag": true, "requireAdminApproval": true}'
)
ON CONFLICT DO NOTHING;

View File

@@ -0,0 +1,11 @@
CREATE TABLE IF NOT EXISTS ai_assistant_rules (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL,
description TEXT,
rules JSONB NOT NULL,
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW()
);
ALTER TABLE ai_assistant_settings
ADD COLUMN IF NOT EXISTS rules_id INTEGER REFERENCES ai_assistant_rules(id);

View File

@@ -0,0 +1,5 @@
-- Добавление недостающих полей для интеграции с Telegram и Email, а также для системного сообщения
ALTER TABLE ai_assistant_settings
ADD COLUMN IF NOT EXISTS telegram_settings_id INTEGER REFERENCES telegram_settings(id),
ADD COLUMN IF NOT EXISTS email_settings_id INTEGER REFERENCES email_settings(id),
ADD COLUMN IF NOT EXISTS system_message TEXT;

View File

@@ -6,6 +6,8 @@ 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();
@@ -61,19 +63,28 @@ async function processGuestMessages(userId, guestId) {
const guestMessages = guestMessagesResult.rows;
logger.info(`Found ${guestMessages.length} guest messages for guest ID ${guestId}`);
// Создаем новый диалог для этих сообщений
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]
// --- Новый порядок: ищем последний диалог пользователя ---
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]
);
const conversation = newConversationResult.rows[0];
logger.info(`Created new conversation ${conversation.id} for guest messages`);
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 = [];
@@ -81,7 +92,6 @@ async function processGuestMessages(userId, guestId) {
// Обрабатываем каждое гостевое сообщение
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()(
@@ -103,39 +113,59 @@ async function processGuestMessages(userId, guestId) {
guestMessage.attachment_data // BYTEA
]
);
const savedUserMessage = userMessageResult.rows[0];
logger.info(`Saved user message with ID ${savedUserMessage.id}`);
savedMessageIds.push(guestMessage.id);
// Получаем ответ от ИИ только для текстовых сообщений
if (!guestMessage.is_ai && guestMessage.content) {
logger.info('Getting AI response for:', guestMessage.content);
const language = guestMessage.language || 'auto';
// Предполагаем, что aiAssistant.getResponse принимает только текст
const aiResponseContent = await aiAssistant.getResponse(guestMessage.content, language);
logger.info('AI response received' + (aiResponseContent ? '' : ' (empty)'), 'for conversation', conversation.id);
if (aiResponseContent) {
// Сохраняем ответ от ИИ (у него нет вложений)
const aiMessageResult = await db.getQuery()(
`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
]
// --- Генерируем ответ ИИ на гостевое сообщение, если это текст ---
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]
);
logger.info(`Saved AI response with ID ${aiMessageResult.rows[0].id}`);
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);
}
}
} else {
logger.info(`Skipping AI response for guest message ID ${guestMessage.id} (is_ai: ${guestMessage.is_ai}, hasContent: ${!!guestMessage.content})`);
}
// --- конец блока генерации ответа ИИ ---
} catch (error) {
logger.error(`Error processing guest message ${guestMessage.id}: ${error.message}`, { stack: error.stack });
// Продолжаем с другими сообщениями в случае ошибки
@@ -254,10 +284,28 @@ router.post('/guest-message', upload.array('attachments'), async (req, res) => {
// Не прерываем ответ пользователю из-за ошибки сессии
}
// Получаем настройки ассистента для 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
guestId: guestId, // Возвращаем использованный guestId
systemMessage: 'Для продолжения диалога авторизуйтесь: подключите кошелек, перейдите в чат-бот Telegram или отправьте письмо на email.',
telegramBotUrl,
supportEmail: supportEmailAddr
});
} catch (error) {
logger.error('Error saving guest message:', error);
@@ -303,18 +351,27 @@ router.post('/message', requireAuth, upload.array('attachments'), async (req, re
}
conversation = convResult.rows[0];
} else {
// Создаем новый диалог, если ID не предоставлен
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]
// Ищем последний диалог пользователя
const lastConvResult = await db.getQuery()(
'SELECT * FROM conversations WHERE user_id = $1 ORDER BY updated_at DESC, created_at DESC LIMIT 1',
[userId]
);
conversation = newConvResult.rows[0];
conversationId = conversation.id;
logger.info('Created new conversation', { conversationId, 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 });
}
}
// Подготавливаем данные для вставки сообщения пользователя
@@ -348,9 +405,32 @@ router.post('/message', requireAuth, upload.array('attachments'), async (req, re
let aiMessage = null;
if (messageContent) { // Только для текстовых сообщений
try {
// Получаем настройки ассистента
const aiSettings = await aiAssistantSettingsService.getSettings();
let rules = null;
if (aiSettings && aiSettings.rules_id) {
rules = await aiAssistantRulesService.getRuleById(aiSettings.rules_id);
}
logger.info('AI System Prompt:', aiSettings ? aiSettings.system_prompt : 'not set');
logger.info('AI Rules:', rules ? JSON.stringify(rules.rules) : 'not set');
// Получаем последние 10 сообщений из диалога для истории (до текущего сообщения)
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 detectedLanguage = language === 'auto' ? aiAssistant.detectLanguage(messageContent) : language;
logger.info('Getting AI response for:', messageContent);
const aiResponseContent = await aiAssistant.getResponse(messageContent, detectedLanguage);
const aiResponseContent = await aiAssistant.getResponse(
messageContent,
detectedLanguage,
history,
aiSettings ? aiSettings.system_prompt : '',
rules ? rules.rules : null
);
logger.info('AI response received' + (aiResponseContent ? '' : ' (empty)'), { conversationId });
if (aiResponseContent) {
@@ -396,6 +476,12 @@ router.post('/message', requireAuth, upload.array('attachments'), async (req, re
return formatted;
};
// Обновляем updated_at у диалога
await db.getQuery()(
'UPDATE conversations SET updated_at = NOW() WHERE id = $1',
[conversationId]
);
res.json({
success: true,
conversationId: conversationId,
@@ -541,6 +627,26 @@ router.get('/history', requireAuth, async (req, res) => {
}
});
// --- Новый роут для связывания гостя после аутентификации ---
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' });
}
});
// Экспортируем маршрутизатор и функцию processGuestMessages отдельно
module.exports = router;
module.exports.processGuestMessages = processGuestMessages;

View File

@@ -8,6 +8,8 @@ const authTokenService = require('../services/authTokenService');
const aiProviderSettingsService = require('../services/aiProviderSettingsService');
const aiAssistant = require('../services/ai-assistant');
const dns = require('node:dns').promises;
const aiAssistantSettingsService = require('../services/aiAssistantSettingsService');
const aiAssistantRulesService = require('../services/aiAssistantRulesService');
// Логируем версию ethers для отладки
logger.info(`Ethers version: ${ethers.version || 'unknown'}`);
@@ -239,4 +241,92 @@ router.post('/ai-settings/:provider/verify', requireAdmin, async (req, res, next
}
});
router.get('/ai-assistant', requireAdmin, async (req, res, next) => {
try {
const settings = await aiAssistantSettingsService.getSettings();
res.json({ success: true, settings });
} catch (error) {
next(error);
}
});
router.put('/ai-assistant', requireAdmin, async (req, res, next) => {
try {
const updated = await aiAssistantSettingsService.upsertSettings({ ...req.body, updated_by: req.session.userId || null });
res.json({ success: true, settings: updated });
} catch (error) {
next(error);
}
});
// Получить все наборы правил
router.get('/ai-assistant-rules', requireAdmin, async (req, res, next) => {
try {
const rules = await aiAssistantRulesService.getAllRules();
res.json({ success: true, rules });
} catch (error) {
next(error);
}
});
// Получить набор правил по id
router.get('/ai-assistant-rules/:id', requireAdmin, async (req, res, next) => {
try {
const rule = await aiAssistantRulesService.getRuleById(req.params.id);
res.json({ success: true, rule });
} catch (error) {
next(error);
}
});
// Создать набор правил
router.post('/ai-assistant-rules', requireAdmin, async (req, res, next) => {
try {
const created = await aiAssistantRulesService.createRule(req.body);
res.json({ success: true, rule: created });
} catch (error) {
next(error);
}
});
// Обновить набор правил
router.put('/ai-assistant-rules/:id', requireAdmin, async (req, res, next) => {
try {
const updated = await aiAssistantRulesService.updateRule(req.params.id, req.body);
res.json({ success: true, rule: updated });
} catch (error) {
next(error);
}
});
// Удалить набор правил
router.delete('/ai-assistant-rules/:id', requireAdmin, async (req, res, next) => {
try {
await aiAssistantRulesService.deleteRule(req.params.id);
res.json({ success: true });
} catch (error) {
next(error);
}
});
// Получить все email_settings для выпадающего списка
router.get('/email-settings', requireAdmin, async (req, res, next) => {
try {
const { rows } = await require('../db').getQuery()('SELECT id, from_email FROM email_settings ORDER BY id');
res.json({ success: true, items: rows });
} catch (error) {
next(error);
}
});
// Получить все telegram_settings для выпадающего списка
router.get('/telegram-settings', requireAdmin, async (req, res, next) => {
try {
const { rows } = await require('../db').getQuery()('SELECT id, bot_username FROM telegram_settings ORDER BY id');
res.json({ success: true, items: rows });
} catch (error) {
next(error);
}
});
module.exports = router;

View File

@@ -34,30 +34,51 @@ class AIAssistant {
}
// Основной метод для получения ответа
async getResponse(message, language = 'auto') {
async getResponse(message, language = 'auto', history = null, systemPrompt = '', rules = null) {
try {
console.log('getResponse called with:', { message, language });
console.log('getResponse called with:', { message, language, history, systemPrompt, rules });
// Определяем язык, если не указан явно
const detectedLanguage = language === 'auto' ? this.detectLanguage(message) : language;
console.log('Detected language:', detectedLanguage);
// Сначала пробуем прямой API запрос
// Формируем system prompt с учётом правил
let fullSystemPrompt = systemPrompt || '';
if (rules && typeof rules === 'object') {
fullSystemPrompt += '\n' + JSON.stringify(rules, null, 2);
}
// Формируем массив сообщений для Qwen2.5/OpenAI API
const messages = [];
if (fullSystemPrompt) {
messages.push({ role: 'system', content: fullSystemPrompt });
}
if (Array.isArray(history) && history.length > 0) {
for (const msg of history) {
if (msg.role && msg.content) {
messages.push({ role: msg.role, content: msg.content });
}
}
}
// Добавляем текущее сообщение пользователя
messages.push({ role: 'user', content: message });
// Пробуем прямой API запрос (OpenAI-совместимый endpoint)
try {
console.log('Trying direct API request...');
const response = await this.fallbackRequest(message, detectedLanguage);
const response = await this.fallbackRequestOpenAI(messages, detectedLanguage);
console.log('Direct API response received:', response);
return response;
} catch (error) {
console.error('Error in direct API request:', error);
}
// Если прямой запрос не удался, пробуем через ChatOllama
// Если прямой запрос не удался, пробуем через ChatOllama (склеиваем сообщения в текст)
const chat = this.createChat(detectedLanguage);
try {
const prompt = messages.map(m => `${m.role === 'user' ? 'Пользователь' : m.role === 'assistant' ? 'Ассистент' : 'Система'}: ${m.content}`).join('\n');
console.log('Sending request to ChatOllama...');
const response = await chat.invoke(message);
const response = await chat.invoke(prompt);
console.log('ChatOllama response:', response);
return response.content;
} catch (error) {
@@ -70,24 +91,17 @@ class AIAssistant {
}
}
// Альтернативный метод запроса через прямой API
async fallbackRequest(message, language) {
// Новый метод для OpenAI/Qwen2.5 совместимого endpoint
async fallbackRequestOpenAI(messages, language) {
try {
console.log('Using fallback request method with:', { message, language });
const systemPrompt =
language === 'ru'
? 'Вы - полезный ассистент. Отвечайте на русском языке.'
: 'You are a helpful assistant. Respond in English.';
console.log('Sending request to Ollama API...');
const response = await fetch(`${this.baseUrl}/api/generate`, {
console.log('Using fallbackRequestOpenAI with:', { messages, language });
const model = this.defaultModel;
const response = await fetch(`${this.baseUrl}/v1/chat/completions`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
model: this.defaultModel,
prompt: message,
system: systemPrompt,
model,
messages,
stream: false,
options: {
temperature: 0.7,
@@ -95,16 +109,17 @@ class AIAssistant {
},
}),
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
console.log('Ollama API response:', data);
return data.response;
// Qwen2.5/OpenAI API возвращает ответ в data.choices[0].message.content
if (data.choices && data.choices[0] && data.choices[0].message && data.choices[0].message.content) {
return data.choices[0].message.content;
}
return data.response || '';
} catch (error) {
console.error('Error in fallback request:', error);
console.error('Error in fallbackRequestOpenAI:', error);
throw error;
}
}

View File

@@ -0,0 +1,35 @@
const db = require('../db');
const TABLE = 'ai_assistant_rules';
async function getAllRules() {
const { rows } = await db.getQuery()(`SELECT * FROM ${TABLE} ORDER BY id`);
return rows;
}
async function getRuleById(id) {
const { rows } = await db.getQuery()(`SELECT * FROM ${TABLE} WHERE id = $1`, [id]);
return rows[0] || null;
}
async function createRule({ name, description, rules }) {
const { rows } = await db.getQuery()(
`INSERT INTO ${TABLE} (name, description, rules, created_at, updated_at)
VALUES ($1, $2, $3, NOW(), NOW()) RETURNING *`,
[name, description, rules]
);
return rows[0];
}
async function updateRule(id, { name, description, rules }) {
const { rows } = await db.getQuery()(
`UPDATE ${TABLE} SET name = $1, description = $2, rules = $3, updated_at = NOW() WHERE id = $4 RETURNING *`,
[name, description, rules, id]
);
return rows[0];
}
async function deleteRule(id) {
await db.getQuery()(`DELETE FROM ${TABLE} WHERE id = $1`, [id]);
}
module.exports = { getAllRules, getRuleById, createRule, updateRule, deleteRule };

View File

@@ -0,0 +1,48 @@
const db = require('../db');
const TABLE = 'ai_assistant_settings';
async function getSettings() {
const { rows } = await db.getQuery()(`SELECT * FROM ${TABLE} ORDER BY id LIMIT 1`);
const settings = rows[0] || null;
if (!settings) return null;
// Получаем связанные данные из telegram_settings и email_settings
let telegramBot = null;
let supportEmail = null;
if (settings.telegram_settings_id) {
const tg = await db.getQuery()('SELECT * FROM telegram_settings WHERE id = $1', [settings.telegram_settings_id]);
telegramBot = tg.rows[0] || null;
}
if (settings.email_settings_id) {
const em = await db.getQuery()('SELECT * FROM email_settings WHERE id = $1', [settings.email_settings_id]);
supportEmail = em.rows[0] || null;
}
return {
...settings,
telegramBot,
supportEmail
};
}
async function upsertSettings({ system_prompt, selected_rag_tables, languages, model, rules, updated_by, telegram_settings_id, email_settings_id, system_message }) {
const { rows } = await db.getQuery()(
`INSERT INTO ${TABLE} (id, system_prompt, selected_rag_tables, languages, model, rules, updated_at, updated_by, telegram_settings_id, email_settings_id, system_message)
VALUES (1, $1, $2, $3, $4, $5, NOW(), $6, $7, $8, $9)
ON CONFLICT (id) DO UPDATE SET
system_prompt = EXCLUDED.system_prompt,
selected_rag_tables = EXCLUDED.selected_rag_tables,
languages = EXCLUDED.languages,
model = EXCLUDED.model,
rules = EXCLUDED.rules,
updated_at = NOW(),
updated_by = EXCLUDED.updated_by,
telegram_settings_id = EXCLUDED.telegram_settings_id,
email_settings_id = EXCLUDED.email_settings_id,
system_message = EXCLUDED.system_message
RETURNING *`,
[system_prompt, selected_rag_tables, languages, model, rules, updated_by, telegram_settings_id, email_settings_id, system_message]
);
return rows[0];
}
module.exports = { getSettings, upsertSettings };