ваше сообщение коммита
This commit is contained in:
@@ -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 });
|
||||
|
||||
@@ -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
31
backend/routes/rag.js
Normal 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;
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user