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

This commit is contained in:
2025-06-24 16:45:50 +03:00
parent 45cde76e85
commit 261a4ecb2a
17 changed files with 1209 additions and 155 deletions

View File

@@ -351,9 +351,9 @@ router.post('/message', requireAuth, upload.array('attachments'), async (req, re
} else {
// Обычный пользователь — только в свой диалог
convResult = await db.getQuery()(
'SELECT * FROM conversations WHERE id = $1 AND user_id = $2',
[conversationId, userId]
);
'SELECT * FROM conversations WHERE id = $1 AND user_id = $2',
[conversationId, userId]
);
}
if (convResult.rows.length === 0) {
logger.warn('Conversation not found or access denied', { conversationId, userId });

View File

@@ -2,6 +2,8 @@ const express = require('express');
const router = express.Router();
const db = require('../db');
const { broadcastMessagesUpdate } = require('../wsHub');
const telegramBot = require('../services/telegramBot');
const emailBot = new (require('../services/emailBot'))();
// GET /api/messages?userId=123
router.get('/', async (req, res) => {
@@ -42,11 +44,100 @@ router.get('/', async (req, res) => {
router.post('/', async (req, res) => {
const { user_id, sender_type, content, channel, role, direction, attachment_filename, attachment_mimetype, attachment_size, attachment_data, metadata } = req.body;
try {
const result = await db.getQuery()(
`INSERT INTO messages (user_id, sender_type, content, channel, role, direction, created_at, attachment_filename, attachment_mimetype, attachment_size, attachment_data, metadata)
VALUES ($1,$2,$3,$4,$5,$6,NOW(),$7,$8,$9,$10,$11) RETURNING *`,
[user_id, sender_type, content, channel, role, direction, attachment_filename, attachment_mimetype, attachment_size, attachment_data, metadata]
// Проверка наличия идентификатора для выбранного канала
if (channel === 'email') {
const emailIdentity = await db.getQuery()(
'SELECT provider_id FROM user_identities WHERE user_id = $1 AND provider = $2 LIMIT 1',
[user_id, 'email']
);
if (emailIdentity.rows.length === 0) {
return res.status(400).json({ error: 'У пользователя не указан email. Сообщение не отправлено.' });
}
}
if (channel === 'telegram') {
const tgIdentity = await db.getQuery()(
'SELECT provider_id FROM user_identities WHERE user_id = $1 AND provider = $2 LIMIT 1',
[user_id, 'telegram']
);
if (tgIdentity.rows.length === 0) {
return res.status(400).json({ error: 'У пользователя не привязан Telegram. Сообщение не отправлено.' });
}
}
if (channel === 'wallet' || channel === 'web3' || channel === 'web') {
const walletIdentity = await db.getQuery()(
'SELECT provider_id FROM user_identities WHERE user_id = $1 AND provider = $2 LIMIT 1',
[user_id, 'wallet']
);
if (walletIdentity.rows.length === 0) {
return res.status(400).json({ error: 'У пользователя не привязан кошелёк. Сообщение не отправлено.' });
}
}
// 1. Проверяем, есть ли беседа для user_id
let conversationResult = await db.getQuery()(
'SELECT * FROM conversations WHERE user_id = $1 ORDER BY updated_at DESC, created_at DESC LIMIT 1',
[user_id]
);
let conversation;
if (conversationResult.rows.length === 0) {
// 2. Если нет — создаём новую беседу
const title = `Чат с пользователем ${user_id}`;
const newConv = await db.getQuery()(
'INSERT INTO conversations (user_id, title, created_at, updated_at) VALUES ($1, $2, NOW(), NOW()) RETURNING *',
[user_id, title]
);
conversation = newConv.rows[0];
} else {
conversation = conversationResult.rows[0];
}
// 3. Сохраняем сообщение с conversation_id
const result = await db.getQuery()(
`INSERT INTO messages (user_id, conversation_id, sender_type, content, channel, role, direction, created_at, attachment_filename, attachment_mimetype, attachment_size, attachment_data, metadata)
VALUES ($1,$2,$3,$4,$5,$6,$7,NOW(),$8,$9,$10,$11,$12) RETURNING *`,
[user_id, conversation.id, sender_type, content, channel, role, direction, attachment_filename, attachment_mimetype, attachment_size, attachment_data, metadata]
);
// 4. Если это исходящее сообщение для Telegram — отправляем через бота
if (channel === 'telegram' && direction === 'out') {
try {
console.log(`[messages.js] Попытка отправки сообщения в Telegram для user_id=${user_id}`);
// Получаем Telegram ID пользователя
const tgIdentity = await db.getQuery()(
'SELECT provider_id FROM user_identities WHERE user_id = $1 AND provider = $2 LIMIT 1',
[user_id, 'telegram']
);
console.log(`[messages.js] Результат поиска Telegram ID:`, tgIdentity.rows);
if (tgIdentity.rows.length > 0) {
const telegramId = tgIdentity.rows[0].provider_id;
console.log(`[messages.js] Отправка сообщения в Telegram ID: ${telegramId}, текст: ${content}`);
const bot = await telegramBot.getBot();
try {
const sendResult = await bot.telegram.sendMessage(telegramId, content);
console.log(`[messages.js] Результат отправки в Telegram:`, sendResult);
} catch (sendErr) {
console.error(`[messages.js] Ошибка при отправке в Telegram:`, sendErr);
}
} else {
console.warn(`[messages.js] Не найден Telegram ID для user_id=${user_id}`);
}
} catch (err) {
console.error('[messages.js] Ошибка отправки сообщения в Telegram:', err);
}
}
// 5. Если это исходящее сообщение для Email — отправляем email
if (channel === 'email' && direction === 'out') {
try {
// Получаем email пользователя
const emailIdentity = await db.getQuery()(
'SELECT provider_id FROM user_identities WHERE user_id = $1 AND provider = $2 LIMIT 1',
[user_id, 'email']
);
if (emailIdentity.rows.length > 0) {
const email = emailIdentity.rows[0].provider_id;
await emailBot.sendEmail(email, 'Новое сообщение', content);
}
} catch (err) {
console.error('[messages.js] Ошибка отправки email:', err);
}
}
broadcastMessagesUpdate();
res.json({ success: true, message: result.rows[0] });
} catch (e) {
@@ -123,4 +214,90 @@ router.get('/conversations', async (req, res) => {
}
});
// Массовая рассылка сообщения во все каналы пользователя
router.post('/broadcast', async (req, res) => {
const { user_id, content } = req.body;
if (!user_id || !content) {
return res.status(400).json({ error: 'user_id и content обязательны' });
}
try {
// Получаем все идентификаторы пользователя
const identitiesRes = await db.getQuery()(
'SELECT provider, provider_id FROM user_identities WHERE user_id = $1',
[user_id]
);
const identities = identitiesRes.rows;
// --- Найти или создать беседу (conversation) ---
let conversationResult = await db.getQuery()(
'SELECT * FROM conversations WHERE user_id = $1 ORDER BY updated_at DESC, created_at DESC LIMIT 1',
[user_id]
);
let conversation;
if (conversationResult.rows.length === 0) {
const title = `Чат с пользователем ${user_id}`;
const newConv = await db.getQuery()(
'INSERT INTO conversations (user_id, title, created_at, updated_at) VALUES ($1, $2, NOW(), NOW()) RETURNING *',
[user_id, title]
);
conversation = newConv.rows[0];
} else {
conversation = conversationResult.rows[0];
}
const results = [];
let sent = false;
// Email
const email = identities.find(i => i.provider === 'email')?.provider_id;
if (email) {
try {
await emailBot.sendEmail(email, 'Новое сообщение', content);
// Сохраняем в messages с conversation_id
await db.getQuery()(
`INSERT INTO messages (user_id, conversation_id, sender_type, content, channel, role, direction, created_at, metadata)
VALUES ($1, $2, $3, $4, $5, $6, $7, NOW(), $8)`,
[user_id, conversation.id, 'admin', content, 'email', 'user', 'out', JSON.stringify({ broadcast: true })]
);
results.push({ channel: 'email', status: 'sent' });
sent = true;
} catch (err) {
results.push({ channel: 'email', status: 'error', error: err.message });
}
}
// Telegram
const telegram = identities.find(i => i.provider === 'telegram')?.provider_id;
if (telegram) {
try {
const bot = await telegramBot.getBot();
await bot.telegram.sendMessage(telegram, content);
await db.getQuery()(
`INSERT INTO messages (user_id, conversation_id, sender_type, content, channel, role, direction, created_at, metadata)
VALUES ($1, $2, $3, $4, $5, $6, $7, NOW(), $8)`,
[user_id, conversation.id, 'admin', content, 'telegram', 'user', 'out', JSON.stringify({ broadcast: true })]
);
results.push({ channel: 'telegram', status: 'sent' });
sent = true;
} catch (err) {
results.push({ channel: 'telegram', status: 'error', error: err.message });
}
}
// Wallet/web3
const wallet = identities.find(i => i.provider === 'wallet')?.provider_id;
if (wallet) {
// Здесь можно реализовать отправку через web3, если нужно
await db.getQuery()(
`INSERT INTO messages (user_id, conversation_id, sender_type, content, channel, role, direction, created_at, metadata)
VALUES ($1, $2, $3, $4, $5, $6, $7, NOW(), $8)`,
[user_id, conversation.id, 'admin', content, 'wallet', 'user', 'out', JSON.stringify({ broadcast: true })]
);
results.push({ channel: 'wallet', status: 'saved' });
sent = true;
}
if (!sent) {
return res.status(400).json({ error: 'У пользователя нет ни одного канала для рассылки.' });
}
res.json({ success: true, results });
} catch (e) {
res.status(500).json({ error: 'Broadcast error', details: e.message });
}
});
module.exports = router;

31
backend/routes/rag.js Normal file
View File

@@ -0,0 +1,31 @@
const express = require('express');
const router = express.Router();
const { ragAnswer, generateLLMResponse } = require('../services/ragService');
router.post('/answer', async (req, res) => {
const { tableId, question, userTags, product, systemPrompt, priority, date, rules, history, model, language } = req.body;
try {
const ragResult = await ragAnswer({ tableId, userQuestion: question, userTags, product });
const llmResponse = await generateLLMResponse({
userQuestion: question,
context: ragResult.context,
clarifyingAnswer: ragResult.clarifyingAnswer,
objectionAnswer: ragResult.objectionAnswer,
answer: ragResult.answer,
systemPrompt,
userTags: userTags?.join ? userTags.join(', ') : userTags,
product,
priority: priority || ragResult.priority,
date: date || ragResult.date,
rules,
history,
model,
language
});
res.json({ ...ragResult, llmResponse });
} catch (e) {
res.status(500).json({ error: e.message });
}
});
module.exports = router;

View File

@@ -53,10 +53,19 @@ router.get('/:id', async (req, res, next) => {
router.post('/:id/columns', async (req, res, next) => {
try {
const tableId = req.params.id;
const { name, type, options, order } = req.body;
const { name, type, options, order, tagIds, purpose } = req.body;
let finalOptions = options;
// Собираем options
finalOptions = finalOptions || {};
if (type === 'tags' && Array.isArray(tagIds)) {
finalOptions.tagIds = tagIds;
}
if (purpose) {
finalOptions.purpose = purpose;
}
const result = await db.getQuery()(
'INSERT INTO user_columns (table_id, name, type, options, "order") VALUES ($1, $2, $3, $4, $5) RETURNING *',
[tableId, name, type, options ? JSON.stringify(options) : null, order || 0]
[tableId, name, type, finalOptions ? JSON.stringify(finalOptions) : null, order || 0]
);
res.json(result.rows[0]);
} catch (err) {

View File

@@ -52,30 +52,126 @@ router.put('/profile', requireAuth, async (req, res) => {
});
*/
// Получение списка пользователей с контактами
router.get('/', async (req, res, next) => {
// Получение списка пользователей с фильтрацией
router.get('/', requireAuth, async (req, res, next) => {
try {
const usersResult = await db.getQuery()('SELECT id, first_name, last_name, created_at, preferred_language FROM users ORDER BY id');
const users = usersResult.rows;
// Получаем все user_identities разом
const identitiesResult = await db.getQuery()('SELECT user_id, provider, provider_id FROM user_identities');
const identities = identitiesResult.rows;
// Группируем идентификаторы по user_id
const identityMap = {};
for (const id of identities) {
if (!identityMap[id.user_id]) identityMap[id.user_id] = {};
if (!identityMap[id.user_id][id.provider]) identityMap[id.user_id][id.provider] = id.provider_id;
const {
tagIds = '',
dateFrom = '',
dateTo = '',
contactType = 'all',
search = '',
newMessages = ''
} = req.query;
const adminId = req.user && req.user.id;
// --- Формируем условия ---
const where = [];
const params = [];
let idx = 1;
// Фильтр по дате
if (dateFrom) {
where.push(`DATE(u.created_at) >= $${idx++}`);
params.push(dateFrom);
}
// Собираем контакты
if (dateTo) {
where.push(`DATE(u.created_at) <= $${idx++}`);
params.push(dateTo);
}
// Фильтр по типу контакта
if (contactType !== 'all') {
where.push(`EXISTS (
SELECT 1 FROM user_identities ui
WHERE ui.user_id = u.id AND ui.provider = $${idx++}
)`);
params.push(contactType);
}
// Фильтр по поиску
if (search) {
where.push(`(
LOWER(u.first_name) LIKE $${idx} OR
LOWER(u.last_name) LIKE $${idx} OR
EXISTS (SELECT 1 FROM user_identities ui WHERE ui.user_id = u.id AND LOWER(ui.provider_id) LIKE $${idx})
)`);
params.push(`%${search.toLowerCase()}%`);
idx++;
}
// --- Основной SQL ---
let sql = `
SELECT u.id, u.first_name, u.last_name, u.created_at, u.preferred_language,
(SELECT provider_id FROM user_identities WHERE user_id = u.id AND provider = 'email' LIMIT 1) AS email,
(SELECT provider_id FROM user_identities WHERE user_id = u.id AND provider = 'telegram' LIMIT 1) AS telegram,
(SELECT provider_id FROM user_identities WHERE user_id = u.id AND provider = 'wallet' LIMIT 1) AS wallet
FROM users u
`;
// Фильтрация по тегам
if (tagIds) {
const tagIdArr = tagIds.split(',').map(Number).filter(Boolean);
if (tagIdArr.length > 0) {
sql += `
JOIN user_tags ut ON ut.user_id = u.id
WHERE ut.tag_id = ANY($${idx++})
GROUP BY u.id
HAVING COUNT(DISTINCT ut.tag_id) = $${idx++}
`;
params.push(tagIdArr);
params.push(tagIdArr.length);
}
} else if (where.length > 0) {
sql += ` WHERE ${where.join(' AND ')} `;
}
if (!tagIds) {
sql += ' ORDER BY u.id ';
}
// --- Выполняем запрос ---
const usersResult = await db.getQuery()(sql, params);
let users = usersResult.rows;
// --- Фильтрация по новым сообщениям ---
if (newMessages === 'yes' && adminId) {
// Получаем время последнего прочтения для каждого пользователя
const readRes = await db.getQuery()(
'SELECT user_id, last_read_at FROM admin_read_messages WHERE admin_id = $1',
[adminId]
);
const readMap = {};
for (const row of readRes.rows) {
readMap[row.user_id] = row.last_read_at;
}
// Получаем последнее сообщение для каждого пользователя
const msgRes = await db.getQuery()(
`SELECT user_id, MAX(created_at) as last_msg_at FROM messages GROUP BY user_id`
);
const msgMap = {};
for (const row of msgRes.rows) {
msgMap[row.user_id] = row.last_msg_at;
}
// Оставляем только тех, у кого есть новые сообщения
users = users.filter(u => {
const lastRead = readMap[u.id];
const lastMsg = msgMap[u.id];
return lastMsg && (!lastRead || new Date(lastMsg) > new Date(lastRead));
});
}
// --- Формируем ответ ---
const contacts = users.map(u => ({
id: u.id,
name: [u.first_name, u.last_name].filter(Boolean).join(' ') || null,
email: identityMap[u.id]?.email || null,
telegram: identityMap[u.id]?.telegram || null,
wallet: identityMap[u.id]?.wallet || null,
email: u.email || null,
telegram: u.telegram || null,
wallet: u.wallet || null,
created_at: u.created_at,
preferred_language: u.preferred_language || []
}));
res.json({ success: true, contacts });
} catch (error) {
logger.error('Error fetching contacts:', error);